从汇编语言角度看除数为2^n时 int 的除法

776 阅读3分钟

更多精彩文章,请关注作者微信公众号:码工笔记

一、问题

今天查一个问题的时候遇到了一个有意思的知识点,这里记录一下。

背景不多说了,总之是在 Windows 用 VS2019 开发一个 C++ 的程序,调到一个开源库里类似下面的两行代码(非真实代码,意会即可):

int k = 0; 
int x = --k / 2;

x 应该是多少呢?

立即回答出 0 的同学给你点个赞,我当时是觉得有点不确定 -- 和 / 哪个优先级高,于是调试了一下,顺便看了一下汇编,其汇编大概是这个样子(VS的截图没保存,用万能的CompilerExplorer[1]看一下):

image.png

然后就发现了一些奇怪的事,上图中红框标出的部分,你知道它在干啥吗?

首先是 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 的方向取整,也即正数要向下取整,负数要向上取整,例如:

  • 1/2=01/2=0
  • 5/2=25/2=2
  • 1/2=0-1/2=0
  • 5/2=2-5/2 = -2

假设被除数为 xx,除数为 2n2^n,则上面的规则可表示为:

  • x>=0x>=0 时,x2n\frac{x}{2^n} 的结果取下整数:
x2n=x2nxn \frac{x}{2^n} = \lfloor\frac{x}{2^n}\rfloor \Leftrightarrow x \gg n

这种情况下直接向右做算术移位即可。

  • x<0x < 0 时,x2n\frac{x}{2^n} 的结果取上整数:
x2n=x2n\frac{x}{2^n} = \lceil\frac{x}{2^n}\rceil

其经过简单转换后得到:

x2n=x+2n12n(x+2n1)n \lceil\frac{x}{2^n}\rceil = \lfloor\frac{x+2^n-1}{2^n}\rfloor \Leftrightarrow (x+2^n-1) \gg n

即需要先对 x 做运算,再做算术右移。

举例来说,就是当 x<0x<0 时:

  • x2=x+212(x+1)1\lceil\frac{x}{2}\rceil = \lfloor\frac{x+2-1}{2}\rfloor \Leftrightarrow (x+1)\gg 1

  • x8=x+23123(x+7)3\lceil\frac{x}{8}\rceil = \lfloor\frac{x+2^3-1}{2^3}\rfloor \Leftrightarrow (x+7)\gg3

三、结果验证

那么 msvc 编译器是按这个算法实现的吗?

先来看一下本文开始的 CompilerExplorer 截图中的汇编程序:

  • sub eax, edx 这条指令在执行时:
    • 若 eax 是正数,edx 中全是 0,此条指令相当于是 nop;
    • 若 eax 是负数,edx 中全是 1,此条指令相当于 eax -= -1,即 eax += 1;
  • 然后再来个 sar eax, 1;正是实现了(x+1)1(x+1)\gg1

再来看一下 x8\frac{x}{8} 的情况如何呢?CompilerExplorer 的编译结果如下:

image.png

  • 当 eax > 0 时:

    • cdq 将 edx 填为 0x00000000
    • and edx, 7 相当于 nop
    • add eax, edx 相当于 nop
    • sar eax, 3 右移 3 位,实现了 x23\lfloor\frac{x}{2^3}\rfloor
  • 当 eax < 0 时:

    • cdq 将 edx 填为 0xFFFFFFFF
    • and edx, 7 使 edx = 7
    • add eax, edx 相当于 eax += 7,即 x+231x + 2^3 - 1
    • sar eax, 3 右移 3 位

    即实现了 x+2313\lfloor\frac{x+2^3-1}{3}\rfloor

参考资料

[1] gcc.godbolt.org/

[2] zhidao.baidu.com/question/39…

[3] stackoverflow.com/questions/1…