浅谈sar指令在整数除法中的优化

探讨SAR指令在整数除法中的应用及优化技巧,包括2^n除法和非2^n除法的实现原理及具体步骤。
标 题:  【转载】浅谈sar指令在整数除法中的优化


初学《加密与解密3》看到第四章里的编译器的算法优化时的一点困惑,然后动手实践了一下得到一点心得体会,拿来跟大家分享!
   ps.本人汇编基础非常差,sar指令也是刚刚学会的,另外脑袋转的慢,别人用5分钟能解决的问题,我得用10分钟,因此这篇文章我花了6个多小时才完成。
如果有错误欢迎指出,以免误导他人!

浅谈Sar指令在整数除法中的优化

还不懂sar指令的同学马上去baidu啊,Google啊查去,我刚查完!
首先来看一段vc代码
代码:
int get()
{
  return -8;
}

int main()
{
  int a = get()/2;
  printf("%d",a);
  return 0;
}
汇编代码(本文里反汇编码都是指Release版本下的OD返回编码)如下
代码:
00401000  /$  B8 F8FFFFFF   mov     eax, -8   ; get子程序 总是返回-8
00401005  \.  C3            retn
00401006      90            nop
00401010  /$  E8 EBFFFFFF   call    00401000  ;得到返回值 eax = -8
00401015  |.  99            cdq                ;将eax的值符合扩展到edx
00401016  |.  2BC2          sub     eax, edx   ;等价于 eax = eax + 符号位(eax为正数或者0时符号位为0,负数时为1)
00401018  |.  D1F8          sar     eax, 1      ;带符合右移一位
0040101A  |.  50            push    eax
0040101B  |.  68 30704000   push    00407030      ;  ASCII "%d"
00401020  |.  E8 0B000000   call    00401030       ;printf函数
00401025  |.  83C4 08       add     esp, 8
00401028  |.  33C0          xor     eax, eax
0040102A  \.  C3            retn
0040102B      90            nop
简单的伪代码表示如下
代码:
Mov eax, idata              ;idata表示一个整数
Add eax, eax的符号位
Sar eax, 1
上面是一个整数/2的情况 
如果一个整数/2^n(这里2^n表示2的n次方) 应该如何表示呢
我们将上面的VC代码修改这么一句
int a = get()/2; =>   int a = get()/16;
通过反汇编自己翻译成伪代码就是这样的
代码:
Mov eax, idata              ;idata表示一个整数
Add eax, (eax的符号位)*(2^n - 1) ;这里n=4
Sar eax, n
上面这个就是一般的除数是2^n一般的公式了,正负整数都管用

粗略的用这样一个数学公式来表示:
Idata/(2^n) = YWn(idata+2^n - 1)
注意 YWn表示将其后面的数以sar方式移动n位 即 sar (数),n

再变换一下,将idata换成(idata – 2^n + 1)
(Idata – 2^n+1)/(2^n) = YWn(idata)             -----------最终公式

Ps.上面的公式是我OD反汇编总结出来的,缺少数学的严格推理证明。我不会证,谁来证明下啊?感激不尽!

下面来解释一个难点,也是写本文的初衷
看一段VC代码:
代码:
int get()
{
  int i=rand()%2;
   if(i)
  return rand();
   else
   return rand()*(-1);
}

int main()
{
  int a = get()/9;
  printf("%d",a);
  return 0;
}
反汇编码:
代码:
00401010  /$  E8 EBFFFFFF   call    00401000   ;调用get函数 eax=随即数
00401015  |.  8BC8          mov     ecx, eax  ; ecx = eax
00401017  |.  B8 398EE338   mov     eax, 38E38E39 ;这是一个编译器预先设定好的MagicNumber
0040101C  |.  F7E9          imul    ecx ;带符合的乘法,不多说
0040101E  |.  D1FA          sar     edx, 1 ;将乘法的高32位结果算数右移一位
00401020  |.  8BC2          mov     eax, edx ;eax = edx
00401022  |.  C1E8 1F       shr     eax, 1F   ;逻辑右移31位
00401025  |.  03D0          add     edx, eax ;以上这3句的意思就是  edx = edx+edx的符号位,本文最重要的目的就是解释他为什么加1
00401027  |.  52            push    edx
00401028  |.  68 30704000   push    00407030              ;  ASCII "%d"
0040102D  |.  E8 0E000000   call    00401040  ;printf edx
00401032  |.  83C4 08       add     esp, 8
00401035  |.  33C0          xor     eax, eax
00401037  \.  C3            retn
关于MagicNumber这里给你几个连接自己看,不多介绍
http://bbs.pediy.com/showthread.php?t=68849
http://bbs.pediy.com/showthread.php?t=81456
我们把上面的反汇编代码写成伪代码,如下
代码:
idata 表示一个32位有符号数,这里一定是有符号的,否则逻辑错误
magic 表示立即数0x38E38E39  他是一个正数
FH(idata) 表示idata的最高位,即 FH(idtat) = (idata & 0x80000000)? 1:0
[edx,eax]表示 edx和eax组成的64位的寄存器

Mov [edx,eax], magic*idata
Sar [edx,eax],33      ;由于移位的操作此时eax里面的值已经没有意义了
Add edx + idata的符号位  ;因为magic是个正数,所以等价于magic*idata的符号位
而我们的本意是 idata/9
因此得到如下数学公式,我们就是要证明他的正确性
YW33(magic*idata) + FH(idata) = idata/9          -------------- (黄金公式)
①.  当FH(idata) == 0时
上述黄金公式即
Magic*idata/2^33 = idata/9

Magic*9 = 2^33
我们将magic = 0x0x38E38E39带入  然后用科学计算器验证了下结果如下
Magic*9 = 0x200000001 
2^33 = 0x200000000
这种优化的除法本身就是一种近似,这里不多讨论为什么存在一个1的差异
证毕。
②.  当FH(idata) == 1时
由 最终公式(Idata – 2^n+1)/(2^n) = YWn(idata)
得到
YW33(magic*idata) = (magic*idata – 2^33 + 1)/(2^33)
将右边带入黄金公式即:
(magic*idata – 2^33 + 1)/(2^33) + 1 = idata/9

Magic*idata/2^33 – 1 + 1 = idata/9          (;1/2^33 ≈ 0因此忽略掉)

Magic*idata/2^33 = idata/9

Magic*9 = 2^33

























浅谈Sar指令在整数除法中的优化

第一部分

首先来看一段vc代码

int get()

{

    return-8;

}

int main()

{

    inta = get()/2;

    printf("%d",a);

    return0;

}

汇编代码(本文里反汇编码都是指Release版本下的OD返回编码)如下

00401000  /$  B8 F8FFFFFF  mov     eax, -8   ; get子程序 总是返回-8

00401005  \. C3            retn

00401006      90            nop

00401010  /$  E8EBFFFFFF   call    00401000  ;得到返回值 eax = -8

00401015  |. 99            cdq              ;将eax的值符合扩展到edx

00401016  |. 2BC2          sub     eax, edx  ;等价于 eax = eax + 符号位(eax为正数或者0时符号位为0,负数时为1)

00401018  |.  D1F8         sar     eax, 1      ;带符合右移一位

0040101A  |. 50            push    eax

0040101B  |.  6830704000   push    00407030     ;  ASCII "%d"

00401020  |.  E80B000000   call    00401030       ;printf函数

00401025  |.  83C4 08      add     esp, 8

00401028  |.  33C0         xor     eax, eax

0040102A  \. C3            retn

0040102B      90            nop

 

简单的伪代码表示如下

moveax, idata              ;idata表示一个整数

addeax, eax的符号位

sareax, 1

 

上面是一个整数/2的情况

如果一个整数/2^n(这里2^n表示2的n次方) 应该如何表示呢

我们将上面的VC代码修改这么一句

inta = get()/2; => int a = get()/16;

通过反汇编自己翻译成伪代码就是这样的

moveax, idata              ;idata表示一个整数

addeax, (eax的符号位)*(2^n - 1) ;这里n=4

sareax, n

上面这个就是一般的除数是2^n一般的公式了,正负整数都管用

 

粗略的用这样一个数学公式来表示:

idata/(2^n) = YWn(idata+2^n - 1)

注意 YWn表示将其后面的数以sar方式移动n位即 sar (数),n

 

再变换一下,将idata换成(idata – 2^n + 1)

(idata – 2^n+1)/(2^n) = YWn(idata) -------  (最终公式)

 

ps.上面的公式是我OD反汇编总结出来的,缺少数学的严格推理证明。我不会证,谁来证明下啊?感激不尽!

注:(以上2个公式仅对负数管用,正数的公式是idata/(2^n)= YWn(idata) )

 

第二部分

再看一段VC代码:

int get()

{

    inti=rand()%2;

  if(i)

    returnrand();

  else

  return rand()*(-1);

}

int main()

{

    inta = get()/9;

    printf("%d",a);

    return0;

}

 

反汇编码:

00401010  /$  E8EBFFFFFF   call    00401000  ;调用get函数 eax=随即数

00401015  |. 8BC8          mov     ecx, eax ; ecx = eax

00401017  |.  B8398EE338   mov     eax,38E38E39 ;这是一个编译器预先设定好的MagicNumber

0040101C  |.  F7E9         imul    ecx       ;带符合的乘法,不多说

0040101E  |. D1FA          sar     edx, 1 ;将乘法的高32位结果算数右移一位

00401020  |. 8BC2          mov     eax, edx ;eax =edx

00401022  |. C1E8 1F       shr    eax, 1F   ;逻辑右移31

00401025  |. 03D0          add     edx, eax ;以上这3句的意思就是  edx = edx+edx的符号位,本文最重要的目的就是解释他为什么加1

00401027  |. 52            push    edx

00401028  |.  6830704000   push    00407030  ;  ASCII"%d"

0040102D  |.  E80E000000   call    00401040 ;printf edx

00401032  |.  83C4 08       add    esp, 8

00401035  |.  33C0          xor     eax, eax

00401037  \. C3            retn

 

关于MagicNumber这里给你几个连接自己看,不多介绍

http://bbs.pediy.com/showthread.php?t=68849

http://bbs.pediy.com/showthread.php?t=81456

我们把上面的反汇编代码写成伪代码,如下

idata表示一个32位有符号数,这里一定是有符号的,否则逻辑错误

magic表示立即数0x38E38E39  他是一个正数

FH(idata)表示idata的最高位,即 FH(idtat) = (idata & 0x80000000)? 1:0

[edx,eax]表示 edx和eax组成的64位的寄存器

 

mov[edx,eax], magic*idata

sar[edx,eax],33 ; 移位的操作执行完本行指令后eax里面的值就没有意义了

addedx + idata的符号位 ;因为magic是个正数,所以等价于magic*idata的符号位

 

而我们的本意是 idata/9

因此得到如下数学公式,我们就是要证明他的正确性

YW33(magic*idata) + FH(idata) = idata/9 ---------- (黄金公式)

①.当FH(idata) == 0时

上述黄金公式即

magic*idata/2^33 = idata/9

magic*9 = 2^33

我们将magic = 0x0x38E38E39带入  然后用科学计算器验证了下结果如下

magic*9 = 0x200000001

2^33 = 0x200000000

这种优化的除法本身就是一种近似,这里不多讨论为什么存在一个1的差异

证毕。

②.当FH(idata) == 1时

最终公式 (Idata – 2^n+1)/(2^n) = YWn(idata)

得到

YW33(magic*idata) = (magic*idata – 2^33+ 1)/(2^33)

将右边带入黄金公式即:

(magic*idata – 2^33 + 1)/(2^33) + 1 =idata/9

Magic*idata/2^33 – 1 + 1 = idata/9       (;1/2^33 ≈ 0因此忽略掉)

magic*idata/2^33 = idata/9

magic*9 = 2^33

证毕。

 

 


对于公式Idata/(2^n)=YWn(idata+2^n-1)
有一点需要说明,即intel使用的有符号除法的商是采取向零圆整的方式(如:(-5)/2=-2……-1,5/2=2……1),所以对于不能整除的数,移位的结果与商差1。这就是移位前加上(2^n-1)的原因。


假设负数idata的补码二进制表示为
bbbbb……b
    ______
      ↑
      n位


(2^n-1)二进制表示为
00001……1
    ______
      ↑
      n位


如果idata能够整除(2^n),则其二进制补码末尾n位应该都是零,此时加上(2^n-1)再移位与直接移位结果相同,结果等于商
如果idata不能整除(2^n),则末尾n位中必有不是零的部分,此时加上(2^n-1)再移位相当于将移位结果加1,这样就与商相等了。


<think>我们之前讨论了除法溢出时需要在指令后添加中断处理。但用户当前的问题似乎是关于编译器在除法指令后添加的优化指令,与溢出无关。 根据引用内容,编译器会将除法优化为乘法(当除数为常数时)。例如,除以常数3会被转换为乘以一个魔数(如0x55555556)并配合移位和加法操作。 具体来说,用户问题:“在除法指令后面编译器会添加的一条指令” —— 实际上,编译器并不是在除法指令后面添加指令,而是将除法指令替换为一系列其他指令(乘法、移位等)来实现优化。 但如果我们考虑编译器生成的指令序列,在优化除法时,它可能会生成多条指令。然而,用户问的是“添加一条指令”,这可能是指在除法指令(或替换后的指令序列)之后添加的指令。 然而,在引用[2]的代码示例中,我们可以看到在乘法指令(imul)之后,编译器添加了移位(shr/sar)和加法(add)等指令。这些指令是用来修正结果的。 但用户特别指出“一条指令”,那么最典型的可能是:在乘法之后,编译器通常会添加一条移位指令(shr或sar)来调整结果。因为根据优化原理,除以常数D可以转换为乘以一个魔数M(即2^N/D的整数部分),然后取乘积的高位,再通过移位来调整。 例如,在引用[2]中: 计算10/3: mov ecx, 10 mov eax, 055555556h ; M = 0x55555556 imul ecx ; 结果在edx:eax,通常取edx(高位) mov eax, edx shr eax, 01fh ; 这里是一条移位指令,用于调整符号位(有符号除法的修正) add edx, eax ; 然后加法指令 所以,在乘法指令之后,编译器添加了移位指令(shr/sar)和加法指令(add)。但如果我们只关注除法被转换为乘法后的第一条后续指令,那么通常是移位指令(shr或sar)。 另外,引用[1]提到,当除数为2的幂时,编译器会直接优化为乘法(乘以0.5)而不需要移位?不对,整数除法中,除数为2的幂时,通常用右移指令(算术右移或有符号,逻辑右移无符号)来实现。但引用[1]提到的是浮点数,它用乘法代替除法(乘以0.5代替除以2)。 总结:对于整数除法,当除数为常数且不是2的幂时,编译器会将其优化为乘法和移位、加减等指令序列。在乘法指令(imul)之后,通常会紧跟移位指令(如shr或sar)来调整结果。 因此,我们可以回答:在除法优化中,编译器在乘法指令后通常会添加一条移位指令(如shr或sar)来修正结果。 注意:这个问题可能不是指溢出,而是指编译器优化除法时生成的指令序列。 但是,用户之前的问题是关于除法溢出的,而这次的问题似乎转向了编译器优化。所以我们需要结合上下文。 由于用户两次问题之间没有直接关联,我们按照当前问题回答。 所以,对于问题“除法指令后编译器添加的一条指令是什么”,我们回答:在编译器将除法优化为乘法之后,通常会添加一条移位指令(如shr或sar)来调整结果。 但是,如果用户仍然是指除法溢出(即没有优化的情况),那么溢出处理是在除法指令后添加中断处理(如之前回答)。但根据引用内容,用户这次的问题明显是关于编译器优化除法的。 因此,我们按照优化来回答。 生成的相关问题: 1. 为什么编译器将除法优化为乘法? 2. 移位指令除法优化中起什么作用? 3. 这种优化适用于哪些情况? </think>在编译器优化中,除法指令后**最常添加的指令是移位指令(shr/sar)**,这是将除法转换为乘法优化时的关键步骤。具体原理如下: ### 编译器优化除法的典型流程 1. **替换除法为乘法** 编译器将除法 $a/b$ 转换为等价形式:$a \times \frac{2^n}{b} \div 2^n$ 其中 $\frac{2^n}{b}$ 是预计算的**魔数(Magic Number)**,$n$ 是移位位数[^2]。 2. **添加移位指令** 在乘法指令(`imul`/`mulss`)后添加移位指令完成优化: ```asm imul eax, magic_number ; 乘法替代除法 sar edx, shift_bits ; ★ 关键添加的移位指令(有符号用sar,无符号用shr) ``` ### 示例分析(引用[2]案例) - **计算 $10/5$**: ```asm mov ecx, 10 ; 被除数 mov eax, 066666667h ; 魔数(编译器预计算) imul ecx ; 乘法指令 sar edx, 1 ; ★ 添加的算术右移指令(关键步骤) ``` 此处 `sar edx, 1` 等价于 $\div 2$,最终完成 $\frac{10}{5}=2$ 的优化计算。 - **计算 $10/6$**: ```asm mov ecx, 10 mov eax, 02AAAAAABh ; 魔数 imul ecx shr eax, 01fh ; ★ 添加的逻辑右移指令(符号位处理) add edx, eax ; 最终修正 ``` ### 为什么需要移位指令? 1. **修正乘法结果** 魔数乘法产生 $a \times \frac{2^n}{b}$ 的结果存储在高低位寄存器(如 `edx:eax`),需右移 $n$ 位获取整数商。 2. **性能优化** 移位指令(1时钟周期)比除法指令(~20时钟周期)快数十倍[^1]。 3. **处理符号位** 有符号除法用 `sar`(算术右移)保留符号,无符号用 `shr`(逻辑右移)。 ### 特殊优化:除数为2的幂 当 $b=2^k$ 时,编译器直接替换为右移指令(无乘法步骤): ```asm mov eax, 100 sar eax, 3 ; 直接等价于 100/8 ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值