Mempercepat akselerasi atau mengenal SIMD, Bagian 2 - AVX

Bagian sebelumnya menyebabkan diskusi panas, di mana ternyata AVX / AVX2 sebenarnya di CPU desktop, tidak hanya AVX512. Oleh karena itu, kami terus berkenalan dengan SIMD, tetapi sudah dengan bagian modernnya - AVX. Dan kami juga akan menganalisis beberapa komentar:


  • Apakah _mm256_load_si256 lebih lambat dari akses memori langsung?
  • Apakah menggunakan perintah AVX melalui register SSE mempengaruhi kecepatan?
  • Apakah _popcnt benar-benar seburuk itu?

Sedikit tentang AVX


AVX / AVX2 adalah versi yang lebih kuat dari SSE yang memperluas sebagian besar operasi SSE 128-bit menjadi 256 bit, plus membawa sejumlah instruksi baru.


Dari seluk-beluk implementasi, kita dapat membedakan bahwa pada tingkat assembler AVX menggunakan 3 argumen, yang memungkinkan untuk tidak menghancurkan data di dua yang pertama. SSE menyimpan hasilnya di salah satu argumen.


Juga harus diingat bahwa dengan pengalamatan langsung, data harus disejajarkan dengan 32 byte, di SSE, penyelarasan dengan 16.


Versi benchmark yang ditambah


Perubahan:


  1. Jumlah elemen telah ditingkatkan 10.000 kali (hingga 10.240.000), agar tidak masuk ke dalam cache prosesor.
  2. Alignment diubah dari 16 byte menjadi 32 untuk mendukung AVX.
  3. Menambahkan implementasi AVX mirip dengan SSE.

Kode benchmark
 #include <benchmark/benchmark.h> #include <x86intrin.h> #include <cstring> #define ARR_SIZE 10240000 #define VAL 50 static int16_t *getRandArr() { auto res = new int16_t[ARR_SIZE]; for (int i = 0; i < ARR_SIZE; ++i) { res[i] = static_cast<int16_t>(rand() % (VAL * 2)); } return res; } static auto arr = getRandArr(); static int16_t *getAllignedArr() { auto res = aligned_alloc(32, sizeof(int16_t) * ARR_SIZE); memcpy(res, arr, sizeof(int16_t) * ARR_SIZE); return static_cast<int16_t *>(res); } static auto allignedArr = getAllignedArr(); static void BM_Count(benchmark::State &state) { for (auto _ : state) { int64_t cnt = 0; for (int i = 0; i < ARR_SIZE; ++i) if (arr[i] == VAL) ++cnt; benchmark::DoNotOptimize(cnt); } } BENCHMARK(BM_Count); static void BM_SSE_COUNT_SET_EPI(benchmark::State &state) { for (auto _ : state) { int64_t cnt = 0; auto sseVal = _mm_set1_epi16(VAL); for (int i = 0; i < ARR_SIZE; i += 8) { cnt += _popcnt32( _mm_movemask_epi8( _mm_cmpeq_epi16( sseVal, _mm_set_epi16(arr[i + 7], arr[i + 6], arr[i + 5], arr[i + 4], arr[i + 3], arr[i + 2], arr[i + 1], arr[i]) ) ) ); } benchmark::DoNotOptimize(cnt >> 1); } } BENCHMARK(BM_SSE_COUNT_SET_EPI); static void BM_SSE_COUNT_LOADU(benchmark::State &state) { for (auto _ : state) { int64_t cnt = 0; auto sseVal = _mm_set1_epi16(VAL); for (int i = 0; i < ARR_SIZE; i += 8) { cnt += _popcnt32( _mm_movemask_epi8( _mm_cmpeq_epi16( sseVal, _mm_loadu_si128((__m128i *) &arr[i]) ) ) ); } benchmark::DoNotOptimize(cnt >> 1); } } BENCHMARK(BM_SSE_COUNT_LOADU); static void BM_SSE_COUNT_DIRECT(benchmark::State &state) { for (auto _ : state) { int64_t cnt = 0; auto sseVal = _mm_set1_epi16(VAL); for (int i = 0; i < ARR_SIZE; i += 8) { cnt += _popcnt32( _mm_movemask_epi8( _mm_cmpeq_epi16( sseVal, *(__m128i *) &allignedArr[i] ) ) ); } benchmark::DoNotOptimize(cnt >> 1); } } BENCHMARK(BM_SSE_COUNT_DIRECT); #ifdef __AVX2__ static void BM_AVX_COUNT_LOADU(benchmark::State &state) { for (auto _ : state) { int64_t cnt = 0; auto avxVal = _mm256_set1_epi16(VAL); for (int i = 0; i < ARR_SIZE; i += 16) { cnt += _popcnt32( _mm256_movemask_epi8( _mm256_cmpeq_epi16( avxVal, _mm256_loadu_si256((__m256i *) &arr[i]) ) ) ); } benchmark::DoNotOptimize(cnt >> 1); } } BENCHMARK(BM_AVX_COUNT_LOADU); static void BM_AVX_COUNT_LOAD(benchmark::State &state) { for (auto _ : state) { int64_t cnt = 0; auto avxVal = _mm256_set1_epi16(VAL); for (int i = 0; i < ARR_SIZE; i += 16) { cnt += _popcnt32( _mm256_movemask_epi8( _mm256_cmpeq_epi16(avxVal, _mm256_load_si256((__m256i *) &allignedArr[i]) ) ) ); } benchmark::DoNotOptimize(cnt >> 1); } } BENCHMARK(BM_AVX_COUNT_LOAD); static void BM_AVX_COUNT_DIRECT(benchmark::State &state) { for (auto _ : state) { int64_t cnt = 0; auto avxVal = _mm256_set1_epi16(VAL); for (int i = 0; i < ARR_SIZE; i += 16) { cnt += _popcnt32( _mm256_movemask_epi8( _mm256_cmpeq_epi16( avxVal, *(__m256i *) &allignedArr[i] ) ) ); } benchmark::DoNotOptimize(cnt >> 1); } } BENCHMARK(BM_AVX_COUNT_DIRECT); #endif BENCHMARK_MAIN(); 

Hasil baru terlihat seperti ini (-O0):


 --------------------------------------------------------------------- Benchmark Time CPU Iterations --------------------------------------------------------------------- BM_Count 17226622 ns 17062958 ns 41 BM_SSE_COUNT_SET_EPI 8901343 ns 8814845 ns 79 BM_SSE_COUNT_LOADU 3664778 ns 3664766 ns 185 BM_SSE_COUNT_DIRECT 3468436 ns 3468423 ns 202 BM_AVX_COUNT_LOADU 2090817 ns 2090796 ns 343 BM_AVX_COUNT_LOAD 1904424 ns 1904419 ns 364 BM_AVX_COUNT_DIRECT 1814875 ns 1814854 ns 385 

Total percepatan total adalah 9+ kali, AVX diharapkan lebih cepat dari SSE hampir 2 kali.


Apakah _mm256_load_si256 dari akses memori langsung?


Tidak ada jawaban yang pasti. C -O0 lebih lambat dari akses langsung, tetapi lebih cepat dari _mm256_loadu_si256 :


 --------------------------------------------------------------------- Benchmark Time CPU Iterations --------------------------------------------------------------------- BM_AVX_COUNT_LOADU 2090817 ns 2090796 ns 343 BM_AVX_COUNT_LOAD 1904424 ns 1904419 ns 364 BM_AVX_COUNT_DIRECT 1814875 ns 1814854 ns 385 

C -O3 lebih cepat dari akses memori langsung, tetapi masih diharapkan lebih lambat _mm256_loadu_si256 .


 --------------------------------------------------------------------- Benchmark Time CPU Iterations --------------------------------------------------------------------- BM_AVX_COUNT_LOADU 992319 ns 992368 ns 701 BM_AVX_COUNT_LOAD 956120 ns 956166 ns 712 BM_AVX_COUNT_DIRECT 1027624 ns 1027674 ns 730 

Dalam kode produksi, masih lebih baik menggunakan _mm256_load_si256 daripada akses langsung, kompiler dapat mengoptimalkan opsi ini dengan lebih baik.


Apakah menggunakan perintah AVX mempengaruhi register SSE mempengaruhi kecepatan?


Jawaban singkatnya adalah tidak . Untuk percobaan, saya mengkompilasi dan menjalankan benchmark dengan -mavx2 dan dengan -msse4.2 .


-mavx2


_popcnt32(_mm_movemask_epi8(_mm_cmpeq_epi16(...))) berubah menjadi


 vpcmpeqw %xmm1,%xmm0,%xmm0 vpmovmskb %xmm0,%edx popcnt %edx,%edx 

Hasil:


 ------------------------------------------------------------ Benchmark Time CPU Iterations ------------------------------------------------------------ BM_SSE_COUNT_SET_EPI 9376699 ns 9376767 ns 75 BM_SSE_COUNT_LOADU 4425510 ns 4425560 ns 159 BM_SSE_COUNT_DIRECT 3938604 ns 3938648 ns 177 

-msse4.2


_popcnt32(_mm_movemask_epi8(_mm_cmpeq_epi16(...))) berubah menjadi


 pcmpeqw %xmm1,%xmm0 pmovmskb %xmm0,%edx popcnt %edx,%edx 

Hasil:


 ------------------------------------------------------------ Benchmark Time CPU Iterations ------------------------------------------------------------ BM_SSE_COUNT_SET_EPI 9309352 ns 9309375 ns 76 BM_SSE_COUNT_LOADU 4382183 ns 4382195 ns 159 BM_SSE_COUNT_DIRECT 3944579 ns 3944590 ns 176 

bonus


Perintah AVX _popcnt32(_mm256_movemask_epi8(_mm256_cmpeq_epi16(...))) berubah menjadi


 vpcmpeqw %ymm1,%ymm0,%ymm0 vpmovmskb %ymm0,%edx popcnt %edx,%edx 

Apakah _popcnt begitu buruk?


Dalam salah satu komentar Antervis menulis:


Namun, Anda memiliki sedikit cacat pada algoritme. Mengapa melakukannya melalui movemask + popcnt? Untuk array yang tidak lebih dari 2 ^ 18 elemen, pertama-tama Anda dapat mengumpulkan jumlah elemen-bijaksana:
auto cmp = _mm_cmpeq_epi16 (sseVal, sseArr);
cmp = _mm_and_si128 (cmp, _mm_set1_epi16 (1));
jumlah = _mm_add_epi16 (jumlah, cmp);

dan kemudian, di akhir siklus, buat satu penambahan horizontal (jangan lupa limpahan).

Saya membuat patokan


 static void BM_AVX_COUNT_DIRECT_WO_POPCNT(benchmark::State &state) { auto avxVal1 = _mm256_set1_epi16(1); for (auto _ : state) { auto sum = _mm256_set1_epi16(0); auto avxVal = _mm256_set1_epi16(VAL); for (int i = 0; i < ARR_SIZE; i += 16) { sum = _mm256_add_epi16( sum, _mm256_and_si256( avxVal1, _mm256_cmpeq_epi16( avxVal, *(__m256i *) &allignedArr[i]) ) ); } auto arrSum = (uint16_t *) &sum; size_t cnt = 0; for (int j = 0; j < 16; ++j) cnt += arrSum[j]; benchmark::DoNotOptimize(cnt >> 1); } } 

dan ternyata lebih lambat dari c -O0 :


 --------------------------------------------------------------------- Benchmark Time CPU Iterations --------------------------------------------------------------------- BM_AVX_COUNT_DIRECT 1814821 ns 1814785 ns 392 BM_AVX_COUNT_DIRECT_WO_POPCNT 2386289 ns 2386227 ns 287 

dan sedikit lebih cepat dengan -O3 :


 --------------------------------------------------------------------- Benchmark Time CPU Iterations --------------------------------------------------------------------- BM_AVX_COUNT_DIRECT 960941 ns 960924 ns 722 BM_AVX_COUNT_DIRECT_WO_POPCNT 948611 ns 948596 ns 732 

Source: https://habr.com/ru/post/id440632/


All Articles