zoukankan      html  css  js  c++  java
  • ++i? i++? i+=1? i=i+1? 何必纠结?

    前言

    今天在牛客上看面经,看到一个问题:num++; num+=1; num = num +1; 哪个效率最高?
    自从学习C语言开始,我就在纠结for语言应该写i++,还是++i,其实这个问题,可以通过汇编代码来看看。

    区别

    首先说明,自增操作符是 num = num + 1 或者 num += 1 的缩写,但又有不同,比如 C++ 中涉及到了操作符重载,其他语言又有不同的特性,但是本文只讨论最简单最经典的 C 。

    赋值顺序:

    int m = i++; // 变量 m 被赋值为 i 后,变量 i 才自增
    int m = ++i; // 变量 i 自增后,变量 m 才被赋值为 i
    

    i++ 只能作为右值,而 ++i 可以作为左右值:

    int *p1 = &(++i); // 正确
    int *p2 = &(i++); // 错误
    ++i = 1; // 正确
    i++ = 1; // 错误
    

    i++ 不能作为左值的原因,观察其汇编可以知道,i++ 返回的只是一个临时变量,或者说只是一个存在寄存器中的值。而 ++i 返回的就是 i 本身,或者说是 i 的引用地址。

    底层汇编

    先来看一段代码:

    int main() {
        int i = 0;
        i++;
        ++i;
        i+=1;
        i=i+1;
        return 0;
    }
    

    在 gcc -O0 无优化编译后的汇编代码为:

    a.out`main:
        0x100000f70 <+0>:  pushq  %rbp
        0x100000f71 <+1>:  movq   %rsp, %rbp
        0x100000f74 <+4>:  xorl   %eax, %eax
        0x100000f76 <+6>:  movl   $0x0, -0x4(%rbp)
        0x100000f7d <+13>: movl   $0x0, -0x8(%rbp)
        0x100000f84 <+20>: movl   -0x8(%rbp), %ecx
        0x100000f87 <+23>: addl   $0x1, %ecx
        0x100000f8a <+26>: movl   %ecx, -0x8(%rbp)
        0x100000f8d <+29>: movl   -0x8(%rbp), %ecx
        0x100000f90 <+32>: addl   $0x1, %ecx
        0x100000f93 <+35>: movl   %ecx, -0x8(%rbp)
        0x100000f96 <+38>: movl   -0x8(%rbp), %ecx
        0x100000f99 <+41>: addl   $0x1, %ecx
        0x100000f9c <+44>: movl   %ecx, -0x8(%rbp)
        0x100000f9f <+47>: movl   -0x8(%rbp), %ecx
        0x100000fa2 <+50>: addl   $0x1, %ecx
        0x100000fa5 <+53>: movl   %ecx, -0x8(%rbp)
        0x100000fa8 <+56>: popq   %rbp
        0x100000fa9 <+57>: retq   
    

    可以惊讶地发现,四种写法的汇编代码竟然都一样:

    movl   -0x8(%rbp), %ecx
    addl   $0x1, %ecx
    movl   %ecx, -0x8(%rbp)
    

    从这一点看,似乎四种写法的开销都是两次内存访问。但是他们的功能不都一样,我们可以这样再改:

    int main() {
        int i = 0;
        int m;
        m = i++;
        m = ++i;
        m = i+=1;
        m = i=i+1;
        return 0;
    }
    

    再看汇编,发现了变化:

    a.out`main:
        0x100000f70 <+0>:  pushq  %rbp
        0x100000f71 <+1>:  movq   %rsp, %rbp
        0x100000f74 <+4>:  xorl   %eax, %eax
        0x100000f76 <+6>:  movl   $0x0, -0x4(%rbp)
        0x100000f7d <+13>: movl   $0x0, -0x8(%rbp)
        0x100000f84 <+20>: movl   -0x8(%rbp), %ecx
        0x100000f87 <+23>: movl   %ecx, %edx
        0x100000f89 <+25>: addl   $0x1, %edx
        0x100000f8c <+28>: movl   %edx, -0x8(%rbp)
        0x100000f8f <+31>: movl   %ecx, -0xc(%rbp)
        0x100000f92 <+34>: movl   -0x8(%rbp), %ecx
        0x100000f95 <+37>: addl   $0x1, %ecx
        0x100000f98 <+40>: movl   %ecx, -0x8(%rbp)
        0x100000f9b <+43>: movl   %ecx, -0xc(%rbp)
        0x100000f9e <+46>: movl   -0x8(%rbp), %ecx
        0x100000fa1 <+49>: addl   $0x1, %ecx
        0x100000fa4 <+52>: movl   %ecx, -0x8(%rbp)
        0x100000fa7 <+55>: movl   %ecx, -0xc(%rbp)
        0x100000faa <+58>: movl   -0x8(%rbp), %ecx
        0x100000fad <+61>: addl   $0x1, %ecx
        0x100000fb0 <+64>: movl   %ecx, -0x8(%rbp)
        0x100000fb3 <+67>: movl   %ecx, -0xc(%rbp)
        0x100000fb6 <+70>: popq   %rbp
        0x100000fb7 <+71>: retq   
    

    m = i++; 对应的汇编为:

    movl   -0x8(%rbp), %ecx
    movl   %ecx, %edx
    addl   $0x1, %edx
    movl   %edx, -0x8(%rbp)
    movl   %ecx, -0xc(%rbp)
    

    三次内存访问,用了两个寄存器。
    另外三种写法的汇编为:

    movl   -0x8(%rbp), %ecx
    addl   $0x1, %ecx
    movl   %ecx, -0x8(%rbp)
    movl   %ecx, -0xc(%rbp)
    

    同样三次内存访问,不过相比之下,只用了一个寄存器。

    这么一看,由于寄存器操作速度是相当快的,访问内存才是效率的决定因素,所以四种写法效率差别并不大,甚至可以忽略不计。硬要说就是 i++; 这种写法最慢,另外三种写法一样。

    结论

    既生 ++i ,何生 i++ ?唯一一个理由就是,手指有些短,习惯先按 i 再按 + 。
    不过用生命中宝贵的几秒钟来纠结 CPU 的几个时钟周期,真的不值得。窗外的月光,更令人着迷。

  • 相关阅读:
    第五周 day5 python学习笔记
    第四周 day4 python学习笔记
    第三周 day3 python学习笔记
    常用屏幕分辨率
    学习可以借鉴的大牛们的网站
    jq动态控制样式的一些方法(批量控制样式,带参数控制样式)
    jq实现事件委托
    h5Css新加的一些新的属性
    css布局模型
    task_13
  • 原文地址:https://www.cnblogs.com/trav/p/10260078.html
Copyright © 2011-2022 走看看