最終更新:2026-10-12
C++ SIMD ベクトル化¶
最近、ベクトル化関連の内容を見ていて、少し頭が痛くなったので、この機会に高性能SIMDプログラミングを学んでいきます。
SIMDはsingle-instruction multiple-dataの略称で、単一命令複数データを意味します。
従来のコンピュータアーキテクチャでは、CPUは一度に1つのデータ要素しか処理できません。しかし、多くのタスクは大量のデータに対して同じ操作を実行する必要があります。例えば、配列内のすべての要素に対する加算、乗算、論理操作などです。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 |
さらに例を挙げます:
_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ビットには、32ビットfloatを16個格納できます。
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));
すると、次の結果をすぐに得ることができます:
2.2 データの並び順を素早くシャッフルする¶
入力データが
であるとき、次の結果を素早く得ることができます:
対応する実装:
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 ローテーション¶
ある配列:
に対して、ローテーションして次の結果を得ることができます:
または
つまり、rotate downまたは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を学び続けていきます。一緒に頑張りましょう〜
注:本節の完全なサンプルは知識星球で更新済みです。よろしくお願いします〜
関連トピック¶
- C++ Concurrency in Action 読書ガイド | C++那些事
- C++ デッドロック調査 | C++那些事
- C++17 並列 STL | C++那些事
- C++ False Sharing とキャッシュライン | C++那些事
- GDB マルチスレッド All-Stop デバッグ | C++那些事
- Helgrind 並行エラー検出 | C++那些事
- libev イベントループの原理 | C++那些事
- C++ std::thread の内部実装 | C++那些事
- C++ std::thread と jthread | C++那些事
- C++ マルチスレッド入門 | C++那些事