• 分析各种表达式求值过程


    目录

    算术运算与赋值

    编译器常用的两种优化方案

    常量传播

    常量折叠

    加法

    Debug编译选项组下编译后的汇编代码分析

    Release开启02执行效率优先

    减法

    Release版下优化和加法一致,不再赘述

    乘法

    除法

    算术结果溢出

    自增和自减

    关系运算与逻辑运算

    JCC指令

    位运算


    算术运算与赋值

    算术运算包括加法、减法、乘法和除法,也称为四则运算。

    赋值运算类似于数学中的“等于”,是将一个内存空间中的数据传递到另一个内存空间。因为内存没有处理器那样的控制能力,所以各个内存单元之间是无法直接传递数据的,必须通过处理器访问并中转,以实现两个内存单元之间的数据传输。

    编译器常用的两种优化方案

    在编译过程中,编译器常常会采用“常量传播”和“常量折叠”的方案对代码中的变量与常量进行优化

    常量传播

            将编译期间可计算出结果的变量转换成常量,这样就减少了变量的使用

    常量折叠

            当出现多个常量进行计算,且编译器可以在编译期间计算出结果时,源码中所有的常量计算都将被计算结果代替

            如果在程序的逻辑中,声明的变量没有被修改过,而且上下文中不存在针对此变量的取地址和间接访问操作,那么这个变量就等价于常量,编译器就认为可以删除这个变量,直接用常量代替。使用常量的好处是可以生成立即数寻址的目标代码,常量作为立即数成为指令的一部分,从而减少了内存的访问次数。

    加法

    加法运算对应的汇编指令为ADD。在执行加法运算时,不同的操作数对应的转换指令不同,编译器会根据优化方式选择最佳的匹配方案。在编译器中常用的优化方案有如下两种。

    1. 生成文件占用空间最少。
    2. 执行效率最快。

    在VS中,Release编译选项组的默认选项为02选项——执行效率最快。在Debug编译选项组中,使用的是Od+ZI选项,此选项使编译器产生的一切代码都以便于调试为根本前提,甚至为了便于单步跟踪以及源码和目标代码块的对应阅读,不惜增加冗余代码。当然也不是完全放弃优化,在不影响调试的前提下,会尽可能地进行优化。

    Debug编译选项组下编译后的汇编代码分析

    源码

    1. #include<stdio.h>
    2. int main()
    3. {
    4. int n1 = 0;
    5. int n2 = 0;
    6. // 变量+常量
    7. n1 = n1 + 1;
    8. // 常量+常量
    9. n1 = 1 + 2;
    10. // 变量+变量
    11. n1 = n1 + n2;
    12. printf("n1=%d\n", n1);
    13. return 0;
    14. }

    反汇编分析

    归纳:

    1. 两个常量相加:编译期间就会计算出结构
    2. 有变量参与:变量取值存入寄存器相加后通过寄存器存入变量

    Release开启02执行效率优先

    开启02选项后,编译出来的汇编代码会有较大的变化。由于效率优先,编译器会将无用代码去除,并对可合并代码进行归并处理。

    例如在代码清单4-1中,“n1 = n1 + 1;”这样的代码将被删除,因为在其后又重新对变量n1进行了赋值操作,而在此之前没有对变量n1做任何访问,所以编译器判定此句代码是可被删除的。

    减法

    计算机中,减法是通过加法实现的,减正等于加负,负数可以使用反码来代替;

    源码

    1. #include<stdio.h>
    2. int main(int argc, char* argv[])
    3. {
    4. int n1 = argc;
    5. int n1;
    6. int n2 = 0;
    7. scanf("%d", &n2);
    8. n1 = n1 - 100;
    9. n1 = n1 + 5 - n2;
    10. printf("n1 = %d \r\n", n1);
    11. return 0;
    12. }

    反汇编

    Release版下优化和加法一致,不再赘述

    乘法

    乘法运算对应的汇编指令分为有符号imul和无符号mul两种。由于乘法指令的执行周期较长,在编译过程中,编译器会先尝试将乘法转换成加法,或使用移位等周期较短的指令。当它们都不可转换时,才会使用乘法指令。

    源码

    1. #include<stdio.h>
    2. int main(int argc, char* argv[])
    3. {
    4. int n1 = argc;
    5. int n2 = argc;
    6. // 变量乘常量
    7. printf("n1 * 15 = %d\n", n1 * 15);
    8. // 变量乘常量(2的幂)
    9. printf("n1 * 16 = %d\n", n1 * 16);
    10. // 两个常量相乘
    11. printf("2 * 2 = %d\n", 2 * 2);
    12. printf("n2 * 4 + 5 = %d\n", n2 * 4 + 5);
    13. // 混合运算
    14. printf("n1 * n2 = %d\n", n1 * n2);
    15. // 两变量相乘
    16. return 0;
    17. }

    反汇编

    有符号数乘以常量值,且这个常量非2的幂,会直接使用有符号乘法imul指令或者左移加减运算进行优化。

    当常量值为2的幂时,编译器会采用执行周期短的左移运算代替执行周期长的乘法指令。由于任何十进制数都可以转换成二进制数表示,在二进制数中乘以2就等同于所有位依次向左移动1位。
    乘法运算与加法运算的结合编译器采用LEA指令处理。LEA语句的目的并不是获取地址。

    除了两个未知变量的相乘无法优化外,其他形式的乘法运算都可以进行优化处理。如果运算表达式中有一个常量值,则此时编译器会首先匹配各类优化方案,最后对不符合优化方案的运算进行调整。
    无符号乘法的原理与之相同

    除法

            除法运算对应的汇编指令分为有符号idiv和无符号div两种。除法指令的执行周期较长,效率也较低,所以编译器会想尽办法用其他运算指令代替除法指令。C++中的除法和数学中的除法不同,在C++中,除法运算不保留余数,有专门求取余数的运算(运算符为%),也称之为取模运算。对于整数除法,C++的规则是仅保留整数部分,小数部分完全舍弃。

    编译器在除法的优化涉及到高深的数学知识,暂且放放

    算术结果溢出

    当数据大小超过存储空间时,就会发生溢出,溢出的数据不会保留;

    进位:无符号数超出存储范围叫作进位。因为没有符号位,不会破坏数据,而进位的1位数据会被进位标志为CF保存。而在标志位CF中,可通过查看进位标志位CF,检查数据是否进位

    溢出:有符号数超出存储范围叫作溢出,由于数据进位,从而破坏了有符号数的最高位——符号位。只有有符号数才有符号位,所以溢出只针对有符号数。可查看溢出标志位OF,检查数据是否溢出。OF的判定规则很简单,如果参与加法计算的数值符号一致,而计算结果符号不同,则判定OF成立,其他都不成立。

    自增和自减

    C++中使用“++”“--”来实现自增和自减操作。自增和自减有两种定义:

    • 一种为自增自减运算符在语句块之后,则先执行语句块,再执行自增自减;
    • 另一种恰恰相反,自增自减运算符在语句块之前,则先执行自增和自减,再执行语句块。
    • 通常,自增和自减是被拆分成两条汇编指令语句执行的

    源码

    1. #include<stdio.h>
    2. int main(int argc, char* argv[])
    3. {
    4. int n1 = argc;
    5. int n2 = argc;
    6. n2 = 5 + (n1++);
    7. n2 = 5 + (++n1);
    8. n1 = 5 + (n2--);
    9. n1 = 5 + (--n2);
    10. return 0;
    11. }

    反汇编

    先将自增自减运算进行分离,然后根据运算符的位置来决定执行顺序 。 将 原 语 句 块 “n1=5+
    (n1++);”分解为“n2=5+n1;”和“n1=n1+1”,这样就实现了先参与语句块运算,再自增1。同理,前缀++的拆分过程只是执行顺序做了替换,先将自身加1,再参与表达式运算。在识别过程中,后缀++必然会保存计算前的变量值,在表达式计算完成后,才取出之前的值加1,这是个显著特点。
     

    关系运算与逻辑运算

    • 或:比较运算符||左右的语句的结果,如果有一个值为真,则返回真值;如果都为假,则返回假值。
    • 与:比较运算符&&左右的语句的结果,如果有一个值为假,则返回假
    • 值;如果都为真值,则返回真值。
    • 非:改变运算符!后面语句的真假结果,如果该语句的结果为真值,则返回假值;如果为假值,则返回真值。

    JCC指令

    通常情况下,这些条件跳转指令都与CMP和TEST匹配出现,但条件跳转指令检查的是标记位。因此,在有修改标记位的代码处,也可以根据需要使用条件跳转指令修改程序流程。

    位运算

    二进制数据的运算称为位运算,位运算操作符如下:

    1. “<<”:左移运算,最高位左移到CF中,最低位零
    2. “>>”:右移运算,最高位不变,最低位右移到CF中。
    3. “|”:位或运算,在两个数的相同位上,只要有一个为1,则结果 为1。
    4. “&”:位与运算,在两个数的相同位上,只有同时为1时,结果才 为1。
    5. “^”:异或运算,在两个数的相同位上,当两个值相同时为0,不同时为1。
    6. “~”:取反运算,将操作数每一位上的1变0,0变1。

    有待提升之处:

    1. JCC指令,位运算的反汇编指令不够熟练

    2.除法的优化原理涉及复杂的数学知识,还没了解

  • 相关阅读:
    Python网络爬虫与信息提取 第1周网络爬虫之规则 单元1:Requests库入门
    技术学习:Python(21)|爬虫篇|selenium自动化操作浏览器
    mysql数据增删改
    Deployments
    LabVIEW利用局部放电分析高压电气设备状态诊断
    测试左移和测试右移,我们为何要“上下求索”?
    【打工日常】解决docker对镜像pull的很慢的问题
    数据要素安全流通:挑战与解决方案
    10. 元组、集合
    服务器怎么买,腾讯云服务器新手购买的流程方法步骤
  • 原文地址:https://blog.csdn.net/qq_61553520/article/details/133441977