使用 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 期间临时存储中间结果,这些结果随后在 ROUND2ROUND4 期间使用。

由于 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 位才能加载。随后,一个简单的 VALIGNDVPORD 用于将上下两部分合并到一个 ZMM 寄存器中(其中包含 16 个通道的 32 位 DWORDS)。

掩码支持

由于指针是从 Go 切片中直接传入的,因此我们需要防止空指针。为此,在 AVX512 汇编代码中传入一个 16 位掩码,该掩码在 VPGATHERQD 指令期间用于屏蔽掉可能导致段错误的通道。

操作

为了使操作尽可能简单,有一个“服务器”协调各个哈希状态并在有新数据传入时更新它们。这可以可视化为如下所示

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

在上图(简化)示例中,4 个通道已完全填充,2 个通道已部分填充,最后 2 个通道为空。黑色区域将简单地从结果中屏蔽并忽略(请注意,实际实现每个服务器使用 16 个通道)。

开源

源代码在 MIT 许可证下开源,可在 github 上的存储库 md5-simd 中获得。试一试,我们欢迎任何反馈。