防止 gcc 将我的 AVX2 内在函数复制到 REP MOVS

Posted

技术标签:

【中文标题】防止 gcc 将我的 AVX2 内在函数复制到 REP MOVS【英文标题】:Prevent gcc from mangling my AVX2 intrinsics copy into REP MOVS 【发布时间】:2019-11-01 20:45:12 【问题描述】:

考虑以下循环:

template <typename T>
void copytail(T* __restrict__ dest, const T* __restrict__ src, size_t count) 
  constexpr size_t chunk_size = 4 * 32;
  size_t byte_count = sizeof(T) * count;
  size_t chunks = byte_count / chunk_size;
  auto rest = byte_count - byte_count / chunk_size * chunk_size;
  auto rest_vecs = (rest + 31) / 32;
  __m256i* dest256 = (__m256i*)((char *)dest + byte_count - rest_vecs * 32);
  __m256i* src256  = (__m256i*)((char *)src  + byte_count - rest_vecs * 32);
  for (size_t j = 0; j < rest_vecs; j++) 
      _mm256_storeu_si256(dest256 + j, _mm256_loadu_si256(src256 + j));
  



void tail_copy(char* d, const char* s, size_t overshoot) 
    copytail(d, s, overshoot);

不要想太多它的作用,因为它是基于更完整功能的简化测试用例 - 但基本上它从src 复制多达 4 个 AVX2 向量到dest,与 结束区域。

无论出于何种原因1-O3 的 gcc 8.1 产生了这个奇怪的程序集:

tail_copy(char*, char const*, unsigned long):
  mov rax, rdx
  and eax, 127
  add rax, 31
  mov rcx, rax
  and rcx, -32
  sub rdx, rcx
  shr rax, 5
  je .L30
  sal rax, 5
  mov r8d, eax
  add rdi, rdx
  add rsi, rdx
  test dil, 1
  jne .L32
.L3:
  test dil, 2
  jne .L33
.L4:
  test dil, 4
  jne .L34
.L5:
  mov ecx, r8d
  shr ecx, 3
  rep movsq   # oh please no
  xor eax, eax
  test r8b, 4
  jne .L35
  test r8b, 2
  jne .L36
  # many more tail-handling cases follow

基本上是一个rep movsq 来调用主副本的微代码,然后是一堆尾部处理代码来处理奇数字节(大部分没有显示,完整的程序集可以在godbolt 上看到)。

在我的情况下,这比 vmovdqu 加载/存储慢一个数量级。

即使它打算使用rep movs,CPU 也有 ERMSB,所以rep movsb 可能不需要额外清理的确切字节数与rep movsq 一样有效。但是 CPU 确实没有具有“快速短代表”功能(Ice Lake)所以我们rep movs 启动开销是一个大问题。

我希望 gcc 或多或少地按照所写的方式发出我的复制循环 - 至少 32 字节的 AVX2 加载和存储应该在源代码中出现。重要的是,我希望它是这个函数的本地函数:也就是说,不改变编译器参数。


1 可能是memcpy 识别然后memcpy 内联。

【问题讨论】:

volatile unaligned_m256i 可能会有所帮助。 (使用 GNU C 原生向量语法来声明您自己的 typedef long long vec256u __attribute__((vector_size(32), may_alias, aligned(1))))。这不是一个好的解决方案,但您赢得与编译器这场斗争的另一个选择可能是内联 asm。 ICC 和 MSVC 不优化内在函数(至少不是 ALU 内在函数,关于加载/存储的 IDK),但切换编译器更加激烈。 你对gcc-10生​​成的代码满意吗? 【参考方案1】:

您关于memcpy 识别的假设似乎是正确的(__builtin_memcpy 首次出现在ldist 传递中,可以在-fdump-tree-all 日志中看到),这会抑制优化:

__attribute__ ((optimize ("no-tree-loop-distribute-patterns")))
void tail_copy(char* d, const char* s, size_t overshoot) 
    copytail(d, s, overshoot);

将其应用于模板定义似乎也可以。

如果 CPU 支持 ERMS(就像大多数带有 AVX2 的 Intel CPU 一样),但不清楚这是否是一种改进。

【讨论】:

从技术上讲,ERMS 仅适用于 rep movsb 而不是 rep movsq,尽管 rep movsq 在最近的硬件上似乎几乎一样快。然而,在我的情况下,“同样快”结果却慢了一个数量级。 @BeeOnRope 我讨厌tree-loop-distribute-patterns。多次烧死我。编译器应该在任何 SIMD 内在函数上抑制它们。【参考方案2】:

也许这个解决方案太明显了,但您可以通过删除 __restrict__ 来防止 gcc(和 clang)识别代码中的 memcpy

template <typename T>
void copytail(T* dest, const T* src, size_t count) 
  constexpr size_t chunk_size = 4 * 32;
  size_t byte_count = sizeof(T) * count;
  size_t chunks = byte_count / chunk_size;
  auto rest = byte_count - byte_count / chunk_size * chunk_size;
  auto rest_vecs = (rest + 31) / 32;
  __m256i* dest256 = (__m256i*)((char *)dest + byte_count - rest_vecs * 32);
  __m256i* src256  = (__m256i*)((char *)src  + byte_count - rest_vecs * 32);
  for (size_t j = 0; j < rest_vecs; j++) 
      _mm256_storeu_si256(dest256 + j, _mm256_loadu_si256(src256 + j));
  

神箭比较:https://godbolt.org/z/osjO91

【讨论】:

以上是关于防止 gcc 将我的 AVX2 内在函数复制到 REP MOVS的主要内容,如果未能解决你的问题,请参考以下文章

用于灰度到 ARGB 转换的 C++ SSE2 或 AVX2 内在函数

GCC avx2intrin.h(版本 X-9.2)中缺少 _mm_broadcastsd_pd

int64_t 指针转换为 AVX2 内在 _m256i

使用 GCC 的 MSP430 位操作内在函数

在 GCC 上设置打包的 long long 的正确对齐以与 avx2 指令一起使用

我可以使用内在函数加速类型转换吗?