C++ 程序集为啥不是由 g++ 向量化的“source 4 bytes to dest 4 bytes”赋值?
Posted
技术标签:
【中文标题】C++ 程序集为啥不是由 g++ 向量化的“source 4 bytes to dest 4 bytes”赋值?【英文标题】:C++ assembly why isn't this "source 4 bytes to dest 4 bytes" assignment vectorized by g++?C++ 程序集为什么不是由 g++ 向量化的“source 4 bytes to dest 4 bytes”赋值? 【发布时间】:2022-01-24 04:19:14 【问题描述】:#include <stdio.h>
#include <iostream>
#include <random>
using namespace std;
volatile int res = 0;
void copy(char* __restrict__ dst, char* __restrict__ src)
dst[0] = src[0];
dst[1] = src[1];
dst[2] = src[2];
dst[3] = src[3];
void copyOffset(char* __restrict__ dst, char* __restrict__ src, size_t offset)
dst[0] = src[offset + 0];
dst[1] = src[offset + 1];
dst[2] = src[offset + 2];
dst[3] = src[offset + 3];
void copyAsInt(char *dst, char *src)
*((int*)dst) = *((int*)src);
//----
void copy16(char* __restrict__ dst, char* __restrict__ src)
dst[0] = src[0];
dst[1] = src[1];
dst[2] = src[2];
dst[3] = src[3];
dst[4] = src[4];
dst[5] = src[5];
dst[6] = src[6];
dst[7] = src[7];
dst[8] = src[8];
dst[9] = src[9];
dst[10] = src[10];
dst[11] = src[11];
dst[12] = src[12];
dst[13] = src[13];
dst[14] = src[14];
dst[15] = src[15];
void copyOffset16(char* __restrict__ dst, char* __restrict__ src, size_t offset)
dst[0] = src[offset + 0];
dst[1] = src[offset + 1];
dst[2] = src[offset + 2];
dst[3] = src[offset + 3];
dst[4] = src[offset + 4];
dst[5] = src[offset + 5];
dst[6] = src[offset + 6];
dst[7] = src[offset + 7];
dst[8] = src[offset + 8];
dst[9] = src[offset + 9];
dst[10] = src[offset + 10];
dst[11] = src[offset + 11];
dst[12] = src[offset + 12];
dst[13] = src[offset + 13];
dst[14] = src[offset + 14];
dst[15] = src[offset + 15];
int main()
char *a = new char[1001], *b = new char[16];
//--- which pair of statements below is unsafe or not equal each other?
copyOffset(b, a, 20);
res = b[rand() % 4]; // use b[] for something to prevent optimization
copy(b, &a[20]);
res = b[rand() % 4];
//--- non 4 bytes aligned
copyOffset(b, a, 18);
res = b[rand() % 4];
copy(b, &a[18]);
res = b[rand() % 4];
//---
copyOffset16(b, a, 26);
res = b[rand() % 16];
copy(b, &a[26]);
res = b[rand() % 16];
return 1;
我正在尝试复制 4 个字节(确保分配源和目标)。但是,源地址可能不是 4 字节对齐的。要复制 4 个字节,我希望编译器发出一个复制 DWORD 指令,如 copyAsInt()
。我正在使用-O3 -mavx
标志,并使用godbolt 和gcc 11.2 来查看汇编代码。
正如预期的那样,函数copy()
被转换为与copyAsInt()
相同。但是,由于某种原因,函数 copyOffset() 被翻译为分别复制每个字节。
copy(char*, char*):
mov eax, DWORD PTR [rsi]
mov DWORD PTR [rdi], eax
ret
copyOffset(char*, char*, unsigned long):
movzx eax, BYTE PTR [rsi+rdx]
mov BYTE PTR [rdi], al
movzx eax, BYTE PTR [rsi+1+rdx]
mov BYTE PTR [rdi+1], al
movzx eax, BYTE PTR [rsi+2+rdx]
mov BYTE PTR [rdi+2], al
movzx eax, BYTE PTR [rsi+3+rdx]
mov BYTE PTR [rdi+3], al
ret
同时,函数 copy16() 和 copyOffset16() 都按预期进行了矢量化。
copy16(char*, char*):
vmovdqu xmm0, XMMWORD PTR [rsi]
vmovdqu XMMWORD PTR [rdi], xmm0
ret
copyOffset16(char*, char*, unsigned long):
vmovdqu xmm0, XMMWORD PTR [rsi+rdx]
vmovdqu XMMWORD PTR [rdi], xmm0
ret
那么为什么编译器不优化copyOffset()
以使用mov DWORD
?另外,main()
中是否有任何不安全或可能出现意外行为的语句对?
编辑:切换到 x86-64 gcc(主干)会导致 gcc 发出预期的指令。所以我猜这种行为只是由于编译器启发式的。
【问题讨论】:
TL:DR:这是 GCC11 中错过的优化,已在主干中修复。还有一个在 clang 中错过的优化,通过在 AL 中使用 byte-mov
使情况变得更糟,导致错误的依赖。当前接受的答案几乎在它提出的每个单独的点上都是错误的,包括它链接的 Agner 指令表中的数字; IMO 您应该不接受它,以免误导未来的读者。
【参考方案1】:
因为这样更有效。 XMM (SSE) 或任何其他 SIMD 指令通常往往非常繁重,因此具有非常高的延迟。编译器会考虑到这一点。
这种对 SSE/AVX 的痴迷确实被夸大了。 SSE/AVX 在非常具体的优化中可能很有用,但在 99% 的编译器使用它们的情况下,它们实际上是无效的。
轶事证据,但我曾经使用“-march=x86_64”重新编译了一个非常大的优化二进制文件,它删除了几乎所有 SIMD 扩展,并且性能实际上比使用“-march=native”更好。
一个简单的 MOVB(字节)操作具有 1 个周期的延迟,并且可以在 Intel Skylake 上同时执行多达 4 个操作。所以里面的8条指令都可以在2个周期内有效执行。
MOVUPS 指令有大约 5 个周期延迟,最大吞吐量为 2(同时),具体取决于平台,您需要其中两个:从内存到 %xmm0 和从 %xmm0 到内存,总共从 5到 10 个周期。
当然,由于管道行为、微操作、端口、L1/2/3 缓存等原因,事情比这要复杂得多,但这是解释编译器在做什么的第一次尝试。
参考:https://www.agner.org/optimize/instruction_tables.pdf
代码:https://godbolt.org/z/3KjxfdW3W
【讨论】:
但它不需要使用大的 SIMD 指令。为什么它不使用简单的移动 DWORD 指令,例如在 copyAsInt() 中? 实际上 GCC 完全按照您所说的进行。 godbolt.org/z/zf5h9xrqz 这是不同启发式公式的问题,我敢打赌。 是的,这是正确的 - 适用于 x86,不适用于 ARM 等其他架构 @HuyLe:要在 C 中安全地表达未对齐的严格别名安全加载或存储,请使用memcpy
或 GNU C typedef uint32_t aliasing_unaliged_uint32 __attribute__((aligned(1), may_alias))
,如 Why does unaligned access to mmap'ed memory sometimes segfault on AMD64? 和 Why does glibc's strlen need to be so complicated to run quickly?
如果它是真的,这个答案甚至没有意义,GCC 对复制的 4 个字节的行为奇怪的代码,所以在那里使用向量加载/存储没有意义,进行 DWORD 加载/存储是有意义的,这就是 GCC 未能做到的。以上是关于C++ 程序集为啥不是由 g++ 向量化的“source 4 bytes to dest 4 bytes”赋值?的主要内容,如果未能解决你的问题,请参考以下文章