单指令多数据流(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,我们就会错过很多潜在的优化。以下是可以提高日常生活场景中性能的具体事项的非详尽列表:
- simdjson
- 通过矢量化每秒解码数十亿个整数
- 矢量化和性能可移植的快速排序
- Hyperscan 简介
- From slow to SIMD: A Go optimization story
- How to Use AVX512 in Golang via C Compiler
此外,它将使这些当前存在的软件包更具可移植性和可维护性:
在这个月即将发布的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编译以下代码时,可以启用自动向量化:
|
|
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) 的代码应该很容易。这个库中的大部分代码都是自动生成的,这有助于维护。
|
|
2.2.2 alivanz/go-simd
[alivanz/go-simd](https://github.com/alivanz/go-simd)实现了 Go 语言的 SIMD(单指令多数据)操作,专门针对 ARM NEON 架构进行了优化。其目标是为特定的计算任务提供优化的并行处理能力。
下面是一个加法和乘法的例子:
|
|
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等),从而实现高性能的向量化计算。然而,编写和维护汇编代码对开发者提出了较高的要求,且代码的可移植性较差。
|
|
当然需要a,b和 result 数组的地址是对齐的,以获得最佳性能。
结论
尽管Go语言目前对SIMD的支持尚不完善,但社区已经通过第三方库和汇编代码提供了一些解决方案。未来,随着Go编译器的改进和标准库的支持(相信Go官方最终会支持的),Go语言在高性能计算领域的潜力将进一步释放。对于开发者而言,掌握SIMD技术将有助于编写更高效的Go代码,应对日益复杂的计算任务。