序
主要通过观察反汇编代码来观测编译器对代码的优化策略,主要观察默认情况下的优化
策略与-O1编译选项下的优化策略
乘法指令对应的汇编指令为:
有符号乘法imul
无符号乘法mul
乘法指令执行周期过长,编译器会首先通过移位配合加法、减法来完成 当使用这些指
令都无法完成时,才会使用乘法指令
实例代码
#include <stdio.h>
#include <stdlib.h>
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,0x2add 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无符号乘法进行运算 |