Files
CPlusPlusThings/site_extras/cpp-concurrency-cpp-simd.md
light-city dfe1c933b3 docs: address community issue feedback (#5 #338 #142 #246 #136 #303)
- const README: rewrite the space-saving claim per issue #5 follow-ups
  (constant-expression const vs #define immediates; which const qualify
  as constant expressions)
- const README: fix the class-section diagnosis per issue #338 (overload
  resolution picks the const overload; the restriction only surfaces
  once that overload is removed)
- sizeof README: virtual-inheritance result is compiler-dependent
  (GCC merges vbptr+vfptr -> 16, MSVC keeps them separate -> 32) #246
- static README: new section on internal linkage / access restriction
  with anonymous-namespace comparison #142
- 10-day practice: full problem list table + fixed day links #136
- new design_pattern/singleton/singleton_default.hpp: reusable
  thread-safe CRTP singleton template, compile+concurrency tested #303
2026-10-13 19:23:54 +08:00

174 lines
5.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
> 来自 [C++那些事网站](https://light-city.github.io/stories_things/cpp-concurrency/cpp-simd/) 的文章,由站点同步脚本导出。
# 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可以看看Interl Intrinsics Guide。
> https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html
## 2.实际例子
### 2.1 求最小值
对于512位,我们可以存储16个32位float。
```cpp
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));
```
于是我们可以快速得到:
```cpp
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
```
我们可以快速得到:
```cpp
16.16 2 14.14 13.13 5 6 7 8 9 10 11 12 13 14 15 16
```
对应实现:
```cpp
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。
在这里,我们可以这样实现:
```cpp
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,一起加油吧~
注:本节的完整示例已在星球更新,谢谢~