6 c++ assembly x86-64 type-conversion undefined-behavior
...或者更确切地说,为什么static_cast-ing不会减慢我的功能?
考虑下面的函数,它执行整数除法:
int Divide(int x, int y) {
int ret = 0, i = 32;
long j = static_cast<long>(y) << i;
while (x >= y) {
while (x < j) --i, j >>= 1;
ret += 1 << i, x -= j;
}
return ret;
}
Run Code Online (Sandbox Code Playgroud)
正如人们所期望的那样,这表现得相当不错。但是,如果我们删除static_cast第 3 行,如下所示:
int Divide(int x, int y) {
int ret = 0, i = 32;
long j = y << i;
while (x >= y) {
while (x < j) --i, j >>= 1;
ret += 1 << i, x -= j;
}
return ret;
}
Run Code Online (Sandbox Code Playgroud)
x对于较大和y较小的病理输入,此版本的执行速度明显较慢,有时慢数百倍(我没有严格测量,但应该相差不远) 。我很好奇,想研究一下原因,并尝试深入研究汇编代码。然而,除了第 3 行的转换差异之外,我得到了完全相同的输出。这是第 3 行输出以供参考(源代码):
和static_cast:
movsxd rax, dword ptr [rbp - 8]
mov ecx, dword ptr [rbp - 16]
shl rax, cl
mov qword ptr [rbp - 24], rax
Run Code Online (Sandbox Code Playgroud)
没有static_cast:
mov eax, dword ptr [rbp - 8]
mov ecx, dword ptr [rbp - 16]
shl eax, cl
cdqe
mov qword ptr [rbp - 24], rax
Run Code Online (Sandbox Code Playgroud)
其余部分相同。
我真的很好奇开销发生在哪里。
编辑:我已经进一步测试了一点,看起来 while 循环是花费大部分时间的地方,而不是y初始化时。额外的cdqe指令似乎不足以保证墙上时间的总增加。
一些免责声明,因为我收到了很多与实际问题无关的评论:
long在我的平台上是 8 个字节长,所以不会溢出。我想知道什么可能导致运行时间增加,而批评上述内容的评论实际上并没有解决这个问题。
相关的不是运行时间cdqe或movsxd对比mov,而是循环的不同起始值,导致不同的迭代计数,特别是对于病理情况。
未经优化的 Clang 完全按照编写的方式编译源代码,对 an 进行移位int,然后将结果符号扩展为long. 移位计数 UB 对于禁用优化的编译器来说是不可见的,因为为了实现一致的调试,它假设变量值可以在 statements 之间更改,因此行为取决于目标机器对操作数大小的移位计数所做的操作。
当针对 x86-64 进行编译时,会产生long j = (long)(y<<0);,即long j = y;,而不是将这些位放在 64 位值的顶部。
x86 标量移位类似于shl eax, cl使用 掩码计数&31(64 位操作数大小除外),因此移位使用的计数为32 % 32 == 0. 我认为 AArch64 会使移位计数饱和,即让您移出所有位。
请注意,它执行 32 位操作数大小shl eax, cl,然后使用符号扩展结果cdqe,而不是执行符号扩展重新加载y,然后执行 64 位操作数大小shl rax,cl。
如果您使用调试器单步执行,您可以准确地看到局部变量值。(这是未优化的调试构建的主要好处,这不是您应该进行基准测试的内容。)并且您可以计算迭代次数。
while (x >= y) {
while (x < j) --i, j >>= 1;
ret += 1 << i, x -= j;
}
Run Code Online (Sandbox Code Playgroud)
对于j = y,如果我们完全进入外循环,则内循环条件始终为 false。
所以它永远不会运行,j始终保持不变,并且i保持不变为 32。
1<<32再次编译为具有 32 位操作数大小的可变计数移位,因为1具有类型int。(1LL类型为long long,并且可以安全地左移 32)。在 x86-64 上,这只是一种缓慢的方法ret += 1;。
x -= j;当然是公正的x -= y;,所以我们正在计算要进行多少次减法x < y。
众所周知,对于大商来说,通过重复减法进行除法非常慢,因为运行时间与商成线性比例。
不过,你确实得到了正确的结果。耶。
顺便说一句,long在某些目标(例如 Windows x64 和 32 位平台)上仅是 32 位;如果您想要宽度两倍的类型,请使用long longor 。也许 static_assert 可以确保不是那么宽。int64_tintint
启用优化后,我认为同样的事情仍然成立:clang 看起来像是在没有存储/重新加载的情况下编译为类似的 asm。因此,它实际上/事实上定义了1<<32编译为 x86 移位指令的行为。
但我没有测试,这只是快速浏览一下 asm https://godbolt.org/z/M33vqGj5P并注意到类似的事情mov r8d, 1;shl r8d, cl(32 位操作数大小);add eax, r8d