zoukankan      html  css  js  c++  java
  • 深入理解计算机操作系统(四)

    阅读经典——《深入理解计算机系统》03

    1. 复合型类型转换的内在原理
    2. 局部变量一定进内存?
    3. 奇葩的加载有效地址指令leal
    4. if...else和三元运算符

    复合型类型转换的内在原理

    上一篇文章的最后,我们讲解了复合型类型转换,比如从shortunsigned相当于分两步,先从short转换到int,再从int转换到unsigned。也就是说,先扩大范围,再改变符号性质。而实际上呢,在机器级程序中这个转换真的分两步吗?

    答案是,只需要一步,下面的指令就足以完成从shortunsigned的转换。假设转换前变量存储在寄存器%ax中,转换后的结果存储在%edx指向的内存地址中。(书中采用AT&T格式的汇编语言,与Intel格式的区别见参考资料。)

    movswl %ax, (%edx)
    

    这是一个简单的移动指令,与最普通的mov相比,后缀wl表示源操作数(即左操作数)长度为w(即word,一个字),目标操作数(即右操作数)长度为l(即long,双字),s表示从w扩展到l高位全部用符号位补充。仍然用上次的例子,源操作数为short s = -12345,用二进制表示为

    1100 1111 1100 0111
    

    按照movswl指令的规则将其扩展为4个字节32位

    1111 1111 1111 1111 1100 1111 1100 0111
    

    现在按照unsigned类型将该数翻译为10进制,结果为4294954951,完全正确。

    可见,在机器级代码中很容易实现复合型类型转换,只需要一次移动操作。但是,问题的实质是什么?

    寄存器或内存中的数据只有长度之分, 并没有符号类型之分。也就是说,从short转换到int其实仍然是上面的汇编代码,只不过同样的结果被识别为int类型后会被翻译为不同的10进制数。

    如果思考得更加深入,也许会提出另一个问题:我觉得复合型类型转换的另一条路径——从shortunsigned short再到unsigned更加合理,有没有指令可以实现这种方式的复合型类型转换?

    毋庸置疑,编译器的实现方式应该是和指令集无关的,所以如果想要改动编译器使其默认选择另一种转换路径的话,一定是可以实现的。就像下面这样

    movzwl %ax, (%edx)
    

    唯一的不同就是后缀的s变成了z,之前是按照符号位扩展,而现在是纯粹用0扩展。对于short s = -12345

    1100 1111 1100 0111
    

    执行上面的指令后,结果为

    0000 0000 0000 0000 1100 1111 1100 0111
    

    再按照unsigned将这个二进制数翻译为10进制,就是53191,对应了上一章的结果。

    因此,两种路径都很容易实现,而编译器选择了最合乎情理的那一条。

    局部变量一定进内存?

    通常,局部变量会在运行时进入栈,我们在第一章中已经介绍过进程的虚拟地址空间的分配。

    但是,如果函数非常短,以致于某些局部变量可以一直保存在寄存器中,以获得更短的执行时间,这时候,就不一定需要进内存了。

    比如交换函数的代码

    int exchange(int *xp, int y)
    {
        int x = *xp;
        *xp = y;
        return x;
    }
    

    编译为汇编代码如下

    #xp at %ebp+8, y at %ebp+12
    movl 8(%ebp), %edx         #Get xp
    #by copying to %eax below, x becomes the return value
    movl (%edx), %eax          #Get x at xp
    movl 12(%ebp), %ecx        #Get y
    movl %ecx, (%edx)          #Store y at xp
    

    程序并没有为局部变量x申请内存,而是一直保存在寄存器%eax中,而且%eax寄存器的值默认作为函数的返回值,程序效率很高。

    奇葩的加载有效地址指令leal

    IA32指令集(详细介绍见参考资料)中有这样一条加载有效地址指令leal,用法为leal S, D,效果是将S的地址存入D。可是这条指令往往用在计算乘法上,比如下面的例子

    leal (%eax, %eax, 2), %eax
    

    实现的功能相当于%eax = %eax * 3。括号中是一种比例变址寻址,将第一个数加上第二个数和第三个数的乘积作为地址寻址,leal的效果使源操作数正好是寻址得到的地址,然后将其赋值给%eax寄存器。为什么用这种方式算乘法,而不是用乘法指令imul呢?

    这是因为Intel处理器有一个专门的地址运算单元,使得leal的执行不必经过ALU,而且只需要单个时钟周期。相比于imul来说要快得多。因此,对于大部分乘数为小常数的情况,编译器都会使用leal完成乘法操作。

    if...else和三元运算符

    这是一个有趣的话题,我们常常会问,if...else和三元运算符相比,哪个性能更好?

    这就涉及到汇编层面的两个术语:条件跳转条件转移。以一个计算两数差的绝对值函数为例:

    int absdiff(int x, int y)
    {
        if (x < y)
            return y-x;
        else
            return x-y;
    }
    

    编译后的汇编代码如下:

    # x at %ebp+8, y at %ebp+12
        movl 8(%ebp), %edx       #Get x
        movl 12(%ebp), %eax      #Get y
        cmpl %eax, %edx          #Compare x:y
        jge .L2                  #if >= goto x_ge_y
        subl %edx, %eax          #Compute result = x-y
        jmp .L3                  #Goto done
    .L2                       #x_ge_y:
        subl %eax, %edx          #Compute result = x-y
        movl %edx, %eax          #Set result as return value
    .L3:                      #done: Begin completion code
    

    代码中做了两次条件跳转,分别是jge .L2jmp .L3,当满足条件时跳转到指定的代码位置。

    再来看三目运算符的代码:

    int absdiff(int x, int y)
    {
        return x < y ? y-x : x-y;
    }
    

    编译后的汇编代码如下:

    # x at %ebp+8, y at %ebp+12
        movl 8(%ebp), %ecx       #Get x
        movl 12(%ebp), %edx      #Get y
        movl %edx, %ebx          #Copy y
        subl %ecx, %ebx          #Compute y-x
        movl %ecx, %eax          #Copy x
        subl %edx, %eax          #Compute x-y and set as return value
        cmpl %edx, %ecx          #Compare x:y
        cmovl %ebx, %eax         #If <, replace return value with y-x
    

    我们发现跳转指令没了,取而代之的是最后一条cmovl指令,这就是我们所说的条件转移指令,后缀l表示Less,当前一条指令cmpl的结果为“小于”时,将%ebx赋值给%eax

    两段代码的区别:前者先根据条件跳转,跳转后再执行各个分支中的指令;而后者先把两个分支全部计算出结果,然后用一个条件转移指令给出结果。至于哪种方式性能更好,现在还不能下结论,我们需要了解一个处理器技术——分支预测。

    现代处理器往往采用多级流水线技术同时操作多条指令,当预取指令遇到分支的时候,如果等待实际选择结果,将会白白浪费多个时钟周期。所以处理器会采用分支预测技术猜测程序将会选择哪个分支,然后令该分支的指令进入流水线。现代处理器一般能达到90%以上的预测成功率,因此,分支预测可以大大提高指令执行效率。但是,一旦预测失败,需要撤销已经执行的操作,这往往会额外消耗掉20~40个时钟周期的时间,导致程序执行效率下降。

    回到我们之前的程序,采用指令跳转方式时就需要考虑分支预测带来的影响。如果该函数被调用多次而且参数值毫无规律,将会给分支预测带来巨大的困难,预测成功率很低,导致程序性能下降。在这种情况下,采用指令转移方式会好得多。

    但是,指令转移并不是万能的,如果两个分支中执行了非常耗时的操作,比如调用了另一个复杂的函数。那么,由于指令转移方式需要预先执行完每个分支的操作,用时会增加一倍,性能反而不如指令跳转方式。因此,采用哪种方式需要考虑实际情况。

    最后,回答刚开始提出的问题,if...else和三目运算符哪个性能更好?

    请不要被上面举的两个例子误导,其实它们是完全一样的。考虑到编译器的优化(具体的编译器优化选项见参考资料),无论是if...else语句还是三目运算符,生成的汇编代码很可能是一致的。编译器会考虑前面提到的因素,自动选择采用指令跳转方式还是指令转移方式实现。所以,对于C语言程序员来说,写哪个都一样。

    哈哈,看到这里是不是有点失望了?虽然理解了这么多实现原理却并没有什么用。不过理解程序的本质总是好的,万一哪天编译器耍小脾气不给我们优化了,我们还可以自己动手搞一套,艺多不压身嘛。

    参考资料



    作者:金戈大王
    链接:https://www.jianshu.com/p/eaa85130b8e4
    來源:简书
    简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。
  • 相关阅读:
    适配器模式
    自己DIY word2010脚注和尾注没有的格式
    Linux单网卡,双IP,双网关配置,并搭建squid proxy上网
    about using gnuplot
    ReadDirectoryChangesW 函数 流沙
    Jquery easyui 异步树 流沙
    Overlapped I/O 学习 流沙
    jQuery.get(url,[data],[callback]) 流沙
    MsgWaitForMultipleObjectsEx用法 流沙
    Oracle smon_scn_time 表 说明
  • 原文地址:https://www.cnblogs.com/zzdbullet/p/9354576.html
Copyright © 2011-2022 走看看