这章公式太多而且比较复杂,看的时候只是粗略地过了一遍,现在亲自推理一遍,加深印象

VS调试设置:x86 Release模式

以下每部分会得到3个产物,分别为:

  • x/c的优化公式
  • 计算c的公式
  • 计算M的公式(如果存在M)

a. 除数为无符号2的幂

#include <stdio.h>
int main(int argc, char* argv[]) //argc表示被除数x
{
	printf("argc / 16 = %u", (unsigned)argc / 16);
	return 0;
}

反汇编(只展示关键代码的反汇编,下文同理)

printf("argc / 16 = %u", (unsigned)argc / 16);
00EB1043  mov         eax,dword ptr [argc]  
00EB1046  shr         eax,4  
00EB1049  push        eax  
00EB104A  push        offset string "argc / 16 = %u" (0EB20E8h)  
00EB104F  call        printf (0EB1010h)  
00EB1054  add         esp,8

这里只用了shr做右移运算,把eax右移了4位
公式写出来就是

\[\left[ \frac{x}{2^n} \right] = x >> n \]

其中 c 是 2^n ,n表示右移n位

b. 除数为无符号非2的幂(上)

#include <stdio.h>  
int main(int argc, char* argv[])
{  
	printf("argc / 3 = %u", (unsigned)argc / 3); 
	return 0;  
}

反汇编

printf("argc / 3 = %u", (unsigned)argc / 3);
00101043  mov         eax,0AAAAAAABh        ;<== Magic Number
00101048  mul         eax,dword ptr [argc]  
0010104B  shr         edx,1  
0010104D  push        edx  
0010104E  push        offset string "argc / 3 = %u" (01020E8h)  
00101053  call        printf (0101010h)  
00101058  add         esp,8

这里生成了一个魔数M,它是怎么来的呢
由于除法指令时间周期长,而乘法指令时间周期短,所以编译器选择乘法指令优化。也就是将除法转换成乘法。
从最简单的两数相除开始,\(\frac{x}{c}\) 可以拆成乘法形式 \(x \cdot \frac{1}{c}\) ,然而 \(\frac{1}{c}\) 很难表示,就对分子分母同时乘上一个数,因为 \(2^n\) 在汇编语言上用一个移位指令就可以解决,所以就选它了,得到 \(x \cdot \frac{2^n}{c \cdot 2^n}\) ,改成乘法就是 \(x \cdot \frac{2^n}{c} \cdot \frac{1}{2^n}\) 。
注意到 \(\frac{2^n}{c}\) 分子分母都是常数,在编译期就可以得出,而且将除法结构改变为乘法结构,所以这部分常数就叫魔数,即 \(M = \frac{2^n}{c}\) ,最终优化结果是 \(\frac{x}{c} = \frac{x \cdot M}{2^n} = (x \cdot M) >> n\)

上述过程如下:

\[\frac{x}{c} = x \cdot \frac{1}{c} = x \cdot \frac{2^n}{c \cdot 2^n} = x \cdot \frac{2^n}{c} \cdot \frac{1}{2^n} = \frac{x \cdot M}{2^n} = (x \cdot M) >> n \]

实际上 \(2^n\) 通常会取得非常大,32位情况下n>32,64位情况下n>64,而shr指令对乘法结果的高位edx右移一位,也就是说此时n = 33

c即 \(\frac{2^n}{M}\)

c. 除数为无符号非2的幂(下)

#include <stdio.h>
int main(int argc, char* argv[]) {
	printf("argc / 7 = %u", (unsigned)argc / 7);
	return 0;
}

反汇编

printf("argc / 7 = %u", (unsigned)argc / 7);
005C1043  mov         ecx,dword ptr [argc]  
005C1046  mov         eax,24924925h  
005C104B  mul         eax,ecx  ;edx:eax = x * M
005C104D  sub         ecx,edx  ;ecx = x - edx <=> x - (x * M) / 2^32    //32位寄存器容量是32bit,取乘法结果高位就是除以2^32
005C104F  shr         ecx,1    ;ecx >> 1 <=> [x - (x * M) / 2^32] / 2
005C1051  add         ecx,edx  ;ecx = [x - (x * M) / 2^32] / 2 + (x * M) / 2^32
005C1053  shr         ecx,2    ;ecx >> 2 <=> {[x - (x * M) / 2^32] / 2 + (x * M) / 2^32} / 2^2
005C1056  push        ecx  
005C1057  push        offset string "argc / 7 = %u" (05C20E8h)  
005C105C  call        printf (05C1010h)  
005C1061  add         esp,8 

把最终结果整理一下,即

\[\left[\left(x - \frac{x \cdot M}{2^{32}}\right)\cdot \frac{1}{2} + \frac{x \cdot M}{2^{32}}\right] \cdot \frac{1}{2^2} = \frac{x}{2^3} + \frac{x \cdot M}{2^{35}} = x \cdot \frac{2^{32} + M}{2^{35}} \]

其中 \(2^{32} + M\) 是带进位值的魔数,这样做可以规避大数计算的问题。
这里M是多少呢,可以通过列方程解出:

\[x \cdot \frac{2^{32} + M}{2^{35}} = x \cdot \frac{1}{c} \]

解得:

\[M = \frac{2^{35}}{c} - 2^{32} \]

又因为 2^35 是取乘法高位后右移3次得到的,因此可以写为:

\[M = \frac{2^{32+n}}{c} - 2^{32} \]

变换一下形式得到c:

\[c = \frac{2^{32+n}}{M + 2^{32}} \]

所以 x/c 最终被优化为:

\[\frac{x}{c} = \left[\left(x - \frac{x \cdot M}{2^{32}}\right)\cdot \frac{1}{2} + \frac{x \cdot M}{2^{32}}\right] \cdot \frac{1}{2^{n-1}} \]

d.除数为有符号2的幂的优化

#include <stdio.h>
int main(int argc,char* argv[])
{
    printf("argc / 8 = %d", argc / 8);
    return 0;
}
printf("argc / 8 = %d", argc / 8);
00CE1043  mov         eax,dword ptr [argc]  
00CE1046  cdq                               ;把eax的符号位复制到edx的每一位,正数edx = 0,负数edx = 0xffffffff
00CE1047  and         edx,7                 ;正数edx = 0,负数edx = 7
00CE104A  add         eax,edx               ;正数 eax = argc + 0,负数eax = argc + 7
00CE104C  sar         eax,3                 ;正数 eax = argc >> 3,负数eax = (argc + 7) >> 3
00CE104F  push        eax  
00CE1050  push        offset string "argc / 8 = %d" (0CE20E8h)  
00CE1055  call        printf (0CE1010h)  
00CE105A  add         esp,8

C语言中除法是向零取整的,如果 \(x \geq 0\) ,

\[\left[ \frac{x}{2^n} \right] = \lfloor \frac{x}{2^n} \rfloor \Leftrightarrow x >> n \]

\(x \leq 0\) ,

\[\left[ \frac{x}{2^n} \right] = \lceil \frac{x}{2^n} \rceil \]

根据推导7,\(x \leq 0\) 时,

\[\left[ \frac{x}{2^n} \right] = \lceil \frac{x}{2^n} \rceil = \lfloor \frac{x+2^n-1}{2^n} \rfloor \Leftrightarrow (x+2^n-1) >> n \]

常数c的表达式同上

e.除数为有符号非2的幂

#include <stdio.h>
int main(int argc, char* argv[])
{
	printf("argc / 9 = %d", argc / 9);
	return 0;
}

反汇编

printf("argc / 9 = %d", argc / 9);
00D01043  mov         eax,38E38E39h     ;Magic Number
00D01048  imul        dword ptr [argc]  ;argc * M
00D0104B  sar         edx,1             ;(argc * M) / (2^32 * 2) 乘法结果高32位右移
00D0104D  mov         eax,edx  
00D0104F  shr         eax,1Fh           ;取符号位 负数为1,正数为0
00D01052  add         eax,edx           ;负数为(argc * M) / (2^32 * 2) + 1,正数为(argc * M) / (2^32 * 2)
00D01054  push        eax  
00D01055  push        offset string "argc / 9 = %d" (0D020E8h)  
00D0105A  call        printf (0D01010h)  
00D0105F  add         esp,8

当 x > 0时:

\[\left[ \frac{x}{c} \right]=\lfloor \frac{x \cdot M}{2^n} \rfloor \]

当 x < 0时:

\[\left[ \frac{x}{c} \right] = \lceil \frac{x \cdot M}{2^n} \rceil = \lfloor \frac{x \cdot M}{2^n} \rfloor + 1 \]

其中

\[M = \frac{2^n}{c} \]

变形得到

\[c = \frac{2^n}{M} \]

f.除数为有符号非2的幂

#include <stdio.h>
int main(int argc, char* argv[])
{
	printf("argc / 7 = %d", argc / 7);
	return 0;
}

反汇编:

printf("argc / 7 = %d", argc / 7);
00A61043  mov         ecx,dword ptr [argc]  
00A61046  mov         eax,92492493h  
00A6104B  imul        ecx                  ;argc * M
00A6104D  add         edx,ecx              ;argc * M / 2^32 + argc
00A6104F  sar         edx,2                ;(argc * M / 2^32 + argc ) / 2^2
00A61052  mov         eax,edx  
00A61054  shr         eax,1Fh              ;取符号位 负数为1,正数为0
00A61057  add         eax,edx              ;负数(argc * M / 2^32 + argc ) / 2^2 + 1,正数(argc * M / 2^32 + argc ) / 2^2
00A61059  push        eax  
00A6105A  push        offset string "argc / 7 = %d" (0A620E8h)  
00A6105F  call        printf (0A61010h)  
00A61064  add         esp,8

x > 0时

\[\frac{x}{c} = ( \frac{x \cdot M}{2^{32}} + x ) \cdot \frac{1}{2^n} \]

\[M = \frac{2^{32+n}}{c} - 2^{32} \]

\[c = \frac{2^{32+n}}{M + 2^{32}} \]

x < 0时

\[\frac{x}{c} = ( \frac{x \cdot M}{2^{32}} + x ) \cdot \frac{1}{2^n} + 1 \]

posted on 2026-09-23 21:36  %HuTao%  阅读(8)  评论(0)    收藏  举报