序
主要通过观察反汇编代码来观测编译器对代码的优化策略,主要观察默认情况下的优化策略与-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;
}
逐行观察
无优化选项
基础变量的赋值
如图所示,在不优化的前提下,会先将argc
与argv
(地址) 分别放入栈中,之后再依据代码,将argc
分别放在nVarOne
和nVarTwo
变量所对应的地址中
nVarOne : $rbp - 0x8
nVarTwo : %rbp - 0x14
1:printf("nVarOne * 15 = %d",nVarOne * 15);
nVarOne * 15
的操作在红色方框中实现,编译器并没有直接使用乘法指令,而是借助了16 - 1 = 15
这个简单的机制,进行了一定的优化:
mov edx,DWORD PTR [rbp-0x8]
将nVarOne
的值(也就是1
) 放入rdx
寄存器低位中mov eax,edx
再将1
放入rax
寄存器的低位中shl eax,0x4
关键操作:使用左移运算将乘法拆分,左移 4 位,此时eax = 16
sub eax,edx
由于edx
中的值为1
,所以完成16 - 1 = 15
的运算,这也正是我们nVarOne * 15
的结果mov esi,eax
将结果放入esi
,用于后续传给printf
即可
2:printf("nVarOne * 16 = %d",nVarOne * 16);
有了上一个优化的方法,这个就非常简单了,直接 左移 4 位即可
shl eax,0x4
左移4
位eax = 16
3:printf("nVarOne * 4 + 5 = %d",nVarOne * 4 + 5);
方式同上:将nVarOne * 4 + 5
转换为nVarOne << 2 + 5
shl eax,0x2
add eax,0x5
4:printf("nVarOne * nVarTwo = %d",nVarOne * nVarTwo);
此时由于nVarOne
与nVarTwo
均为未知变量,而不像上述几种情况中带有常量,所以在非 O1 优化的情况下,直接使用imul
有符号乘法进行相乘
O1 优化选项
在 O1 优化的情况下,没有给nVarOne
与nVarTwo
分配空间,而是直接将参数作为局部变量来使用,所以甚至没有设置rbp
的值,唯一的开辟的 8 字节空间只是为了保存之前rbp
的值
1:printf("nVarOne * 15 = %d",nVarOne * 15);
整体的思路还是1 << 4 - 1
但此时没有使用栈空间,而是直接将argc
的值放入ebp
中 (mov ebp , edi
),再直接对ebp
进行移位与减法的操作,最终将其放入rdx
进行输出即可
2:printf("nVarOne * 16 = %d",nVarOne * 16);
此处直接传递ebp
中的值,由于ebp
之前的值未1 << 4
,所以此处直接输出即可
3:printf("nVarOne * 4 + 5 = %d",nVarOne * 4 + 5);
之前将edi
的值传递了两次,一个份放在了ebp
中,一份放在了ebx
中,此处的这种优化方式就比较有意思了,区别于之前的先移位在做加法,此处直接借用lea
指令完成计算,当这种组合运算中的乘数≠ 2、4、8
时,编译器首先会尝试将乘数拆分为2、4、8
的一种再配合其余运算,若无法拆分则使用imul
计算或其余优化方式
nVarOne * 9 + 5
4:printf("nVarOne * nVarTwo = %d",nVarOne * nVarTwo);
此处依旧是使用imul
无符号乘法进行运算