- VisualStudio2022插件的安装及使用-编程手把手系列文章
- pprof-在现网场景怎么用
- C#实现的下拉多选框,下拉多选树,多级节点
- 【学习笔记】基础数据结构:猫树
原文:blog.fanscore.cn/a/62/ 。
前一段时间公司上线了一套Go实现的推荐系统,上线后发现MMR层虽然只有纯计算但耗时十分离谱,通过pprof定位问题所在之后进行了优化,虽然降低了非常多但是我们认为其中还有优化空间.
可以看到日常平均耗时126ms,P95 360ms.
MMR层主要耗时集中在了余弦相似度的计算部分,这部分我们使用的gonum库进行计算,其底层在x86平台上利用了SSE指令集进行了加速.
SSE指令集已经非常古老了,xmm寄存器只能存储两个双精度浮点数,每次只能并行进行两个双精度浮点数的计算,而AVX2指令集可以并行计算四个,理论上可以获得两倍的性能提升,因此我们决定自己使用AVX2指令集手写汇编的方式替代掉gonum库.
余弦相似度的计算公式为 。
对应的代码为 。
import "gonum.org/v1/gonum/floats"
func CosineSimilarity(a, b []float64) float64 {
dotProduct := floats.Dot(a, b) // 计算a和b的点积
normA := floats.Norm(a, 2) // 计算向量a的L2范数
normB := floats.Norm(b, 2) // 计算向量b的L2范数
return dotProduct / (normA * normB)
}
gonum点积计算Dot的部分汇编代码如下:
TEXT ·DotUnitary(SB), NOSPLIT, $0
...
loop_uni:
// sum += x[i] * y[i] unrolled 4x.
MOVUPD 0(R8)(SI*8), X0
MOVUPD 0(R9)(SI*8), X1
MOVUPD 16(R8)(SI*8), X2
MOVUPD 16(R9)(SI*8), X3
MULPD X1, X0
MULPD X3, X2
ADDPD X0, X7
ADDPD X2, X8
ADDQ $4, SI // i += 4
SUBQ $4, DI // n -= 4
JGE loop_uni // if n >= 0 goto loop_uni
...
end_uni:
ADDPD X8, X7
MOVSD X7, X0
UNPCKHPD X7, X7
ADDSD X0, X7
MOVSD X7, sum+48(FP) // Return final sum.
RET
可以看到其中使用xmm寄存器并行计算两个双精度浮点数,并且还采用了循环展开的优化手段,一个循环中同时进行4个元素的计算.
我们利用AVX2指令集并行计算四个双精度浮点数进行加速 。
loop_uni:
// sum += x[i] * y[i] unrolled 8x.
VMOVUPD 0(R8)(SI*8), Y0 // Y0 = x[i:i+4]
VMOVUPD 0(R9)(SI*8), Y1 // Y1 = y[i:i+4]
VMOVUPD 32(R8)(SI*8), Y2 // Y2 = x[i+4:i+8]
VMOVUPD 32(R9)(SI*8), Y3 // Y3 = x[i+4:i+8]
VMOVUPD 64(R8)(SI*8), Y4 // Y4 = x[i+8:i+12]
VMOVUPD 64(R9)(SI*8), Y5 // Y5 = y[i+8:i+12]
VMOVUPD 96(R8)(SI*8), Y6 // Y6 = x[i+12:i+16]
VMOVUPD 96(R9)(SI*8), Y7 // Y7 = x[i+12:i+16]
VFMADD231PD Y0, Y1, Y8 // Y8 = Y0 * Y1 + Y8
VFMADD231PD Y2, Y3, Y9
VFMADD231PD Y4, Y5, Y10
VFMADD231PD Y6, Y7, Y11
ADDQ $16, SI // i += 16
CMPQ DI, SI
JG loop_uni // if len(x) > i goto loop_uni
可以看到我们每个循环中同时用到8个ymm寄存器即一次循环计算16个数,而且还用到了VFMADD231PD指令同时进行乘法累积的计算.
最终Benchmark结果:
BenchmarkDot 一个循环中计算8个数
BenchmarkDot-2 14994770 78.85 ns/op
BenchmarkDot16 一个循环中计算16个数
BenchmarkDot16-2 22867993 53.46 ns/op
BenchmarkGonumDot Gonum点积计算
BenchmarkGonumDot-2 8264486 144.4 ns/op
可以看到点积部分我们得到了大约2.7倍的性能提升 。
gonum库中进行L2范数计算的算法并不是常规的a1^2 + a2^2 ... + aN^2这种计算,而是采用了Netlib算法,减少了溢出和下溢,其Go源码如下:
func L2NormUnitary(x []float64) (norm float64) {
var scale float64
sumSquares := 1.0
for _, v := range x {
if v == 0 {
continue
}
absxi := math.Abs(v)
if math.IsNaN(absxi) {
return math.NaN()
}
if scale < absxi {
s := scale / absxi
sumSquares = 1 + sumSquares*s*s
scale = absxi
} else {
s := absxi / scale
sumSquares += s * s
}
}
if math.IsInf(scale, 1) {
return math.Inf(1)
}
return scale * math.Sqrt(sumSquares)
}
其汇编代码比较晦涩难懂,但管中窥豹再结合Go源码可以看出来没有用到并行能力,每次循环只计算一个数 。
TEXT ·L2NormUnitary(SB), NOSPLIT, $0
...
loop:
MOVSD (X_)(IDX*8), ABSX // absxi = x[i]
...
我们优化之后的核心代码如下:
loop:
VMOVUPD 0(R8)(SI*8), Y0 // Y0 = x[i:i+4]
VMOVUPD 32(R8)(SI*8), Y1 // Y1 = y[i+4:i+8]
VMOVUPD 64(R8)(SI*8), Y2 // Y2 = x[i+8:i+12]
VMOVUPD 96(R8)(SI*8), Y3 // Y3 = x[i+12:i+16]
VMOVUPD 128(R8)(SI*8), Y4 // Y4 = x[i+16:i+20]
VMOVUPD 160(R8)(SI*8), Y5 // Y5 = y[i+20:i+24]
VMOVUPD 192(R8)(SI*8), Y6 // Y6 = x[i+24:i+28]
VMOVUPD 224(R8)(SI*8), Y7 // Y7 = x[i+28:i+32]
VFMADD231PD Y0, Y0, Y8 // Y8 = Y0 * Y0 + Y8
VFMADD231PD Y1, Y1, Y9
VFMADD231PD Y2, Y2, Y10
VFMADD231PD Y3, Y3, Y11
VFMADD231PD Y4, Y4, Y12
VFMADD231PD Y5, Y5, Y13
VFMADD231PD Y6, Y6, Y14
VFMADD231PD Y7, Y7, Y15
ADDQ $32, SI // i += 32
CMPQ DI, SI
JG loop // if len(x) > i goto loop
我们采用原始的算法计算以利用到并行计算的能力,并且循环展开,一次循环中同时计算32个数,最终Benchmark结果:
BenchmarkAVX2L2Norm
BenchmarkAVX2L2Norm-2 29381442 40.99 ns/op
BenchmarkGonumL2Norm
BenchmarkGonumL2Norm-2 1822386 659.4 ns/op
可以看到得到了大约16倍的性能提升 。
通过这次优化我们在余弦相似度计算部分最终得到了(144.4 + 659.4 * 2) / (53.46 + 40.99 * 2) = 10.8倍的性能提升,效果还是非常显著的。相较于《记一次SIMD指令优化计算的失败经历》这次失败的初次尝试,本次还是非常成功的,切实感受到了SIMD的威力.
另外在本次优化过程中也涨了不少姿势 。
AVX-512指令因为并行度更高理论上性能也更高,但AVX-512指令会造成CPU降频,因此业界使用非常慎重,这一点可以参考字节的json解析库sonic的这个issue: https://github.com/bytedance/sonic/issues/319 。
在一次循环中做更多的工作,优点有很多:
但一次循环使用过多的寄存器从实际Benchmark看性能确实更好,但是否存在隐患我没有看到相关的资料,希望这方面的专家可以指教一下.
最后此篇关于使用AVX2指令集加速推荐系统MMR层余弦相似度计算的文章就讲到这里了,如果你想了解更多关于使用AVX2指令集加速推荐系统MMR层余弦相似度计算的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我正在尝试优化一些矩阵计算,我想知道是否可以在编译时检测 SSE/SSE2/AVX/AVX2/AVX-512/AVX-128-FMA/KCVI[ 1] 是否由编译器启用?非常适合 GCC 和 Clan
我想仅使用avx而不是avx2来实现64位转置操作。它应该这样做: // in = Hh Hl Lh Ll // | X | // out = Hh Lh Hl Ll 这就是使
如果我有一个 AVX 寄存器,里面有 4 个 double 值,我想将它的反向存储在另一个寄存器中,是否可以用一个内部命令来实现? 例如:如果我在 SSE 寄存器中有 4 个 float ,我可以使用
最初我试图重现 Agner Fog 的微体系结构指南部分“YMM 和 ZMM 向量指令的预热期”中描述的效果,它说: The processor turns off the upper parts o
我的 C++ 代码使用 SSE,现在我想改进它以支持 AVX(当它可用时)。因此,我检测 AVX 何时可用并调用使用 AVX 命令的函数。我使用 Win7 SP1 + VS2010 SP1 和带有 A
我有一大块内存,比如说 256 KiB 或更长。我想计算整个 block 中 1 位的数量,或者换句话说:将所有字节的“总体计数”值相加。 我知道 AVX-512 有一个 VPOPCNTDQ inst
有多快 tensorflow-gpu与没有 AVX 和 AVX2 相比,有 AVX 和 AVX2 吗? 我试图使用谷歌找到答案,但没有成功。很难重新编译tensorflow-gpu对于 Windows
为什么avx sqrt(非压缩)指令有三个操作数? vsqrtsd xmm1, xmm2, xmm3 这是否意味着类似于 xmm1=xmm2=sqrt(xmm3)? 编辑:下面的详细答案但总之流水线的
我正在研究Intel intrinsics guide的展开和压缩操作。我对这两个概念感到困惑: 对于__m128d _mm_mask_expand_pd (__m128d src, __mmask8
我在 Intel Intrinsic Guide v2.7 中找不到它们。您知道 AVX 或 AVX2 指令集是否支持它们吗? 最佳答案 原始 AVX 指令集中没有分散或收集指令。 AVX2 添加了收
我正在尝试将函数转换为 AVX 版本。函数本身基本上只是比较浮点数并返回真/假取决于计算。 这是原始函数: bool testSingle(float* thisFloat, float* other
我遇到了 AVX 内部指令 _mm256_testc_pd() 的一个非常奇怪的行为。在这里你可以看到这个功能的描述 https://software.intel.com/sites/landingp
我有一个 256 位 AVX 寄存器,其中包含 4 个单精度复数,存储为实数、虚数、实数、虚数等。我目前正在将整个 256 位寄存器写回内存并在那里求和,但这似乎效率低下. 如何使用 AVX(或 AV
#include "stdio.h" #include "math.h" #include "stdlib.h" #include "x86intrin.h" void dd_m(double *cl
有没有办法对 AVX 寄存器进行水平异或——特别是对 256 位寄存器的四个 64 位组件进行异或? 目标是获得 AVX 寄存器的所有 4 个 64 位组件的异或。它本质上与水平添加( _mm256_
当我尝试使用 AVX 获取数据时,出现运行时错误 - 段错误: int i = 0; const int sz = 9; size_t *src1 = (size_t *)_mm_malloc(sz*
当我尝试使用 AVX 展开最简单的循环时,出现运行时错误 - 段错误: const int sz = 9; float *src = (float *)_mm_malloc(sz*
我想将两个 256 位 vector (__m256d) 合并为一个 256位 vector ,通过省略每个 64 位 double 的上半部分。 所以,如果在下面,a_i, b_i, ... 是 3
我测试了以下简单的功能 void mul(double *a, double *b) { for (int i = 0; i #include #include #include #defi
_mm_i32gather_epi32() 的当前英特尔内在函数指南将每个子词的计算地址描述为: addr := base_addr + SignExtend64(vindex[m+31:m]) *
我是一名优秀的程序员,十分优秀!