首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >使用AVX2指令集加速推荐系统MMR层余弦相似度计算

使用AVX2指令集加速推荐系统MMR层余弦相似度计算

作者头像
Orlion
发布于 2024-10-11 01:24:09
发布于 2024-10-11 01:24:09
21900
代码可运行
举报
文章被收录于专栏:我的独立博客我的独立博客
运行总次数:0
代码可运行

1. 背景

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

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

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

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

1.1 余弦相似度算法

余弦相似度的计算公式为

对应的代码为

代码语言:javascript
代码运行次数:0
运行
复制
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的部分汇编代码如下:

代码语言:javascript
代码运行次数:0
运行
复制
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指令集并行计算四个双精度浮点数进行加速

代码语言:javascript
代码运行次数:0
运行
复制
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结果:

代码语言:javascript
代码运行次数:0
运行
复制
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源码如下:

代码语言:javascript
代码运行次数:0
运行
复制
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源码可以看出来没有用到并行能力,每次循环只计算一个数

代码语言:javascript
代码运行次数:0
运行
复制
TEXT ·L2NormUnitary(SB), NOSPLIT, $0
    ...
loop:
	MOVSD   (X_)(IDX*8), ABSX // absxi = x[i]
	...

我们优化之后的核心代码如下:

代码语言:javascript
代码运行次数:0
运行
复制
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结果:

代码语言:javascript
代码运行次数:0
运行
复制
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看性能确实更好,但是否存在隐患我没有看到相关的资料,希望这方面的专家可以指教一下。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-10-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
Docker 安装 RocketMQ 并结合 SpringBoot 使用实例
在之前的《浅入浅出消息队列》一文中,我们了解了消息队列的作用、优缺点和使用场景,相信你对消息队列已经有了一个大致的概念,文末给自己埋的坑说日后会写一篇实战教程,正好现在实习结束了,也许久没有写实战教程了,于是这就来填坑了。
出其东门
2020/11/03
1.6K0
Docker 安装 RocketMQ 并结合 SpringBoot 使用实例
RocketMQ-启动Nameserver
//ubuntu 下加 sudo 反而报错。最简单的命令: nohup sh mqnamesrv &
潇洒
2023/10/20
4620
RocketMQ的集群部署以及可视化监控界面
​ RocketMQ是一个轻量级、高可用、低延时的消息中间件,能实现消息的存储,消息的失败重试,批量消息处理,延时消息处理等特性,在各种消息中间件中表现优异。
Dream城堡
2022/01/07
8520
RocketMQ的集群部署以及可视化监控界面
rocketmq通过docker安装
测试可用。最后的安装效果 [image.png] 我是使用-host模式的,可以使用其他模式。 本机安装 安装2个镜像 docker pull rocketmqinc/rocketmq docker pull styletang/rocketmq-console-ng  安装name-server(服务发现用的,类似zookeeper) 安装broker(队列服务器) docker run -d --net=host -p 9876:9876 -v /data0/soft_app/rocketmq/data
李子健
2022/04/16
1.5K0
在docker下安装rocketmq【docker-compose】方式安装
最近学习使用 rocketmq,需要搭建 rocketmq 服务端,本文主要记录 rocketmq 搭建过程以及这个过程踩到的一些坑。
凯哥Java
2022/12/16
9.4K0
在docker下安装rocketmq【docker-compose】方式安装
RocketMQ之——单机环境搭建
今天,带来一篇搭建RocketMQ单机环境的文章,为后面的分布式事务专栏做准备。RocketMQ是阿里巴巴开源的一款高性能分布式消息中间件,有关RocketMQ的详细讲解,后面会单独开设一个RocketMQ专栏。这里,先简单介绍一下搭建RocketMQ的单机环境,为分布式事务的介绍做准备。接下来,进入主题。
冰河
2020/10/29
1.6K0
Linux系统:centos7下搭建Rocketmq4.3中间件,和监控台
rocketmq的默认配置极其耗内存,要进行修改。 1)修改runserver.sh配置 注释掉原来的,添加新配置
知了一笑
2019/07/19
9510
【没有测开,只有测试干开发】我也用上了MQ......
终于到了自己也能写MQ的时候了,一定程度上来说,还是属于解耦操作范畴吧。或许在某些场景的使用上,属于算小才大用了,但是本着以稳为主的打法,防止高并发,这样也可以让线程更好的发挥作用吧。
软件测试君
2024/06/18
1420
【没有测开,只有测试干开发】我也用上了MQ......
RocketMQ 环境搭建
RocketMQ 环境搭建 一. 开发环境 操作系统:CentOS7 JDK1.8 二. 安装JDK 下载jdk-8u181-linux-x64.tar.gz包到/usr/local下 解压 tar -zxvf jdk-8u181-linux-x64.tar.gz 重命名 mv jdk1.8.0_181/ ./jdk1.8 配置Java环境变量 修改配置文件 /etc/profile vi /etc/profile 在文件末尾增加Java环境变量配置 export JAVA
张申傲
2020/09/03
9090
构建docker镜像部署rocketmq
而docker默认隔离性不足,获取系统内存得到的是宿主机内存大小,导致内存不足启动失败(例如宿主机内存32G则计算结果为8G)
路过君
2020/12/31
1.9K0
构建docker镜像部署rocketmq
【精选】在CentOS7上安装RocketMQ 4.8.0
在RocketMQ官网上找到下载RocketMQ4.8.0的链接,下载和解压RocketMQ:
鳄鱼儿
2024/05/21
7840
Apache RocketMQ 消息队列部署与可视化界面安装
Apache RocketMQ是一个分布式、队列模型的消息中间件,具有低延迟、高性能和高可靠、万亿级容量和灵活的可扩展性。核心组件由四部分组成:Name Servers,Brokers,Producer 和 Consumer;它们中的每一个都可以水平扩展,而没有单一的故障节点。
陶陶技术笔记
2020/06/02
3.4K0
Apache RocketMQ 消息队列部署与可视化界面安装
RocketMQ实战教程之RocketMQ安装
1、启动NameServer 安装完RocketMQ包后,我们启动NameServer
全干程序员demo
2024/05/27
4080
RocketMQ实战教程之RocketMQ安装
【非docker-compose】docker下安装rocketmq
docker下按照rocketmq。非compose安装。一步一步安装 一:编写broker.conf文件 文件位置:/opt/rocketmq/conf/ 下面这个是复杂版本。 # 所属集群名字 brokerClusterName=DefaultCluster # broker 名字,注意此处不同的配置文件填写的不一样,如果在 broker-a.properties 使用: broker-a, # 在 broker-b.properties 使用: broker-b brokerName=broker-
凯哥Java
2022/12/16
5660
【RocketMQ】003-Windows 安装 RocketMQ
https://archive.apache.org/dist/rocketmq/5.1.0/
訾博ZiBo
2025/01/06
9040
【RocketMQ】003-Windows 安装 RocketMQ
rocketmq 部署启动指南-Docker 版
最近学习使用 rocketmq,需要搭建 rocketmq 服务端,本文主要记录 rocketmq 搭建过程以及这个过程踩到的一些坑。
andyxh
2019/09/05
5K0
rocketmq 部署启动指南-Docker 版
docker下安装rocketmq错误提示:/opt/rocketmq/conf/broker.conf (Is a directory)
在docker下安装rocketmq时候提示错误信息:/opt/rocketmq/conf/broker.conf (Is a directory)
凯哥Java
2022/12/16
2.2K1
docker下安装rocketmq错误提示:/opt/rocketmq/conf/broker.conf (Is a directory)
Docker部署RocketMQ4.x
🌊 关注我不迷路,如果本篇文章对你有所帮助,或者你有什么疑问,欢迎在评论区留言,我一般看到都会回复的。大家点赞支持一下哟~ 💗
杨不易呀
2023/10/30
9390
Docker部署RocketMQ4.x
Docker以挂载方式安装RocketMQ
如果你还没有安装 Docker,请先安装。可以参考官方文档 https://docs.docker.com/install/ 进行安装
关忆北.
2023/10/11
1.9K0
Docker以挂载方式安装RocketMQ
RocketMQ的集群
刚才的演示中,我们已经体验到了RocketMQ是如何工作的。这样,我们回头看RocketMQ的集群架构,就能够有更全面的理解了。
Java廖志伟
2022/03/07
7970
RocketMQ的集群
相关推荐
Docker 安装 RocketMQ 并结合 SpringBoot 使用实例
更多 >
目录
  • 1. 背景
    • 1.1 余弦相似度算法
  • 2. Dot点积计算加速
  • 3. L2范数计算加速
  • 4. 总结
    • AVX-512指令降频问题
    • 循环展开优化
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验