原文:blog.fanscore.cn/a/62/

1. 背景

前一段时间公司上线了一套Go实现的推荐系统,上线后发现MMR层虽然只有纯计算但耗时十分离谱,通过pprof定位问题所在之后进行了优化,虽然降低了非常多但是我们认为其中还有优化空间。

可以看到日常平均耗时126ms,P95 360ms。

MMR层主要耗时集中在了余弦相似度的计算部分,这部分我们使用的gonum库进行计算,其底层在x86平台上利用了SSE指令集进行了加速。

SSE指令集已经非常古老了,xmm寄存器只能存储两个双精度浮点数,每次只能并行进行两个双精度浮点数的计算,而AVX2指令集可以并行计算四个,理论上可以获得两倍的性能提升,因此我们决定自己使用AVX2指令集手写汇编的方式替代掉gonum库。

1.1 余弦相似度算法

余弦相似度的计算公式为

对应的代码为

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)
}

2. Dot点积计算加速

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倍的性能提升

3. L2范数计算加速

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倍的性能提升

4. 总结

通过这次优化我们在余弦相似度计算部分最终得到了(144.4 + 659.4 * 2) / (53.46 + 40.99 * 2) = 10.8倍的性能提升,效果还是非常显著的。相较于《记一次SIMD指令优化计算的失败经历》这次失败的初次尝试,本次还是非常成功的,切实感受到了SIMD的威力。

另外在本次优化过程中也涨了不少姿势

AVX-512指令降频问题

AVX-512指令因为并行度更高理论上性能也更高,但AVX-512指令会造成CPU降频,因此业界使用非常慎重,这一点可以参考字节的json解析库sonic的这个issue: https://github.com/bytedance/sonic/issues/319

循环展开优化

在一次循环中做更多的工作,优点有很多:

  • 减少循环控制的开销,循环变量的更新和条件判断次数更少,降低了分支预测失败的可能性
  • 增加指令并行性,更多的指令可以在流水线中并行执行

但一次循环使用过多的寄存器从实际Benchmark看性能确实更好,但是否存在隐患我没有看到相关的资料,希望这方面的专家可以指教一下。

使用AVX2指令集加速推荐系统MMR层余弦相似度计算的更多相关文章

  1. java算法(1)---余弦相似度计算字符串相似率

    余弦相似度计算字符串相似率 功能需求:最近在做通过爬虫技术去爬取各大相关网站的新闻,储存到公司数据中.这里面就有一个技术点,就是如何保证你已爬取的新闻,再有相似的新闻 或者一样的新闻,那就不存储到数据 ...

  2. 两矩阵各向量余弦相似度计算操作向量化.md

    余弦相似度计算: \cos(\bf{v_1}, \bf{v_2}) = \frac{\left( v_1 \times v_2 \right)}{||v_1|| * ||v_2|| } \cos(\b ...

  3. Python简单实现基于VSM的余弦相似度计算

    在知识图谱构建阶段的实体对齐和属性值决策.判断一篇文章是否是你喜欢的文章.比较两篇文章的相似性等实例中,都涉及到了向量空间模型(Vector Space Model,简称VSM)和余弦相似度计算相关知 ...

  4. Spark Mllib里相似度度量(基于余弦相似度计算不同用户之间相似性)(图文详解)

    不多说,直接上干货! 常见的推荐算法 1.基于关系规则的推荐 2.基于内容的推荐 3.人口统计式的推荐 4.协调过滤式的推荐 协调过滤算法,是一种基于群体用户或者物品的典型推荐算法,也是目前常用的推荐 ...

  5. <tf-idf + 余弦相似度> 计算文章的相似度

    背景知识: (1)tf-idf 按照词TF-IDF值来衡量该词在该文档中的重要性的指导思想:如果某个词比较少见,但是它在这篇文章中多次出现,那么它很可能就反映了这篇文章的特性,正是我们所需要的关键词. ...

  6. AVX图像算法优化系列二: 使用AVX2指令集加速查表算法。

    查表算法,无疑也是一种非常常用.有效而且快捷的算法,我们在很多算法的加速过程中都能看到他的影子,在图像处理中,尤其常用,比如我们常见的各种基于直方图的增强,可以说,在photoshop中的调整菜单里8 ...

  7. KNN cosine 余弦相似度计算

    # coding: utf-8 import collections import numpy as np import os from sklearn.neighbors import Neares ...

  8. 余弦相似度及基于python的三种代码实现、与欧氏距离的区别

    1.余弦相似度可用来计算两个向量的相似程度 对于如何计算两个向量的相似程度问题,可以把这它们想象成空间中的两条线段,都是从原点([0, 0, ...])出发,指向不同的方向.两条线段之间形成一个夹角, ...

  9. 余弦相似度-Cosine Similar(转载)

    余弦相似度用向量空间中两个向量夹角的余弦值作为衡量两个个体间差异的大小.相比距离度量,余弦相似度更加注重两个向量在方向上的差异,而非距离或长度上. 与欧几里德距离类似,基于余弦相似度的计算方法也是把用 ...

  10. Intel的AVX2指令集解读

    原文链接:http://blog.csdn.net/vbskj/article/details/38408213 在Intel Sandy Bridge微架构中,Intel引入了256位SIMD扩展A ...

随机推荐

  1. 【Windows】解决微软商店打不开的问题

    参考贴吧的帖子: https://tieba.baidu.com/p/6028738660#123983609458l 1.打开"运行"输入 inetcpl.cpl (" ...

  2. 基于 ChatGPT 的聊天软件合集打包分享

      「基于 ChatGPT 的聊天软件合集打包」 链接:https://pan.quark.cn/s/ef1f5e9c48e4 BotGem(原名AMA) 官网:https://botgem.com/ ...

  3. 工业AI制造:铝合金冲压、压铸工艺流程 —— 模具参数调整,以满足所需的规格和质量要求

    压铸操作工艺流程作步骤: 模具安装 → 调试 →清理预热模具 → 喷刷涂料 → 合模 → 涂料准备 → 涂料配制 → 压铸 → 冷却与凝固 → 开模 → 顶出铸件 → 质量检验 → 成品 → 废品 → ...

  4. 视频分享---------《无人机背后的PID控制》

    在B站上看到有讲无人机自动控制方面的视频,感觉不错,分享下: https://www.bilibili.com/video/BV1aW411E7Qq/?spm_id_from=333.788.vide ...

  5. win10系统wifi不会自动连接怎么解决

    参考: https://jingyan.baidu.com/article/d621e8da743bab2865913f99.html ================================ ...

  6. 用海豚调度器定时调度从Kafka到HDFS的kettle任务脚本

    在实际项目中,从Kafka到HDFS的数据是每天自动生成一个文件,按日期区分.而且Kafka在不断生产数据,因此看看kettle是不是需要时刻运行?能不能按照每日自动生成数据文件? 为了测试实际项目中 ...

  7. 神经网络之卷积篇:详解更多边缘检测内容(More edge detection)

    详解更多边缘检测内容 已经见识到用卷积运算实现垂直边缘检测,在本博客中,将看到如何区分正边和负边,这实际就是由亮到暗与由暗到亮的区别,也就是边缘的过渡.还能了解到其他类型的边缘检测以及如何去实现这些算 ...

  8. 安装RabbitMQ遇到的一些坑

    Ubantu18.0正确安装RabbitMQ 1.安装erlang 因为RabbitMQ需要erlang语言的支持,所以我们需要先安装erlang. sudo apt-get install erla ...

  9. [考试记录] 2024.7.15 csp-s模拟赛4

    2024.7.15 csp-s模拟赛4 T1 传送带 题面翻译 有一个长度为 \(n\) 的一维网格.网格的第 \(i\) 个单元格包含字符 \(s_i\) ,是"<"或&q ...

  10. 19-canvas绘制文字

    1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="U ...