程序执行太慢?快来学习 SIMD 加速技术,这个案例下的加速效果我也没想到(附带动手实验)
在编译过程中,任何语言都需要转化为对应平台的汇编语言,一个指令完成一个操作。如果告诉你现在有一个指令可以一次完成多个操作,你会怎么使用?
下面教你怎么使用SIMD加速技术。
本文通过Go语言开源社区在ARM64平台对通用字符串比较的优化方案,向读者介绍如何通过SIMD并行化技术提升软件的执行速度。摘自OptimizeLab: https://github.com/OptimizeLab/docs
作者:surechen
SIMD即单指令多数据流(Single Instruction Multiple Data)指令集,是通过一条指令同时对多个数据进行运算的硬件加速技术,能够有效提高CPU的运算速度,主要适用于计算密集型、数据相关性小的多媒体、数学计算、人工智能等领域。
Go语言是一种快速、静态类型的开源语言,可以用来轻松构建简单、可靠、高效的软件。目前已经包括丰富的基础库如数学库、加解密库、图像库、编解码等等,其中对于性能要求较高且编译器目前还不能优化的场景,Go语言通过在底层使用汇编技术进行了优化,其中最重要的就是SIMD技术。
1. 字符串比较的性能问题
先看一个常见的问题,在各行各业的服务系统中,用户登录需要验证用户名或ID,订购货物需要对比货物ID,出行需要验证票号等等,这些都离不开字符串比较操作,字符串实际上就是字节数组,在Go语言中可以表示成[]byte的形式,字符串的比较即两个数组中对应byte的比较。因此可以直观的写出如下的比较函数代码:
似乎看起来还不错,逻辑没有问题,但是这样的实现就够了吗?是否能满足所有的使用场景呢?本文通过性能测试来给出答案。通过Go benchmark进行测试得出如下数据:
[注]ns/op:每次函数执行耗费的纳秒时间;MB/s:每秒处理的兆字节数据;B:字节
如表所示,随着处理数据量的增加,耗时上升明显,当数据量达到4M时,耗时接近3.5毫秒(3496666 ns/op),作为一个基础操作来讲性能表现较差。
2. Go语言社区字符串比较的SIMD优化方案
那么字符串比较的性能问题是否有优化的办法呢?本文就以Go语言社区对字符串比较的优化案例揭开SIMD技术优化的神秘面纱:
优化的补丁在Go社区官网可见。
由于优化前后的代码都是汇编,为便于读者学习代码,首先将上节的Go代码例子与优化前的Equal汇编代码对比,通过如下直观的对比关系图来展示:
如图所示,两者实现逻辑是对应的。此处对于一行Go代码a[i]!=b[i],需要四条汇编指令:
两条取数指令,分别将切片数组a和b中的byte值取到寄存器中;
通过CMP(比较指令)对比两个寄存器中的值,根据比较结果更新状态寄存器;
BEQ(跳转指令)根据状态寄存器值进行跳转,此处是等于则跳转到loop标签处,即如果相等则继续下一轮比较。
现在可以正式开始分析Equal的SIMD优化版了,这里的核心思路是通过单指令同时处理多个byte数据的方式,大幅减少数据加载和比较操作的指令条数,发挥SIMD运算部件的算力,提升性能。
下图是使用SIMD技术优化汇编代码前后的对比图,从图中可以看到优化前代码非常简单,循环取1 byte进行比较,使用SIMD指令优化后,代码被复杂化,这里可以先避免陷入细节,先理解实现原理,具体代码细节可以在章节[优化后代码详解]再进一步学习。此处代码变复杂的主要原因是进行了分情况的分块处理,首先循环处理64 bytes大小的分块,当数组末尾不足64 bytes时,再将余下的按16 bytes分块处理,直到余下长度为1时的情况,下图直观的演示了优化前后的对比关系和优化后分块处理的规则:
3. 优化前代码详解
优化前的代码循环从两个数组中取1 byte进行比较,每byte数据要耗费2个加载操作、1个比较操作、1个数组末尾判断操作,如下所示:
4. 优化后代码详解
优化后的代码因为做了循环展开,所有看起来比较复杂,但逻辑是很清晰的,即采用分块的思路,将数组划分为64/16/8/4/2/1bytes大小的块,使用多个向量寄存器,利用一条SIMD指令最多同时处理16个bytes的优势,同时也减少了边界检查的次数。汇编代码解读如下(代码中添加了关键指令注释):
上述优化代码中,使用VLD1(数据加载指令)一次加载64bytes数据到SIMD寄存器,再使用VCMEQ(相等比较指令)比较SIMD寄存器保存的数据内容得到结果,相比传统用的单字节比较方式,提高了64byte数据块的比较性能。大于16byte小于64byte块数据,使用一个SIMD寄存器一次处理16byte块的数据,小于16byte数据块使用通用寄存器保存数据,一次比较8\4\2\1byte的数据块。
5. 动手实验
感兴趣的读者可以根据下面的命令自己执行一遍,感受SIMD技术的强大。
环境准备
硬件配置:鲲鹏(ARM64)云Linux服务器-通用计算增强型KC1 kc1.2xlarge.2(8核|16GB)
Go语言发行版 >= 1.12.1,此处开发环境准备请参考文章:Go在ARM64开发环境配置
Go语言github源码仓库下载,此处通过Git安装和使用进行版本控制。
操作步骤 如下操作包含在鲲鹏服务器上进行编译测试的全过程,本文已经找到了优化前后的两个提交记录,优化前的commit id:0c68b79和优化后的commit id:78ddf27,可以按如下步骤进行操作:
6. 优化结果
如章节[动手实验]所述使用Go benchmark记录优化前后的数据。获得结果后通过使用Go benchstat进行优化前后的性能对比。结果如下表格:
[注]ns/op:每次函数执行耗费的纳秒时间; ms/op:每次函数执行耗费的毫秒时间; MB/s:每秒处理的兆字节数据; GB/s:每秒处理的G字节数据;
上表中可以清晰的看到使用SIMD优化后,所有的用例性能都有所提升,其中数据大小为4K时性能提升率最高,耗时减少了93.49%;每秒数据处理量提升14.29倍。
如果对开源或优化技术感兴趣,欢迎下方留言或者通过https://github.com/OptimizeLab/docs/issues联系我们。
版权声明: 本文为 InfoQ 作者【Optimize-Lab】的原创文章。
原文链接:【http://xie.infoq.cn/article/9354c2496e3652fd6560aa074】。文章转载请联系作者。
评论