加载字节时,Clang 不会将高位清零。这是错误还是故意的选择?
Clang does not zero the upper bits when loading a byte. Is this a bug or a deliberate choice?
例如,有了这个功能,
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
...
虽然我不明白为什么它会填充 dx
和 ax
,但这是来自 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("%11llu\n", rdtscp() - c);
return 0;
}
clang/LLVM 通常对虚假依赖不计后果。它试图避免在循环中创建 loop-carried dep 链 在单个函数 中,我认为,但是你已经通过制作这个小的 frequently-called 函数 frequently-called 击败了它 noinline
.
避免 xor-zeroing 整数或向量 reg 的整个指令有时可能值得冒险,但为 mov al
节省 1 个字节的代码而不是 movzx eax
似乎不值得风险。多年来,所有 x86 CPUs 都拥有高效的 movzx 加载。
- a non-inline function call creates a loop-carried dep chain due to clang's cavalier attitude towards false dependencies. In that case on XMM registers, rather than scalar int where P6-family partial register renaming would actually break the false dep, also on Sandybridge. But not on Haswell and later, which doesn't rename low8 separately from full registers:
几乎重复
所以是的,这是一个 clang missed-optimization 错误,或者它的启发式方法没有得到回报的情况。我很好奇在 不需要 的代码中,clang 始终使用 movzx
进行窄负载会产生多大的差异(正面或负面),以避免 loop-carried 错误的依赖关系。
如果在不同的 CPU 类型上所有的缺点都很微小,或者至少被避免像这样的减速的巨大优点所平衡,Clang 可能应该改变这一点。 (通过不需要加载+合并,只需加载,在 RS 中需要更少的 back-end uops 占用 space,从而提高性能。现代英特尔将 mov al, mem
解码为 micro-fused 加载+ ALU.)
或者如果由于某种原因 always-movzx 策略总体上不是更好,它仍然应该在这个长的 non-looping 深度链中的某处使用一个,比如每个中间至少一个AL 和 CL,创建更多的 ILP,即使函数只运行一次。 And/or 交替使用 AL 和 DL 之类的。 (clang 13 令人惊讶地使用 DL 作为最后一个字节,但 AL 用于前 7 个字节:https://godbolt.org/z/7PYWGxsse - 在以后的问题中,最好包含您自己的编译器资源管理器 link,其版本/选项与什么匹配你测试过。)
While I don't understand why it is filling dx and ax
看起来 GCC 正在重用相同的 -2
常量,使用 mov edi, edx
(2 个字节)八次而不是 mov edi, -2
(5 个字节)八次。也许 code-size 不是原因,因为 GCC 正常会花费 code-size 来保存指令。 IDK.
此外,GCC 的寄存器分配有时 sub-optimal 围绕 hard-register 约束,例如函数参数和 return 值。所以是的,它只是在浪费指令将传入指针复制到 RAX。该函数没有 return 它。 dil
是寄存器循环的愚蠢选择;当 al
或 dl
不需要时,它需要一个 REX 前缀。
例如,有了这个功能,
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
...
虽然我不明白为什么它会填充 dx
和 ax
,但这是来自 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("%11llu\n", rdtscp() - c);
return 0;
}
clang/LLVM 通常对虚假依赖不计后果。它试图避免在循环中创建 loop-carried dep 链 在单个函数 中,我认为,但是你已经通过制作这个小的 frequently-called 函数 frequently-called 击败了它 noinline
.
避免 xor-zeroing 整数或向量 reg 的整个指令有时可能值得冒险,但为 mov al
节省 1 个字节的代码而不是 movzx eax
似乎不值得风险。多年来,所有 x86 CPUs 都拥有高效的 movzx 加载。
所以是的,这是一个 clang missed-optimization 错误,或者它的启发式方法没有得到回报的情况。我很好奇在 不需要 的代码中,clang 始终使用 movzx
进行窄负载会产生多大的差异(正面或负面),以避免 loop-carried 错误的依赖关系。
如果在不同的 CPU 类型上所有的缺点都很微小,或者至少被避免像这样的减速的巨大优点所平衡,Clang 可能应该改变这一点。 (通过不需要加载+合并,只需加载,在 RS 中需要更少的 back-end uops 占用 space,从而提高性能。现代英特尔将 mov al, mem
解码为 micro-fused 加载+ ALU.)
或者如果由于某种原因 always-movzx 策略总体上不是更好,它仍然应该在这个长的 non-looping 深度链中的某处使用一个,比如每个中间至少一个AL 和 CL,创建更多的 ILP,即使函数只运行一次。 And/or 交替使用 AL 和 DL 之类的。 (clang 13 令人惊讶地使用 DL 作为最后一个字节,但 AL 用于前 7 个字节:https://godbolt.org/z/7PYWGxsse - 在以后的问题中,最好包含您自己的编译器资源管理器 link,其版本/选项与什么匹配你测试过。)
While I don't understand why it is filling dx and ax
看起来 GCC 正在重用相同的 -2
常量,使用 mov edi, edx
(2 个字节)八次而不是 mov edi, -2
(5 个字节)八次。也许 code-size 不是原因,因为 GCC 正常会花费 code-size 来保存指令。 IDK.
此外,GCC 的寄存器分配有时 sub-optimal 围绕 hard-register 约束,例如函数参数和 return 值。所以是的,它只是在浪费指令将传入指针复制到 RAX。该函数没有 return 它。 dil
是寄存器循环的愚蠢选择;当 al
或 dl
不需要时,它需要一个 REX 前缀。