175 lines
4.9 KiB
Markdown
175 lines
4.9 KiB
Markdown
# MD5优化误区:性能不是关键,内存才重要
|
||
|
||
在 Go 语言开发中,MD5计算是一个常见的操作。再过去的使用中,我总会想方设法优化 MD5 的计算性能,然而,通过深入的性能测试和源码分析,发现:**MD5计算本身并不需要性能优化,真正的优化点在于调用者的内存使用**。
|
||
|
||
## 性能测试结果对比
|
||
|
||
让我们先来看一组 benchmark 测试结果,测试对象是1MB的字符串数据:
|
||
|
||
```
|
||
BenchmarkMD5/segment_md5-8 81 42623578 ns/op 1120 B/op 4 allocs/op
|
||
BenchmarkMD5/md5-8 84 43422871 ns/op 32 B/op 1 allocs/op
|
||
BenchmarkMD5/io_md5-8 85 41750913 ns/op 192 B/op 4 allocs/op
|
||
```
|
||
|
||
从结果可以看出几个关键点:
|
||
|
||
1. **性能差异微乎其微**:三种方式的执行时间都在4千万纳秒左右,差异不到5%
|
||
2. **内存分配差异显著**:
|
||
- `md5(直接调用基础库)`: 仅32字节分配,1次分配
|
||
- `io_md5(使用 io.Copy)`: 92字节分配,4次分配
|
||
- `segment_md5(对字节数组分片)`: 1120字节分配,4次分配
|
||
|
||
## 测试的三种MD5实现方式
|
||
|
||
让我们看看这三种不同的实现方式:
|
||
|
||
```go
|
||
// 方式1:直接计算
|
||
func MD5(s []byte) string {
|
||
b := md5.Sum(s)
|
||
return hex.EncodeToString(b[:])
|
||
}
|
||
|
||
// 方式2:分段读取
|
||
func SegmentMD5(r io.Reader) (string, error) {
|
||
h := md5.New()
|
||
buf := make([]byte, 1*1024) // 1KB缓冲区
|
||
for {
|
||
n, err := r.Read(buf)
|
||
if n > 0 {
|
||
if _, err := h.Write(buf[:n]); err != nil {
|
||
return "", err
|
||
}
|
||
}
|
||
if err != nil {
|
||
if errors.Is(err, io.EOF) {
|
||
break
|
||
}
|
||
return "", err
|
||
}
|
||
}
|
||
return hex.EncodeToString(h.Sum(nil)), nil
|
||
}
|
||
|
||
// 方式3:使用io.Copy
|
||
func IOMD5(r io.Reader) (string, error) {
|
||
h := md5.New()
|
||
if _,err := io.Copy(h, r); err != nil {
|
||
return "",err
|
||
}
|
||
return hex.EncodeToString(h.Sum(nil)), nil
|
||
}
|
||
```
|
||
|
||
## 为什么MD5计算不需要性能优化?
|
||
|
||
### 底层算法的真正优化
|
||
|
||
Go标准库的MD5实现已经经过高度优化。让我们看看真正的`Write`函数实现:
|
||
|
||
```go
|
||
func (d *digest) Write(p []byte) (nn int, err error) {
|
||
// ....
|
||
if len(p) >= BlockSize {
|
||
n := len(p) &^ (BlockSize - 1)
|
||
if haveAsm {
|
||
for n > maxAsmSize {
|
||
block(d, p[:maxAsmSize])
|
||
p = p[maxAsmSize:]
|
||
n -= maxAsmSize
|
||
}
|
||
block(d, p[:n])
|
||
} else {
|
||
blockGeneric(d, p[:n])
|
||
}
|
||
p = p[n:]
|
||
}
|
||
if len(p) > 0 {
|
||
d.nx = copy(d.x[:], p)
|
||
}
|
||
return
|
||
}
|
||
```
|
||
|
||
### 优化点详解
|
||
|
||
|
||
**大数据处理优化**:
|
||
```go
|
||
for n > maxAsmSize {
|
||
block(d, p[:maxAsmSize])
|
||
p = p[maxAsmSize:]
|
||
n -= maxAsmSize
|
||
}
|
||
```
|
||
|
||
对超过64KB(maxAsmSize)的数据进行分批处理,这是因为汇编实现是非抢占式的,不能被中断。
|
||
|
||
**汇编优化**:
|
||
```go
|
||
if haveAsm {
|
||
block(d, d.x[:]) // 优化的汇编版本
|
||
} else {
|
||
blockGeneric(d, d.x[:]) // 通用Go版本
|
||
}
|
||
```
|
||
|
||
在支持的平台(如amd64)上,使用专门优化的汇编实现`block`,在其他平台使用通用版本`blockGeneric`。
|
||
|
||
在amd64平台上,`block`函数使用汇编实现:
|
||
|
||
```asm
|
||
TEXT ·block(SB), NOSPLIT, $8-32
|
||
MOVQ dig+0(FP), BP
|
||
MOVQ p_base+8(FP), SI
|
||
MOVQ p_len+16(FP), DX
|
||
SHRQ $0x06, DX // 除以64,计算块数
|
||
SHLQ $0x06, DX // 乘以64,计算总字节数
|
||
// ... 优化的MD5算法实现
|
||
```
|
||
|
||
汇编版本通过直接操作CPU寄存器,避免了Go函数调用的开销,并且使用了SIMD指令等底层优化。
|
||
|
||
## 上层优化的正确姿势
|
||
|
||
既然 MD5 计算本身不需要优化,我们应该将注意力放在上层调用上:
|
||
|
||
### 选择合适的读取方式
|
||
|
||
```go
|
||
// 推荐:处理大文件时使用流式处理
|
||
func ProcessLargeFile(filename string) (string, error) {
|
||
file, err := os.Open(filename)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
defer file.Close()
|
||
|
||
h := md5.New()
|
||
if _, err := io.Copy(h, file); err != nil {
|
||
return "", err
|
||
}
|
||
|
||
return hex.EncodeToString(h.Sum(nil)), nil
|
||
}
|
||
|
||
// 不推荐:一次性读取大文件
|
||
func ProcessLargeFileBad(filename string) (string, error) {
|
||
data, err := os.ReadFile(filename) // 可能消耗大量内存
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
|
||
return MD5(data), nil
|
||
}
|
||
```
|
||
|
||
在处理大文件时,流式处理相比一次性读取的优势在于不必一次性分配大量内存。
|
||
|
||
## 总结
|
||
|
||
对于 MD5 计算,标准库的实现已经是最佳选择。我们要做的是在上层调用上做出明智的选择,避免不必要的内存开销。
|
||
|
||
想要了解更多Go语言中的优化实践,欢迎访问 [goddd](https://github.com/ixugo/goddd) web 框架模板,这里有更多关于Go语言开发的最佳实践。
|