目录
算术运算与赋值
编译器常用的两种优化方案
常量传播
常量折叠
加法
Debug编译选项组下编译后的汇编代码分析
Release开启02执行效率优先
减法
Release版下优化和加法一致,不再赘述
乘法
除法
算术结果溢出
自增和自减
关系运算与逻辑运算
JCC指令
位运算
算术运算与赋值
算术运算包括加法、减法、乘法和除法,也称为四则运算。
赋值运算类似于数学中的“等于”,是将一个内存空间中的数据传递到另一个内存空间。因为内存没有处理器那样的控制能力,所以各个内存单元之间是无法直接传递数据的,必须通过处理器访问并中转,以实现两个内存单元之间的数据传输。
编译器常用的两种优化方案
在编译过程中,编译器常常会采用“常量传播”和“常量折叠”的方案对代码中的变量与常量进行优化
常量传播
将编译期间可计算出结果的变量转换成常量,这样就减少了变量的使用
常量折叠
当出现多个常量进行计算,且编译器可以在编译期间计算出结果时,源码中所有的常量计算都将被计算结果代替
如果在程序的逻辑中,声明的变量没有被修改过,而且上下文中不存在针对此变量的取地址和间接访问操作,那么这个变量就等价于常量,编译器就认为可以删除这个变量,直接用常量代替。使用常量的好处是可以生成立即数寻址的目标代码,常量作为立即数成为指令的一部分,从而减少了内存的访问次数。
加法
加法运算对应的汇编指令为ADD。在执行加法运算时,不同的操作数对应的转换指令不同,编译器会根据优化方式选择最佳的匹配方案。在编译器中常用的优化方案有如下两种。
- 生成文件占用空间最少。
- 执行效率最快。
在VS中,Release编译选项组的默认选项为02选项——执行效率最快。在Debug编译选项组中,使用的是Od+ZI选项,此选项使编译器产生的一切代码都以便于调试为根本前提,甚至为了便于单步跟踪以及源码和目标代码块的对应阅读,不惜增加冗余代码。当然也不是完全放弃优化,在不影响调试的前提下,会尽可能地进行优化。
Debug编译选项组下编译后的汇编代码分析
源码
#include<stdio.h>
int main()
{
int n1 = 0;
int n2 = 0;
// 变量+常量
n1 = n1 + 1;
// 常量+常量
n1 = 1 + 2;
// 变量+变量
n1 = n1 + n2;
printf("n1=%d\n", n1);
return 0;
}
反汇编分析
归纳:
- 两个常量相加:编译期间就会计算出结构
- 有变量参与:变量取值存入寄存器相加后通过寄存器存入变量
Release开启02执行效率优先
开启02选项后,编译出来的汇编代码会有较大的变化。由于效率优先,编译器会将无用代码去除,并对可合并代码进行归并处理。
例如在代码清单4-1中,“n1 = n1 + 1;”这样的代码将被删除,因为在其后又重新对变量n1进行了赋值操作,而在此之前没有对变量n1做任何访问,所以编译器判定此句代码是可被删除的。
减法
计算机中,减法是通过加法实现的,减正等于加负,负数可以使用反码来代替;
源码
#include<stdio.h>
int main(int argc, char* argv[])
{
int n1 = argc;
int n1;
int n2 = 0;
scanf("%d", &n2);
n1 = n1 - 100;
n1 = n1 + 5 - n2;
printf("n1 = %d \r\n", n1);
return 0;
}
反汇编
Release版下优化和加法一致,不再赘述
乘法
乘法运算对应的汇编指令分为有符号imul和无符号mul两种。由于乘法指令的执行周期较长,在编译过程中,编译器会先尝试将乘法转换成加法,或使用移位等周期较短的指令。当它们都不可转换时,才会使用乘法指令。
源码
#include<stdio.h>
int main(int argc, char* argv[])
{
int n1 = argc;
int n2 = argc;
// 变量乘常量
printf("n1 * 15 = %d\n", n1 * 15);
// 变量乘常量(2的幂)
printf("n1 * 16 = %d\n", n1 * 16);
// 两个常量相乘
printf("2 * 2 = %d\n", 2 * 2);
printf("n2 * 4 + 5 = %d\n", n2 * 4 + 5);
// 混合运算
printf("n1 * n2 = %d\n", n1 * n2);
// 两变量相乘
return 0;
}
反汇编
有符号数乘以常量值,且这个常量非2的幂,会直接使用有符号乘法imul指令或者左移加减运算进行优化。
当常量值为2的幂时,编译器会采用执行周期短的左移运算代替执行周期长的乘法指令。由于任何十进制数都可以转换成二进制数表示,在二进制数中乘以2就等同于所有位依次向左移动1位。
乘法运算与加法运算的结合编译器采用LEA指令处理。LEA语句的目的并不是获取地址。
除了两个未知变量的相乘无法优化外,其他形式的乘法运算都可以进行优化处理。如果运算表达式中有一个常量值,则此时编译器会首先匹配各类优化方案,最后对不符合优化方案的运算进行调整。
无符号乘法的原理与之相同
除法
除法运算对应的汇编指令分为有符号idiv和无符号div两种。除法指令的执行周期较长,效率也较低,所以编译器会想尽办法用其他运算指令代替除法指令。C++中的除法和数学中的除法不同,在C++中,除法运算不保留余数,有专门求取余数的运算(运算符为%),也称之为取模运算。对于整数除法,C++的规则是仅保留整数部分,小数部分完全舍弃。
编译器在除法的优化涉及到高深的数学知识,暂且放放
算术结果溢出
当数据大小超过存储空间时,就会发生溢出,溢出的数据不会保留;
进位:无符号数超出存储范围叫作进位。因为没有符号位,不会破坏数据,而进位的1位数据会被进位标志为CF保存。而在标志位CF中,可通过查看进位标志位CF,检查数据是否进位
溢出:有符号数超出存储范围叫作溢出,由于数据进位,从而破坏了有符号数的最高位——符号位。只有有符号数才有符号位,所以溢出只针对有符号数。可查看溢出标志位OF,检查数据是否溢出。OF的判定规则很简单,如果参与加法计算的数值符号一致,而计算结果符号不同,则判定OF成立,其他都不成立。
自增和自减
C++中使用“++”“--”来实现自增和自减操作。自增和自减有两种定义:
- 一种为自增自减运算符在语句块之后,则先执行语句块,再执行自增自减;
- 另一种恰恰相反,自增自减运算符在语句块之前,则先执行自增和自减,再执行语句块。
- 通常,自增和自减是被拆分成两条汇编指令语句执行的
源码
#include<stdio.h>
int main(int argc, char* argv[])
{
int n1 = argc;
int n2 = argc;
n2 = 5 + (n1++);
n2 = 5 + (++n1);
n1 = 5 + (n2--);
n1 = 5 + (--n2);
return 0;
}
反汇编
先将自增自减运算进行分离,然后根据运算符的位置来决定执行顺序 。 将 原 语 句 块 “n1=5+
(n1++);”分解为“n2=5+n1;”和“n1=n1+1”,这样就实现了先参与语句块运算,再自增1。同理,前缀++的拆分过程只是执行顺序做了替换,先将自身加1,再参与表达式运算。在识别过程中,后缀++必然会保存计算前的变量值,在表达式计算完成后,才取出之前的值加1,这是个显著特点。
关系运算与逻辑运算
- 或:比较运算符||左右的语句的结果,如果有一个值为真,则返回真值;如果都为假,则返回假值。
- 与:比较运算符&&左右的语句的结果,如果有一个值为假,则返回假
- 值;如果都为真值,则返回真值。
- 非:改变运算符!后面语句的真假结果,如果该语句的结果为真值,则返回假值;如果为假值,则返回真值。
JCC指令
通常情况下,这些条件跳转指令都与CMP和TEST匹配出现,但条件跳转指令检查的是标记位。因此,在有修改标记位的代码处,也可以根据需要使用条件跳转指令修改程序流程。
位运算
二进制数据的运算称为位运算,位运算操作符如下:
- “<<”:左移运算,最高位左移到CF中,最低位零
- “>>”:右移运算,最高位不变,最低位右移到CF中。
- “|”:位或运算,在两个数的相同位上,只要有一个为1,则结果 为1。
- “&”:位与运算,在两个数的相同位上,只有同时为1时,结果才 为1。
- “^”:异或运算,在两个数的相同位上,当两个值相同时为0,不同时为1。
- “~”:取反运算,将操作数每一位上的1变0,0变1。
有待提升之处:
1. JCC指令,位运算的反汇编指令不够熟练
2.除法的优化原理涉及复杂的数学知识,还没了解