加速加速或了解SIMD,第2部分-AVX

上一部分引起了激烈的讨论,在此期间,事实证明AVX / AVX2实际上存在于台式机CPU中,而不仅仅是AVX512。 因此,我们继续熟悉SIMD,但已经熟悉了它的现代部分-AVX。 我们还将分析一些评论:


  • _mm256_load_si256是否比直接内存访问慢?
  • 通过SSE寄存器使用AVX命令会影响速度吗?
  • _popcnt真的那么糟糕吗?

关于AVX的一些知识


AVX / AVX2是SSE的功能更强大的版本,它将大多数128位SSE操作扩展到256位,还带来了许多新指令。


从实现的微妙之处,我们可以区分出在汇编程序级别,AVX使用3个参数,这不允许破坏前两个数据。 SSE将结果存储在参数之一中。


还应牢记,使用直接寻址时,数据必须按32字节对齐(在SSE中按16对齐)。


基准的增强版


变化:


  1. 元素的数量已增加10,000倍(最多10,240,000),以使其不适合处理器高速缓存。
  2. 对齐方式从16个字节更改为32个以支持AVX。
  3. 添加了类似于SSE的AVX实现。

基准代码
 #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(); 

新结果如下所示(-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 

总的总加速是9倍以上,AVX有望比SSE快近2倍。


_mm256_load_si256是否比直接内存访问_mm256_load_si256


没有确切的答案。 C -O0比直接访问慢,但比_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比直接内存访问要快,但是_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 

在生产代码中,最好使用_mm256_load_si256而不是直接访问;编译器可以更好地优化此选项。


使用AVX命令是否会影响SSE寄存器并影响速度?


简短的答案是否定的 。 对于实验,我使用-mavx2-msse4.2编译并运行了基准测试。


-mavx2


_popcnt32(_mm_movemask_epi8(_mm_cmpeq_epi16(...)))变成


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

结果:


 ------------------------------------------------------------ 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(...)))变成


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

结果:


 ------------------------------------------------------------ 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 

红利


AVX命令_popcnt32(_mm256_movemask_epi8(_mm256_cmpeq_epi16(...)))变成


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

_popcnt很糟糕吗?


Antervis在其中一项评论中写道:


但是,您对算法有一点缺陷。 为什么要通过movemask + popcnt呢? 对于不超过2 ^ 18个元素的数组,可以首先收集逐个元素的和:
自动cmp = _mm_cmpeq_epi16(sseVal,sseArr);
cmp = _mm_and_si128(cmp,_mm_set1_epi16(1));
sum = _mm_add_epi16(sum,cmp);

然后,在循环结束时,进行一次水平相加(不要忘记溢出)。

我做了一个基准


 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); } } 

结果却比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 

-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/zh-CN440632/


All Articles