simd 本身是 CPU 的一部分,不是 GPU 的一部分。
SIMD(Single Instruction Multiple Data) 即单指令流多数据流,是一种采用一个控制器来控制多个处理器,同时对一组数据(又称“数据向量”)中的每一个分别执行相同的操作从而实现空间上的并行性的技术。简单来说就是一个指令能够同时处理多个数据。
simd 指令包含了:
MMX(MultiMedia eXtension) 拓展指令集。可以使用 64bit 的 MM0-MM7 八个寄存器。
SSE(Streaming SIMD Extensions) 拓展指令集。可以使用 128bit XMM0-XMM7 八个寄存器。后续又添加了 8 个寄存器 XMM8-XMM15。
AVX 将 SSE 的 128bit 寄存器拓展到了 256bit,称为 YMM。
AVX512 将 AVX 的 256bit 寄存器拓展到了 512 bit,称为 ZMM 寄存器。
AVX512 指令会导致 CPU 性能下降,从而导致效果反而不如 AVX,因此 Rust 编译时会避开 avx512 指令集。 |
使用 SIMD 的方法
使用 SIMD 的方法从简单到困难如下所示:
依赖编译器做优化。
使用 Intel 提供的 IPP 函数库。
使用 OpenMP 中的指令
#pragma omp simd
。使用 intrinsics 函数。
使用汇编指令。
intrinsics
intrinsics 相关的函数 [1] 包含在了下面的头文件中,其中下面的头文件包含上面的头文件,因此只需要包含最后一个即可:
头文件 | 能力 |
---|---|
xmmintrin.h | SSE, 支持同时对4个32位单精度浮点数的操作。 |
emmintrin.h | SSE 2, 支持同时对2个64位双精度浮点数的操作。 |
pmmintrin.h | SSE 3, 支持对SIMD寄存器的水平操作(horizontal operation),如hadd, hsub等…。 |
tmmintrin.h | SSSE 3, 增加了额外的instructions。 |
smmintrin.h | SSE 4.1, 支持点乘以及更多的整形操作。 |
nmmintrin.h | SSE 4.2, 增加了额外的instructions。 |
immintrin.h | AVX, 支持同时操作8个单精度浮点数或4个双精度浮点数。 |
这些头文件提供的函数命名规则为:
__<return_type> _<vector_size>_<intrin_op>_<suffix>
return_type 统一为各种类型的向量类型。
vector_size 位 \_128, \_256, \_512 等,代表了寄存器的大小。
intrin_op 代表了函数执行的操作。目前存在的操作有:[2]
操作 作用 load
将数据储存到寄存器中。
store
将寄存器中的值写回到内存中。
suffix 后缀可能为:
后缀 作用 p
packed
s
float
d
double
下面是一个使用 SIMD 寄存器加速拷贝的例子。对于 1024B 的 u8 array 而言,循环拷贝每次拷贝 1B,需要拷贝 1024 次,的使用 ZMM 寄存器可以每次拷贝 512/8=64B,只需要拷贝 16 次。
#include <chrono>
#include <cstring>
#include <iostream>
#include <string>
extern "C" {
#include <immintrin.h>
}
using namespace std::chrono;
using namespace std::chrono_literals;
#define EXPAND(i) \
{ \
auto a = _mm512_loadu_si512(data + (i) * 64); \
_mm512_storeu_si512(dst + (i) * 64, a); \
}
int main(int argc, char *argv[]) {
auto start = steady_clock::now();
auto data = new uint8_t[1024];
for (int i = 0; i < 1024 / 64; ++i) {
auto ptr = (uint64_t *)data;
*(ptr + i) = i;
}
auto dst = new uint8_t[1024];
uintptr_t send = 0;
while (true) {
// for (int i = 0; i < 1024; ++i) { (1)
// dst[i] = data[i];
// }
for (int i = 0; i < 16; i += 2) { (2)
auto a = _mm512_loadu_si512(data + i * 64);
_mm512_storeu_si512(dst + i * 64, a);
}
// memcpy(dst, data, 1024); (3)
send += 1;
auto end = steady_clock::now();
auto duration = duration_cast<seconds>(end - start);
if (duration.count() >= 1) {
std::cout << send * 1.f / 1024 / 1024 << "GB" << std::endl;
send = 0;
start = end;
}
}
return 0;
}
使用 for 循环拷贝。
使用 simd 指令拷贝。
使用 memcpy 拷贝。
在 x86 计算机 debug 模式编译的情况下。for 循环 1GB/s,simd 15 GB/s,memcpy 34GB/s。
其中对于 simd:
使用循环完全展开后增长到了 20GB/s。
除去临时变量 a 后速度为 25GB/s。
将上述两个优化连在一起后速度并没有什么改善。