विषय पर बढ़ें

अंतिम अद्यतन: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

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 है, यानी 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));

इस प्रकार हम तेज़ी से पा सकते हैं:

1 2 3 4.4 5 6.6 7.7 8 9 10 11 12 13 14 15 16

2.2 Data का क्रम तेज़ी से बदलना

इनपुट data के लिए

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

संगत 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 के लिए:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

हम rotate करके पा सकते हैं:

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।

यहाँ हम इस तरह 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 के पूरे उदाहरण 星球 पर अपडेट किए जा चुके हैं, धन्यवाद~

संबंधित विषय

टिप्पणियाँ