使用 AVX512 加速聚合 MD5 哈希运算高达 800%
简介
虽然在考虑哈希函数时,MD5 哈希不再是好的选择,但它仍然被广泛应用于各种应用中。因此,对 MD5 哈希速度进行的任何性能改进都值得考虑。
由于 SIMD 处理(AVX2,尤其是 AVX512)的最新改进,我们提供了一个 Go md5-simd 包,它可以将 AVX2 上的 MD5 哈希聚合加速高达 400%,将 AVX512 上的 MD5 哈希聚合加速高达 800%。
此性能提升是通过在单个 CPU 内核上并行运行 8 个(AVX2)或 16 个(AVX512)独立的 MD5 求和来实现的。
务必了解,md5-simd
**不会加速**单个 MD5 哈希和。相反,它允许在同一个 CPU 内核上并行计算多个**独立**的 MD5 和,从而更有效地利用计算资源。
性能
下图比较了 crypto/md5
与 AVX2 与 AVX512 代码之间的性能

与 crypto/md5
相比,AVX2 版本的速度提高了 4 倍
$ benchcmp crypto-md5.txt avx2.txt
benchmark old MB/s new MB/s speedup
BenchmarkParallel/32KB-4 2229.22 7370.50 3.31x
BenchmarkParallel/64KB-4 2233.61 8248.46 3.69x
BenchmarkParallel/128KB-4 2235.43 8660.74 3.87x
BenchmarkParallel/256KB-4 2236.39 8863.87 3.96x
BenchmarkParallel/512KB-4 2238.05 8985.39 4.01x
BenchmarkParallel/1MB-4 2233.56 9042.62 4.05x
BenchmarkParallel/2MB-4 2224.11 9014.46 4.05x
BenchmarkParallel/4MB-4 2199.78 8993.61 4.09x
BenchmarkParallel/8MB-4 2182.48 8748.22 4.01x
与 crypto/md5
相比,AVX512 版本的速度提高了 8 倍
$ benchcmp crypto-md5.txt avx512.txt
benchmark old MB/s new MB/s speedup
BenchmarkParallel/32KB-4 2229.22 11605.78 5.21x
BenchmarkParallel/64KB-4 2233.61 14329.65 6.42x
BenchmarkParallel/128KB-4 2235.43 16166.39 7.23x
BenchmarkParallel/256KB-4 2236.39 15570.09 6.96x
BenchmarkParallel/512KB-4 2238.05 16705.83 7.46x
BenchmarkParallel/1MB-4 2233.56 16941.95 7.59x
BenchmarkParallel/2MB-4 2224.11 17136.01 7.70x
BenchmarkParallel/4MB-4 2199.78 17218.61 7.83x
BenchmarkParallel/8MB-4 2182.48 17252.88 7.91x
设计
md5-simd
具有 AVX2(8 通道并行)和 AVX512(16 通道并行版本)算法来加速计算,并具有以下函数定义
//go:noescape
func block8(state *uint32, base uintptr, bufs *int32, cache *byte, n int)
//go:noescape
func block16(state *uint32, ptrs *int64, mask uint64, n int)
AVX2 版本基于 md5vec 存储库,除了细微(美观)的更改外,基本上没有改变。
AVX512 版本源自 AVX2 版本,但增加了一些进一步的优化和简化。
在较高 ZMM 寄存器中缓存
AVX2 版本传入一个 cache8
内存块(约 0.5 KB),用于在 ROUND1
期间临时存储中间结果,这些结果随后在 ROUND2
到 ROUND4
期间使用。
由于 AVX512 拥有两倍数量的寄存器(32 个 ZMM 寄存器,而 YMM 寄存器为 16 个),因此可以使用上部的 16 个 ZMM 寄存器来保持 CPU 上的中间状态。因此,无需将相应的 cache16
传入 AVX512 块函数。
使用 64 位指针直接加载
AVX2 使用 VPGATHERDD
指令(用于 YMM)使用(8 个独立的)32 位偏移量并行加载 8 个通道。由于无法控制传入 (Go) blockMd5
函数的所有 8 个切片在内存中的布局方式,因此无法为所有 8 个切片推导出“基址”和相应的偏移量(都在 32 位内)。
因此,AVX2 版本使用一个临时缓冲区来收集来自所有 8 个输入切片的要哈希的字节切片,并将此缓冲区与(固定)32 位偏移量一起传递到汇编代码中。
对于 AVX512 版本,不需要此临时缓冲区,因为 AVX512 代码使用一对 VPGATHERQD
指令来直接解除对 64 位指针的引用(从初始化为零的基寄存器地址)。
请注意,需要两个加载(收集)指令,因为 AVX512 版本并行处理 16 个通道,需要总共 16 倍 64 位 = 1024 位才能加载。随后,一个简单的 VALIGND
和 VPORD
用于将上下两部分合并到一个 ZMM 寄存器中(其中包含 16 个通道的 32 位 DWORDS)。
掩码支持
由于指针是从 Go 切片中直接传入的,因此我们需要防止空指针。为此,在 AVX512 汇编代码中传入一个 16 位掩码,该掩码在 VPGATHERQD
指令期间用于屏蔽掉可能导致段错误的通道。
操作
为了使操作尽可能简单,有一个“服务器”协调各个哈希状态并在有新数据传入时更新它们。这可以可视化为如下所示

每当有数据可用时,服务器将最多收集 16 个哈希的数据,并并行处理所有 16 个通道。这意味着如果 16 个哈希都有数据可用,则所有通道都将被填充。但是,由于可能并非如此,服务器将填充较少的通道并无论如何进行一轮处理。通道也可以部分填充。

在上图(简化)示例中,4 个通道已完全填充,2 个通道已部分填充,最后 2 个通道为空。黑色区域将简单地从结果中屏蔽并忽略(请注意,实际实现每个服务器使用 16 个通道)。
开源
源代码在 MIT 许可证下开源,可在 github 上的存储库 md5-simd 中获得。试一试,我们欢迎任何反馈。