更多精彩文章,请关注作者微信公众号:码工笔记。
一、问题
今天查一个问题的时候遇到了一个有意思的知识点,这里记录一下。
背景不多说了,总之是在 Windows 用 VS2019 开发一个 C++ 的程序,调到一个开源库里类似下面的两行代码(非真实代码,意会即可):
int k = 0;
int x = --k / 2;
x 应该是多少呢?
立即回答出 0 的同学给你点个赞,我当时是觉得有点不确定 -- 和 / 哪个优先级高,于是调试了一下,顺便看了一下汇编,其汇编大概是这个样子(VS的截图没保存,用万能的CompilerExplorer[1]看一下):
然后就发现了一些奇怪的事,上图中红框标出的部分,你知道它在干啥吗?
首先是 CDQ 指令:
- CDQ:Convert Double to Quad,将双字长扩展成四字长,也即将 EAX 的最高位(即符号位)拷贝到 EDX 中的所有 bit 中去(有符号扩展)。
要做除法了,先扩展再除,除完以后一个寄存器放商,一个放余数,也能理解。
后面的 sar eax, 1 也好理解,算术右移 1 位来实现除以 2。
但中间的 sub eax, edx 是在做什么呢?
刚才这个case中,eax 也就是 k,这时是 -1,也就是 0xFFFFFFFF,cdq 执行完后 edx = 0xFFFFFFFF。
sub eax, edx 就直接把 eax 变成 0 了,后面是对 0 又做了一个 sar 算术右移。。。这是什么道理?
二、原因
经过一番研(gu)究(ge),终于搞清楚咋回事了。
C 语言标准建议:当对有符号整数进行除法运算时,编译器应尽量将除得的结果向靠近 0 的方向取整,也即正数要向下取整,负数要向上取整,例如:
假设被除数为 ,除数为 ,则上面的规则可表示为:
- 当 时, 的结果取下整数:
这种情况下直接向右做算术移位即可。
- 当 时, 的结果取上整数:
其经过简单转换后得到:
即需要先对 x 做运算,再做算术右移。
举例来说,就是当 时:
三、结果验证
那么 msvc 编译器是按这个算法实现的吗?
先来看一下本文开始的 CompilerExplorer 截图中的汇编程序:
- sub eax, edx 这条指令在执行时:
- 若 eax 是正数,edx 中全是 0,此条指令相当于是 nop;
- 若 eax 是负数,edx 中全是 1,此条指令相当于 eax -= -1,即 eax += 1;
- 然后再来个 sar eax, 1;正是实现了。
再来看一下 的情况如何呢?CompilerExplorer 的编译结果如下:
-
当 eax > 0 时:
- cdq 将 edx 填为 0x00000000
- and edx, 7 相当于 nop
- add eax, edx 相当于 nop
- sar eax, 3 右移 3 位,实现了
-
当 eax < 0 时:
- cdq 将 edx 填为 0xFFFFFFFF
- and edx, 7 使 edx = 7
- add eax, edx 相当于 eax += 7,即
- sar eax, 3 右移 3 位
即实现了 。
参考资料
[1] gcc.godbolt.org/