单指令多数据流(
SIMD
,Single Instruction Multiple Data)是一种并行计算技术,允许一条指令同时处理多个数据点。SIMD 在现代 CPU 中广泛应用,能够显著提升计算密集型任务的性能,如图像处理、机器学习、科学计算等。随着 Go 语言在高性能计算领域的应用逐渐增多,SIMD 支持成为了开发者关注的焦点。
当前很多主流和新型的语言都有相应的
simd
库了,比如 C++、Rust、Zig 等,但 Go 语言的
simd
官方支持还一直在讨论中(
issue#67520
[1]
)。Go 语言的设计目标是简单性和可移植性,而 SIMD 的实现通常需要针对不同的硬件架构进行优化,这与 Go 的设计目标存在一定冲突。因此,Go 语言对 SIMD 的支持一直备受争议。最近几周这个 issue 的讨论有活跃起来, 希望能快点支持。
1. Go 语言与 SIMD 的背景
1.1 Go 语言的性能追求
Go 语言以其简洁的语法、高效的并发模型和快速的编译速度赢得了广泛的应用。然而,Go 在性能优化方面一直面临挑战,尤其是在需要处理大量数据的场景下。SIMD 作为一种高效的并行计算技术,能够显著提升计算性能,因此 Go 社区对 SIMD 的支持呼声日益高涨。
如果没有 SIMD,我们就会错过很多潜在的优化。以下是可以提高日常生活场景中性能的具体事项的非详尽列表:
-
-
-
-
-
From slow to SIMD: A Go optimization story
[6]
-
How to Use AVX512 in Golang via C Compiler
[7]
此外,它将使这些当前存在的软件包更具可移植性和可维护性:
在这个月即将发布的 Go 1.24 版中,将会将内建的 map 使用 Swiss Tables 替换,而 Swiss Tables 针对 AMD64 的架构采用了
SIMD 的代码
[11]
,这是不是 Go 官方代码库首次引进了 SIMD 的指令呢?
当前先前也有人实现了 SIMD 加速 encoding/hex,
被否了
[12]
,当然理由也很充分:加速效果很好但请放弃吧,看起来太复杂,违背了 Go 简洁的初衷。类似的还有
unicode/utf8: make Valid use AVX2 on amd64
[13]
其实 Go 官方在 2023 就已经在标准库 crypto/sha256 中使用 SIMD 指令了
crypto/sha256: add sha-ni implementation
[14]
。
1.2 SIMD 的基本概念
SIMD 通过一条指令同时处理多个数据点,通常用于向量化计算。现代 CPU(如 Intel 的
SSE/AVX
、ARM 的
NEON
)都提供了 SIMD 指令集,允许开发者通过特定的指令集加速计算任务。然而,直接使用 SIMD 指令集通常需要编写汇编代码或使用特定的编译器内置函数,这对开发者提出了较高的要求。
1.2.1 SIMD 的核心思想
SIMD 的核心思想是通过一条指令同时处理多个数据点。例如,传统的标量加法指令一次只能处理两个数,而 SIMD 加法指令可以同时处理多个数(如 4 个、8 个甚至更多)。这种并行化处理方式能够显著提升计算密集型任务的性能。
1.2.2 SIMD 指令集的组成
SIMD 指令集通常包括以下几类指令:
-
-
-
-
-
特殊操作
:如求平方根、绝对值、最大值、最小值等。
1.3 常见的指令集
1.3.1 Intel 的 SIMD 指令集
1.3.1.1 MMX(MultiMedia eXtensions)
-
-
-
-
-
-
引入了 8 个 64 位寄存器(MM0-MM7)。
-
1.3.1.2 SSE(Streaming SIMD Extensions)
-
-
-
数据类型
:单精度浮点数(32 位)、整数(8 位、16 位、32 位、64 位)
-
-
引入了 8 个 128 位寄存器(XMM0-XMM7)。
-
-
后续版本(SSE2、SSE3、SSSE3、SSE4)增加了更多指令和功能。
1.3.1.3 AVX(Advanced Vector Extensions)
-
-
-
数据类型
:单精度浮点数(32 位)、双精度浮点数(64 位)、整数(8 位、16 位、32 位、64 位)
-
-
引入了 16 个 256 位寄存器(YMM0-YMM15)。
-
-
后续版本(AVX2、AVX-512)支持更复杂的操作和更宽的寄存器(512 位)。
1.3.1.4 AVX-512
-
-
-
数据类型
:单精度浮点数(32 位)、双精度浮点数(64 位)、整数(8 位、16 位、32 位、64 位)
-
-
引入了 32 个 512 位寄存器(ZMM0-ZMM31)。
-
-
1.3.2 ARM 的 SIMD 指令集
1.3.2.1 NEON
-
-
-
数据类型
:单精度浮点数(32 位)、整数(8 位、16 位、32 位、64 位)
-
-
-
支持 16 个 128 位寄存器(Q0-Q15)。
-
1.3.2.2 SVE(Scalable Vector Extension)
-
-
-
数据类型
:单精度浮点数(32 位)、双精度浮点数(64 位)、整数(8 位、16 位、32 位、64 位)
-
-
-
引入了谓词寄存器(Predicate Registers),支持条件执行。
-
1.4 编译器内置函数
大多数现代编译器(如 GCC、Clang、MSVC)提供了 SIMD 指令集的内置函数,开发者可以通过这些函数调用 SIMD 指令,而无需编写汇编代码。
1.5 自动向量化
一些编译器支持自动向量化功能,能够自动将标量代码转换为 SIMD 代码。例如,使用 GCC 编译以下代码时,可以启用自动向量化:
gcc -O3 -mavx2 -o program program.c
2. Go 语言中的 SIMD 支持现状
2.1 Go 语言标准库的 SIMD 支持
Go 语言的标准库尚未提供对 SIMD 的直接支持。Go 语言的编译器(gc)也没有自动向量化功能,这意味着开发者无法像在 C/C++中那样通过编译器自动生成 SIMD 代码。
在 Issue
#67520
[15]
中,讨论依然磨磨唧唧,讨论时常偏离到实现的具体方式上(build tag)。
2.2 第三方库与解决方案
尽管 Go 语言标准库缺乏对 SIMD 的直接支持,但社区已经开发了一些第三方库和工具,帮助开发者在 Go 中使用 SIMD 指令集。在
#67520
[16]
的讨论中,Clement Jean 也提供了一个概念化的实现方案:
simd-go-POC
[17]
。
以下是一些第三方实现的(simd 指令,不是基于 simd 实现的库 sonic、simdjson-go 等):
2.2.1
kelindar/simd
kelindar/simd
[18]
这个库包含一组矢量化的数学函数,它们使用 clang 编译器自动矢量化,并转换为 Go 的 PLAN9 汇编代码。对于不支持矢量化的 CPU,或此库没有为其生成代码的 CPU,也提供了通用版本。
目前它仅支持 AVX2,但生成 AVX512 和 SVE (for ARM) 的代码应该很容易。这个库中的大部分代码都是自动生成的,这有助于维护。
sum := simd.SumFloat32s([]float32{1, 2, 3, 4, 5})
2.2.2
alivanz/go-simd
[alivanz/go-simd](https://github.com/alivanz/go-simd)实现了 Go 语言的 SIMD(单指令多数据)操作,专门针对 ARM NEON 架构进行了优化。其目标是为特定的计算任务提供优化的并行处理能力。下面是一个加法和乘法的例子:
package main
import (
"log"
"github.com/alivanz/go-simd/arm"
"github.com/alivanz/go-simd/arm/neon"
)
func main() {
var a, b arm.Int8X8
var add, mul arm.Int16X8
for i := 0; i < 8; i++ {
a[i] = arm.Int8(i)
b[i] = arm.Int8(i * i)
}
log.Printf("a = %+v", b)
log.Printf("b = %+v", a)
neon.VaddlS8(&add, &a, &b)
neon.VmullS8(&mul, &a, &b)
log.Printf("add = %+v", add)
log.Printf("mul = %+v", mul)
}
2.2.3
pehringer/simd
pehringer/simd
[19]
通过 Go 汇编提供 SIMD 支持,实现了算术运算、位运算以及最大值和最小值运算。它允许进行并行的逐元素计算,从而带来 100% 到 400% 的速度提升。目前支持 AMD64 (x86_64) 和 ARM64 处理器。
2.3 Go 汇编与 SIMD
Go 语言支持通过汇编代码直接调用 CPU 指令集,这为 SIMD 的实现提供了可能。开发者可以编写 Go 汇编代码,调用特定的 SIMD 指令集(如 SSE、AVX 等),从而实现高性能的向量化计算。然而,编写和维护汇编代码对开发者提出了较高的要求,且代码的可移植性较差。
// 以下是一个简单的Go汇编示例,使用AVX指令集进行向量加法
TEXT ·add(SB), $0-32
MOVQ a+0(FP), DI
MOVQ b+8(FP), SI
MOVQ result+16(FP), DX
MOVQ len+24(FP), CX
TESTQ CX, CX ; 检查长度是否为0
JZ done ; 如果为0直接返回
MOVQ CX, R8 ; 保存原始长度
SHRQ $2, CX ; 除以4得到循环次数
JZ remainder ; 如果不足4个元素,跳到处理余数
XORQ R9, R9 ; 用于索引的计数器,从0开始
loop:
VMOVUPD (DI)(R9*8), Y0
VMOVUPD (SI)(R9*8), Y1
VADDPD Y0, Y1, Y0
VMOVUPD Y0, (DX)(R9*8)
ADDQ $4, R9
DECQ CX
JNZ loop
remainder: ; 处理剩余的元素
ANDQ $3, R8 ; 获取余数
JZ done
; 这里添加处理余数的代码
done:
RET
当然需要 a,b 和 result 数组的地址是对齐的,以获得最佳性能。
结论
尽管 Go 语言目前对 SIMD 的支持尚不完善,但社区已经通过第三方库和汇编代码提供了一些解决方案。未来,随着 Go 编译器的改进和标准库的支持(相信 Go 官方最终会支持的),Go 语言在高性能计算领域的潜力将进一步释放。对于开发者而言,掌握 SIMD 技术将有助于编写更高效的 Go 代码,应对日益复杂的计算任务。
[1]
issue#67520:
https://github.com/golang/go/issues/67520
[2]
simdjson:
https://github.com/simdjson/simdjson
[3]
通过矢量化每秒解码数十亿个整数:
https://people.csail.mit.edu/jshun/6886-s19/lectures/lecture19-1.pdf