C++那些事之高性能SIMD

2023-09-02 10:42:44 浏览数 (1)

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。

代码语言:javascript复制
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));

于是我们可以快速得到:

代码语言:javascript复制
1 2 3 4.4 5 6.6 7.7 8 9 10 11 12 13 14 15 16

2.2 快速打乱数据顺序

对于输入数据是

代码语言:javascript复制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

我们可以快速得到:

代码语言:javascript复制
16.16 2 14.14 13.13 5 6 7 8 9 10 11 12 13 14 15 16

对应实现:

代码语言:javascript复制
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 旋转

对于一个数组:

代码语言:javascript复制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

我们可以旋转得到:

代码语言:javascript复制
4 5 6 7 8 9 10 11 12 13 14 15 16 1 2 3

or

代码语言:javascript复制
14 15 16 1 2 3 4 5 6 7 8 9 10 11 12 13

也就是rotate down or rotate up。

在这里,我们可以这样实现:

代码语言:javascript复制
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,一起加油吧~

0 人点赞