使用SIMD为Golang加速

汇编的门槛

失去重重抽象的庇护,汇编看上去非常的丑陋。用汇编来编写大型程序简直难以想象,不过在这里,我们仅仅使用汇编来进行AMD64体系下的运算加速,这并不会特别困难。

Golang有自己的汇编语法,总体上是AT&T风格,即:

< opcode source destination >

可以粗暴的认为这是汇编语句的唯一语法。汇编的简单性就在这里,只要你明确了你所需要的指令,数据源以及存储的位置,你就可以写出正确的代码。

Golang对汇编的封装

Golang的封装使得使用汇编变得更加便利,减轻了不少头脑负担。

这里是Golang自己的文档,写的非常简略,但针对伪寄存器和flag的说明已经足够了:godoc

通用寄存器分别是AX BX CX DX SI DI BP SP R8-R15。(虽然这些寄存器有其传统用法,但在寄存器不够用的情况下并不用严格依照传统)

64位下128bit寄存器为X0-X15,256bit寄存器为Y0-Y15。

SIMD

这里有一份不错的代码示例:simd_example

我借它来进行一些补充说明:

  1. 它这里没使用AVX而使用SSE,主要原因我认为是当时的Golang不识别某些AVX指令
  2. 使用NOSPILT这个flag,需要在文件头部加入:#include "textflag.h",或者直接填4
  3. 末尾的SFENCE指令,是因为代码中使用了Non-Temporal Hint
  4. 调用这个汇编函数的函数代码:func Multiply(data []V4, m M4) 这个函数没有body。随后就可以再外部调用它了
  5. Golang的汇编并不是完全对Intel指令的重现,比如MOVDQA成了MOVO(or MOVOA), MOVDQU在Golang是MOVOU。还有一些基本指令的变化以及寄存器的使用都是与其他汇编不同的,准确的说是头脑负担更小,但这些转换也有可能让 部分人觉得不精准,不舒服。在使用过程中大家会很快的发现这个问题。

如何利用这项技术

当你发现你的程序要用同样的指令对大量数据进行计算并且速度不理想的时候,就可以试试汇编。

编写汇编程序最重要的倒不是对指令的熟悉运用,毕竟这些都是可以查阅得到的。最关键的是必须搞清楚函数要用到哪些数据,分别从哪来的,他们会暂时存在哪,接下来会流向哪里?最终储存在什么地方?

简而言之就是掌控好数据流,接下来根据自己的需要翻阅Intel的文档去获取指令。为了避免golang不支持该指令的麻烦,可以查阅go的源代码下的/cmd/internal/obj/x86/anames.go这个文件,里面有其包含的所有汇编指令。

倘若非那条golang不支持的指令不可的话,可以这里提到的方法:adding-unsupported-instructions-in-golang-assembler

或者用现成的转换工具:asm2plan9s

这里的内容不多,例子所展示的内容也不够丰富。如果想获得更多的实战一手材料,可以看我的这份代码reedsolomon