系统
当前位置: 主页 > 杂文详谈 > 系统
对于乘法运算的优化
发布日期:2022-10-26 阅读次数:

主要通过观察反汇编代码来观测编译器对代码的优化策略,主要观察默认情况下的优化策略-O1编译选项下的优化策略

乘法指令对应的汇编指令为:

  • 有符号乘法imul
  • 无符号乘法mul

乘法指令执行周期过长,编译器会首先通过移位配合加法、减法来完成,当使用这些指令都无法完成时,才会使用乘法指令

实例代码

#include 
#include 

int main(int argc,char** argv)
{

        int nVarOne = argc;
        int nVarTwo = argc;

        printf("nVarOne * 15 = %d",nVarOne * 15);

        printf("nVarOne * 16 = %d",nVarOne * 16);

        printf("nVarOne * 4 + 5 = %d",nVarOne * 4 + 5);

        printf("nVarOne * nVarTwo = %d",nVarOne * nVarTwo);

        return 0;
}

逐行观察

无优化选项

基础变量的赋值

1666766785_6358d7c18ab37dae98a0c.png!small?1666766777773

如图所示,在不优化的前提下,会先将argcargv(地址) 分别放入栈中,之后再依据代码,将argc分别放在nVarOnenVarTwo变量所对应的地址中

nVarOne : $rbp - 0x8

nVarTwo : %rbp - 0x14

1:printf("nVarOne * 15 = %d",nVarOne * 15);

1666766790_6358d7c6226bb6cfc177d.png!small?1666766782378

nVarOne * 15的操作在红色方框中实现,编译器并没有直接使用乘法指令,而是借助了16 - 1 = 15这个简单的机制,进行了一定的优化:

  1. mov edx,DWORD PTR [rbp-0x8]nVarOne的值(也就是1) 放入rdx寄存器低位中
  2. mov eax,edx再将1放入rax寄存器的低位中
  3. shl eax,0x4关键操作:使用左移运算将乘法拆分,左移 4 位,此时eax = 16
  4. sub eax,edx由于edx中的值为1,所以完成16 - 1 = 15的运算,这也正是我们nVarOne * 15的结果
  5. mov esi,eax将结果放入esi,用于后续传给printf即可

2:printf("nVarOne * 16 = %d",nVarOne * 16);

1666766793_6358d7c94c879d7e63a1d.png!small?1666766785535

有了上一个优化的方法,这个就非常简单了,直接 左移 4 位即可

  • shl eax,0x4左移4eax = 16

3:printf("nVarOne * 4 + 5 = %d",nVarOne * 4 + 5);

1666766796_6358d7cc72a6e5b1f3988.png!small?1666766788670

方式同上:将nVarOne * 4 + 5转换为nVarOne << 2 + 5

shl eax,0x2add eax,0x5

4:printf("nVarOne * nVarTwo = %d",nVarOne * nVarTwo);

1666766799_6358d7cf4f8fafe9ca2e8.png!small?1666766791564

此时由于nVarOnenVarTwo均为未知变量,而不像上述几种情况中带有常量,所以在非 O1 优化的情况下,直接使用imul有符号乘法进行相乘

O1 优化选项

在 O1 优化的情况下,没有给nVarOnenVarTwo分配空间,而是直接将参数作为局部变量来使用,所以甚至没有设置rbp的值,唯一的开辟的 8 字节空间只是为了保存之前rbp的值

1:printf("nVarOne * 15 = %d",nVarOne * 15);

1666766803_6358d7d30084aa515b177.png!small?1666766795225

整体的思路还是1 << 4 - 1但此时没有使用栈空间,而是直接将argc的值放入ebp中 (mov ebp , edi),再直接对ebp进行移位与减法的操作,最终将其放入rdx进行输出即可

1666766806_6358d7d663cd51ad93a49.png!small?1666766798745

2:printf("nVarOne * 16 = %d",nVarOne * 16);

1666766809_6358d7d94d3a7eebf2aae.png!small?1666766801544

此处直接传递ebp中的值,由于ebp之前的值未1 << 4,所以此处直接输出即可

3:printf("nVarOne * 4 + 5 = %d",nVarOne * 4 + 5);

1666766812_6358d7dcb9313503ee247.png!small?1666766804954

之前将edi的值传递了两次,一个份放在了ebp中,一份放在了ebx中,此处的这种优化方式就比较有意思了,区别于之前的先移位在做加法,此处直接借用lea指令完成计算,当这种组合运算中的乘数≠ 2、4、8时,编译器首先会尝试将乘数拆分为2、4、8的一种再配合其余运算,若无法拆分则使用imul计算或其余优化方式

nVarOne * 9 + 5

1666766816_6358d7e0f0079059ab4a8.png!small?1666766809127

4:printf("nVarOne * nVarTwo = %d",nVarOne * nVarTwo);

1666766819_6358d7e3d0dcc04ed9a20.png!small?1666766812085

此处依旧是使用imul无符号乘法进行运算

本文转载自FreeBuf.COM