c-Clang在加载字节时不会将高位置零.这是一个错误还是一个深思熟虑的选择



例如,使用此函数,

void mask_rol(unsigned char *a, unsigned char *b) {
a[0] &= __rolb(-2, b[0]);
a[1] &= __rolb(-2, b[1]);
a[2] &= __rolb(-2, b[2]);
a[3] &= __rolb(-2, b[3]);
a[4] &= __rolb(-2, b[4]);
a[5] &= __rolb(-2, b[5]);
a[6] &= __rolb(-2, b[6]);
a[7] &= __rolb(-2, b[7]);
}

gcc产生

mov     edx, -2
mov     rax, rdi
movzx   ecx, BYTE PTR [rsi]
mov     edi, edx
rol     dil, cl
and     BYTE PTR [rax], dil
...

虽然我不明白为什么它要填充dxax,但这是来自clang的。

mov     cl, byte ptr [rsi]
mov     al, -2
rol     al, cl
and     byte ptr [rdi], al
...

它不像gcc那样做看似不必要的mov,但它也不关心使用movzx清除高位。

据我所知,gcc执行movzx的原因是为了消除脏高位的错误依赖,但也许clang也有不这样做的原因,所以我运行了一个简单的基准测试,结果就是这样。

$ time ./rol_gcc
2161860550
real    0m0.895s
user    0m0.877s
sys     0m0.002s
$ time ./rol_clang
3205979094
real    0m1.328s
user    0m1.311s
sys     0m0.001s

至少在这种情况下,clang的方法似乎是错误的。

这显然是clang的错误吗,还是在某些情况下clang的方法可以产生更高效的代码?


基准代码

#include <stdio.h>
#include <x86intrin.h>
__attribute__((noinline))
static void mask_rol(unsigned char *a, unsigned char *b) {
a[0] &= __rolb(-2, b[0]);
a[1] &= __rolb(-2, b[1]);
a[2] &= __rolb(-2, b[2]);
a[3] &= __rolb(-2, b[3]);
a[4] &= __rolb(-2, b[4]);
a[5] &= __rolb(-2, b[5]);
a[6] &= __rolb(-2, b[6]);
a[7] &= __rolb(-2, b[7]);
}
static unsigned long long rdtscp() {
unsigned _;
return __rdtscp(&_);
}
int main() {
unsigned char a[8] = {0}, b[8] = {7, 0, 6, 1, 5, 2, 4, 3};
unsigned long long c = rdtscp();
for (int i = 0; i < 300000000; ++i) {
mask_rol(a, b);
}
printf("%11llun", rdtscp() - c);
return 0;
}

clang/LLVM通常对错误的依赖关系是鲁莽的。我认为,它试图避免在单个函数内的循环中创建循环携带的dep链,但通过制作这个经常被称为noinline的小函数,您已经克服了这一点。

有时,避免xor整条指令归零整数或向量regs可能值得冒险,但为mov al而不是movzx eax节省1字节的代码似乎不值得冒险。多年来,所有x86 CPU都具有高效的movzx负载。

几乎重复为什么添加xorps指令会使使用cvtsi2ss和addss~5x的函数更快?-由于clang对虚假依赖的傲慢态度,非内联函数调用创建了一个循环携带的dep链。在这种情况下,在XMM寄存器上,而不是标量int,其中P6家族的部分寄存器重命名实际上会打破错误的dep,在Sandybridge上也是如此。但在Haswell和更高版本中没有,它没有将low8与完整寄存器分开重命名:为什么GCC不使用部分寄存器?


所以,是的,这是一个clang遗漏的优化错误,或者是启发式没有得到回报的情况。我很好奇clang在不需要它来避免循环携带的错误依赖的代码中,总是使用movzx进行窄负载会有多大区别(正或负(。

如果在不同的CPU类型上,任何不利因素都很小,或者至少通过避免这样的速度减慢来平衡,Clang可能会改变这一点。(通过不必加载+合并,只需加载,需要更少的后端uop即可占用RS中的空间,从而提高性能。现代英特尔将mov al, mem解码为微融合加载+ALU。(

或者,如果出于某种原因,always-movzx策略总体上并不更好,它仍然应该在这个长的非循环dep链中的某个位置使用一个策略,比如AL和CL中的每一个至少在中间使用一个,以创建更多的ILP,即使函数只运行一次。和/或交替AL和DL或其他什么。(clang 13令人惊讶地将DL用于最后一个字节,但将AL用于前一个7:https://godbolt.org/z/7PYWGxsse-在未来的问题中,最好包含您自己的编译器资源管理器链接,其中包含与您测试的版本/选项相匹配的版本。(


虽然我不明白为什么它要填充dx和ax

看起来GCC正在重用相同的-2常量,使用mov edi, edx(2字节(八次,而不是mov edi, -2(5字节(八倍。也许代码大小不是原因,因为GCC正常情况下会花费代码大小来保存指令。IDK。

此外,GCC的寄存器分配有时在硬寄存器约束(如函数参数和返回值(方面是次优的。所以,是的,这只是在浪费指令,将传入的指针复制到RAX。函数不返回。dil是寄存器旋转的愚蠢选择;当CCD_ 23或CCD_。

最新更新