अंतिम अद्यतन:2026-10-12
C++ SIMD vectorization¶
हाल में vectorization से जुड़ी सामग्री देख रहा था, देखते ही सिर दुखने लगता है, इस अवसर पर high-performance SIMD programming सीखते हैं।
SIMD का पूरा नाम single-instruction multiple-data है, यानी एक instruction, कई data।
पारंपरिक computer architecture में CPU एक समय पर केवल एक data element process कर सकता है। लेकिन कई tasks में बड़ी मात्रा में data पर एक ही operation करना होता है, जैसे array के सभी elements पर addition, multiplication या logical operations करना। SIMD programming, CPU को विशेष instruction sets उपलब्ध कराकर, CPU को एक साथ कई data elements पर एक ही operation execute करने में सक्षम बनाती है।
यह processing तरीका vectors, matrices, images, audio और video जैसे data की computation के लिए विशेष रूप से उपयुक्त है।
फिलहाल सबसे अधिक उपयोग होने वाले हैं SSE, SSE2, AVX128, AVX256, AVX512।
इस section में AVX512 के कुछ operations का सरल अध्ययन करेंगे, operations बहुत हैं, यहाँ केवल कुछ का परिचय दिया गया है।
1. शब्दावली¶
सबसे पहला प्रश्न यह है कि simd programming का code रोज़ लिखे जाने वाले code जैसा नहीं दिखता, तरह-तरह के underscores और नाम, बिल्कुल समझ नहीं आते, इसे कैसे समझें?
जैसे:
- _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.
इस प्रकार नीचे दिया गया table मिला:
| 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 है, यानी unordered load, जब _mm512_mask_loadu_ps function से memory का data load किया जाता है, तो memory address के किसी alignment requirement का पालन नहीं किया जाता। जबकि _mm512_mask_load_ps 64 byte alignment requirement पूरा करना ज़रूरी मानता है।
इस तरह मिलाकर पढ़ने से बहुत जल्दी हर interface का अर्थ पता चल जाता है।
संबंधित API के लिए Intel Intrinsics Guide देख सकते हैं।
https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html
2. वास्तविक उदाहरण¶
2.1 न्यूनतम मान निकालना¶
512 bits के लिए हम 16 32-bit floats store कर सकते हैं।
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 Data का क्रम तेज़ी से बदलना¶
इनपुट data के लिए
हम तेज़ी से पा सकते हैं:
संगत implementation:
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 Rotation¶
एक array के लिए:
हम rotate करके पा सकते हैं:
or
यानी rotate down or rotate up।
यहाँ हम इस तरह implement कर सकते हैं:
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 का उपयोग करके हम कुछ बहुत दिलचस्प algorithms implement कर सकते हैं, arrays और bulk data की processing को तेज़ कर सकते हैं।
आगे simd सीखना जारी रखेंगे, साथ मिलकर मेहनत करते हैं~
नोट: इस section के पूरे उदाहरण 星球 पर अपडेट किए जा चुके हैं, धन्यवाद~
संबंधित विषय¶
- C++ Concurrency in Action रीडिंग गाइड | C++那些事
- C++ deadlock जांच | C++那些事
- C++17 parallel STL | C++那些事
- C++ False Sharing और cache line | C++那些事
- GDB multithread All-Stop डीबगिंग | C++那些事
- Helgrind concurrency error detection | C++那些事
- libev event loop का सिद्धांत | C++那些事
- C++ std::thread kernel implementation | C++那些事
- C++ std::thread और jthread | C++那些事
- C++ multithreading की शुरुआत | C++那些事