这章公式太多而且比较复杂,看的时候只是粗略地过了一遍,现在亲自推理一遍,加深印象
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位
公式写出来就是
其中 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\)
上述过程如下:
实际上 \(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
把最终结果整理一下,即
其中 \(2^{32} + M\) 是带进位值的魔数,这样做可以规避大数计算的问题。
这里M是多少呢,可以通过列方程解出:
解得:
又因为 2^35 是取乘法高位后右移3次得到的,因此可以写为:
变换一下形式得到c:
所以 x/c 最终被优化为:
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\) ,
\(x \leq 0\) ,
根据推导7,\(x \leq 0\) 时,
常数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时:
当 x < 0时:
其中
变形得到
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时
x < 0时
浙公网安备 33010602011771号