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