啥时候等到Go官方支持SIMD?

单指令多数据流(SIMD,Single Instruction Multiple Data)是一种并行计算技术,允许一条指令同时处理多个数据点。SIMD在现代CPU中广泛应用,能够显著提升计算密集型任务的性能,如图像处理、机器学习、科学计算等。随着Go语言在高性能计算领域的应用逐渐增多,SIMD支持成为了开发者关注的焦点。

当前很多主流和新型的语言都有相应的simd库了,比如C++、Rust、Zig等,但Go语言的simd官方支持还一直在讨论中(issue#67520)。Go语言的设计目标是简单性和可移植性,而SIMD的实现通常需要针对不同的硬件架构进行优化,这与Go的设计目标存在一定冲突。因此,Go语言对SIMD的支持一直备受争议。
最近几周这个issue的讨论有活跃起来, 希望能快点支持。

1. Go语言与SIMD的背景

1.1 Go语言的性能追求

Go语言以其简洁的语法、高效的并发模型和快速的编译速度赢得了广泛的应用。然而,Go在性能优化方面一直面临挑战,尤其是在需要处理大量数据的场景下。SIMD作为一种高效的并行计算技术,能够显著提升计算性能,因此Go社区对SIMD的支持呼声日益高涨。

如果没有 SIMD,我们就会错过很多潜在的优化。以下是可以提高日常生活场景中性能的具体事项的非详尽列表:

此外,它将使这些当前存在的软件包更具可移植性和可维护性:

在这个月即将发布的Go 1.24版中,将会将内建的map使用Swiss Tables替换,而Swiss Tables针对AMD64的架构采用了SIMD的代码,这是不是Go官方代码库首次引进了SIMD的指令呢?

当前先前也有人实现了SIMD加速encoding/hex,被否了,当然理由也很充分:加速效果很好但请放弃吧,看起来太复杂,违背了Go简洁的初衷。
类似的还有unicode/utf8: make Valid use AVX2 on amd64

其实Go官方在2023就已经在标准库crypto/sha256中使用SIMD指令了 crypto/sha256: add sha-ni implementation

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)
  • 推出时间:1996年
  • 寄存器宽度:64位
  • 数据类型:整数(8位、16位、32位)
  • 特点
    • 主要用于多媒体处理。
    • 引入了8个64位寄存器(MM0-MM7)。
    • 不支持浮点数运算。
1.3.1.2 SSE(Streaming SIMD Extensions)
  • 推出时间:1999年
  • 寄存器宽度:128位
  • 数据类型:单精度浮点数(32位)、整数(8位、16位、32位、64位)
  • 特点
    • 引入了8个128位寄存器(XMM0-XMM7)。
    • 支持浮点数运算,适用于科学计算和图形处理。
    • 后续版本(SSE2、SSE3、SSSE3、SSE4)增加了更多指令和功能。
1.3.1.3 AVX(Advanced Vector Extensions)
  • 推出时间:2011年
  • 寄存器宽度:256位
  • 数据类型:单精度浮点数(32位)、双精度浮点数(64位)、整数(8位、16位、32位、64位)
  • 特点
    • 引入了16个256位寄存器(YMM0-YMM15)。
    • 支持更宽的向量操作,性能进一步提升。
    • 后续版本(AVX2、AVX-512)支持更复杂的操作和更宽的寄存器(512位)。
1.3.1.4 AVX-512
  • 推出时间:2016年
  • 寄存器宽度:512位
  • 数据类型:单精度浮点数(32位)、双精度浮点数(64位)、整数(8位、16位、32位、64位)
  • 特点
    • 引入了32个512位寄存器(ZMM0-ZMM31)。
    • 支持更复杂的操作,如掩码操作、广播操作等。
    • 主要用于高性能计算和人工智能领域。

1.3.2 ARM的SIMD指令集

1.3.2.1 NEON
  • 推出时间:2005年
  • 寄存器宽度:128位
  • 数据类型:单精度浮点数(32位)、整数(8位、16位、32位、64位)
  • 特点
    • 广泛应用于移动设备和嵌入式系统。
    • 支持16个128位寄存器(Q0-Q15)。
    • 适用于多媒体处理、信号处理等场景。
1.3.2.2 SVE(Scalable Vector Extension)
  • 推出时间:2016年
  • 寄存器宽度:可变(128位至2048位)
  • 数据类型:单精度浮点数(32位)、双精度浮点数(64位)、整数(8位、16位、32位、64位)
  • 特点
    • 支持可变长度的向量操作,适应不同的硬件配置。
    • 引入了谓词寄存器(Predicate Registers),支持条件执行。
    • 主要用于高性能计算和机器学习。

1.4 编译器内置函数

大多数现代编译器(如GCC、Clang、MSVC)提供了SIMD指令集的内置函数,开发者可以通过这些函数调用SIMD指令,而无需编写汇编代码。

1.5 自动向量化

一些编译器支持自动向量化功能,能够自动将标量代码转换为SIMD代码。例如,使用GCC编译以下代码时,可以启用自动向量化:

1
gcc -O3 -mavx2 -o program program.c

2. Go语言中的SIMD支持现状

2.1 Go语言标准库的SIMD支持

Go语言的标准库尚未提供对SIMD的直接支持。Go语言的编译器(gc)也没有自动向量化功能,这意味着开发者无法像在C/C++中那样通过编译器自动生成SIMD代码。

在Issue #67520 中,讨论依然磨磨唧唧,讨论时常偏离到实现的具体方式上(build tag)。

2.2 第三方库与解决方案

尽管Go语言标准库缺乏对SIMD的直接支持,但社区已经开发了一些第三方库和工具,帮助开发者在Go中使用SIMD指令集。在#67520的讨论中,Clement Jean 也提供了一个概念化的实现方案:simd-go-POC

以下是一些第三方实现的(simd指令,不是基于simd实现的库sonic、simdjson-go等):

2.2.1 kelindar/simd

kelindar/simd这个库包含一组矢量化的数学函数,它们使用 clang 编译器自动矢量化,并转换为 Go 的 PLAN9 汇编代码。对于不支持矢量化的 CPU,或此库没有为其生成代码的 CPU,也提供了通用版本。

目前它仅支持 AVX2,但生成 AVX512 和 SVE (for ARM) 的代码应该很容易。这个库中的大部分代码都是自动生成的,这有助于维护。

1
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 架构进行了优化。其目标是为特定的计算任务提供优化的并行处理能力。
下面是一个加法和乘法的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 通过 Go 汇编提供 SIMD 支持,实现了算术运算、位运算以及最大值和最小值运算。它允许进行并行的逐元素计算,从而带来 100% 到 400% 的速度提升。目前支持 AMD64 (x86_64) 和 ARM64 处理器。

2.3 Go汇编与SIMD

Go语言支持通过汇编代码直接调用CPU指令集,这为SIMD的实现提供了可能。开发者可以编写Go汇编代码,调用特定的SIMD指令集(如SSE、AVX等),从而实现高性能的向量化计算。然而,编写和维护汇编代码对开发者提出了较高的要求,且代码的可移植性较差。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 以下是一个简单的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代码,应对日益复杂的计算任务。