C++那些事之高性能SIMD
最近在看相关向量化的内容,看起来有点头大,借此机会,学习一下高性能SIMD编程。
SIMD全称single-instruction multiple-data,单指令多数据。
在传统的计算机架构中,CPU一次只能处理一个数据元素。但是,许多任务涉及对大量数据执行相同的操作,例如对数组中的所有元素进行加法、乘法或逻辑操作等。SIMD编程通过向CPU提供专门的指令集,使得CPU能够同时对多个数据元素执行相同的操作。
这种处理方式特别适合涉及向量、矩阵、图像、音频和视频等数据的计算。
目前比较常用的有SSE、SSE2、AVX128、AVX256、AVX512。
本节,将简单学习一下AVX512的一些操作,操作比较多,这里只是引入一些。
1.术语
首先第一个问题便是,simd编程的代码跟平时写的代码长相不大一样,各种下划线以及命名,完全看不懂,如何理解呢?
诸如:
_mm512_set1_ps
Broadcast single-precision (32-bit) floating-point value a to all elements of dst.
_mm512_set1_epi32
Broadcast 32-bit integer a to all elements of dst.
于是,找到了下面这个表格:
Abbreviation | Full Name | C/C++ Equivalent |
---|---|---|
ps | packed single-precision | float |
ph | packed half-precision | None* |
pd | packed double-precision | double |
pch | packed half-precision complex | None* |
pi8 | packed 8-bit integer | int8_t |
pi16 | packed 16-bit integer | int16_t |
pi32 | packed 32-bit integer | int32_t |
epi8 | extended packed 8-bit integer | int8_t |
epi16 | extended packed 16-bit integer | int16_t |
epi32 | extended packed 32-bit integer | int32_t |
epi64 | extended packed 64-bit integer | int64_t |
epi64x | extended packed 64-bit integer | int64_t |
https://stackoverflow.com/questions/70911872/what-are-the-names-and-meanings-of-the-intrinsic-vector-element-types-like-epi6
再比如:
_mm512_mask_load_ps
_mm512_mask_loadu_ps
u表示unordered,表示加载无序,当使用 _mm512_mask_loadu_ps
函数加载内存中的数据时,不会执行对内存地址的任何对齐要求。而_mm512_mask_load_ps要求满足 64 字节对齐要求。
这样对照着学习,非常快的便可以知道每个接口的含义了。
相关API可以看看Intel Intrinsics Guide。
https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html
2.实际例子
2.1 求最小值
对于512位,我们可以存储16个32位float。
static inline rf_512 load(float* ptr) { return _mm512_loadu_ps(ptr); }
float a[width] = {1.0, 2.0, 3.0, 22.0, 5.0, 17.0, 9.0, 8.0,
9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0};
float b[width] = {3.3, 6.2, 5.3, 4.4, 5.5, 6.6, 7.7, 8.8,
9.9, 10.10, 21.11, 12.12, 13.13, 14.14, 15.0, 16.16};
rf_512 data = minimum(load(a), load(b));
于是我们可以快速得到:
1 2 3 4.4 5 6.6 7.7 8 9 10 11 12 13 14 15 16
2.2 快速打乱数据顺序
对于输入数据是
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
我们可以快速得到:
16.16 2 14.14 13.13 5 6 7 8 9 10 11 12 13 14 15 16
对应实现:
static inline rf_512 permutexvar(ri_512 idx, rf_512 src) {
return _mm512_permutexvar_ps(idx, src);
}
/*
raw data: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
shuffle data: 16.16 2 14.14 13.13 5 6 7 8 9 10 11 12 13 14 15 16
*/
void print_permutexvar_mask() {
float a[width] = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0,
9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0};
float b[width] = {1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8,
9.9, 10.10, 11.11, 12.12, 13.13, 14.14, 15.0, 16.16};
mask_type mask = make_bit_mask<1, 0, 1, 1, 0>();
int idx_array[width] = {15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0};
ri_512 idx = _mm512_loadu_si512((__m512i*)idx_array);
rf_512 result = permutexvar_mask(load(a), mask, idx, load(b));
float result_arr[width];
store(result, result_arr);
std::cout << "raw data: ";
for (int i = 0; i < width; i++) {
std::cout << a[i] << " ";
}
std::cout << std::endl;
std::cout << "shuffle data: ";
for (int i = 0; i < width; i++) {
std::cout << result_arr[i] << " ";
}
std::cout << std::endl;
}
2.3 旋转
对于一个数组:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
我们可以旋转得到:
4 5 6 7 8 9 10 11 12 13 14 15 16 1 2 3
or
14 15 16 1 2 3 4 5 6 7 8 9 10 11 12 13
也就是rotate down or rotate up。
在这里,我们可以这样实现:
rf_512 rotateGeneral(float* arr, int s) {
int idx_array[width];
for (int i = 0; i < width; i++) {
idx_array[i] = (i + s) % width;
}
ri_512 idx = _mm512_loadu_si512((__m512i*)idx_array);
rf_512 result = permutexvar(idx, load(arr));
return result;
}
等等,还有其他的例子,可以发现通过使用simd,我们可以实现一些非常有趣的算法,加速对数组,批量数据的处理。
后面会继续学习simd,一起加油吧~
注:本节的完整示例已在星球更新,谢谢~