最近在用 for 循环的时候,发现了自增运算符几种不同的写法,有点好奇,就想复习回顾下。
1 自增/减运算符
1.1 初始印象
首先捋一下运算符在前后的差别,脑子中第一印象是这个
- ++i,先加,再用
 - i++,先用,再加
 
1.2 深入剖析
除此之外,还有什么区别呢?我尝试在汇编代码中找一下区别,看下机器语言的执行过程
1.2.1 i++ 汇编实现
i++ 的测试代码如下
int fortest()
{int i = 0;return i++;
}
 
fortest():push    rbpmov     rbp, rspmov     DWORD PTR [rbp-4], 0    ; 初始化int变量mov     eax, DWORD PTR [rbp-4]  ; 先使用变量i的值,拷贝一次lea     edx, [rax+1]            ; 原变量值做一次自增操作mov     DWORD PTR [rbp-4], edx	; 将自增后的edx再拷贝给初始int变量pop     rbpret
 
参考 ChatGPT 回答如下:
push rbp: 将当前函数的基址指针(Base Pointer,BP)压入栈中,为当前函数的栈帧(Stack Frame)做准备。mov rbp, rsp: 将栈指针(Stack Pointer,SP)的值赋给基址指针,建立当前函数的栈帧。mov DWORD PTR [rbp-4], 0: 将值0存储到基址指针减4的内存位置,即在栈帧中分配一个4字节的空间,并将其初始化为0。mov eax, DWORD PTR [rbp-4]: 将栈帧中存储的值加载到寄存器eax中。lea edx, [rax+1]: 将寄存器eax中的值加1,并将结果存储到寄存器edx中。这里使用的是地址计算指令(LEA,Load Effective Address),实际上不会执行真正的加法操作,而是将地址计算的结果存储到目标寄存器中。mov DWORD PTR [rbp-4], edx: 将寄存器edx中的值存储到栈帧中原先存储值0的位置,相当于将原先的值加1。pop rbp: 函数结束时,恢复调用函数前的基址指针。ret: 返回,结束当前函数的执行。
1.2.2 ++i 汇编实现
同样的作为对比,看一下 ++i 的汇编代码
int fortestV2()
{int i = 0;return ++i;
}
 
对应汇编语言代码如下
fortestV2():push    rbpmov     rbp, rspmov     DWORD PTR [rbp-4], 0    ; 初始化add     DWORD PTR [rbp-4], 1    ; 变量自增mov     eax, DWORD PTR [rbp-4]  ; 返回自增后的临时变量pop     rbpret
 
1.3 小结
在此之中发现了一点猫腻,可以看到后缀 i++ 运算符,多了一次拷贝操作。从机器语言的执行过程来看,多了一步操作,就会多一点耗时。
另外,与其他朋友交流的过程中,提到了现代编译器是否会对此代码做优化,暂未有结论,咨询 ChatGPT 结果看起来也是通用的,目前还没具体了解到会对类似代码做优化,记录一下,后续如果学习到了,再作补充完善。
附上 ChatGPT 回答:
 现代编译器通常会进行优化,以尽可能地消除 i++ 和 ++i 的性能差异。这种优化可能包括:
-  
寄存器分配: 编译器可能会将循环中的计数器
i分配到一个寄存器中,这样无论是前置递增还是后置递增,都可以在寄存器中执行,而不必每次都访问内存。 -  
循环展开: 编译器可能会将循环展开为一系列简单的操作,以减少递增操作的次数。在这种情况下,
i++和++i的性能差异可能会被进一步减小。 -  
代码重排: 编译器可能会重新排列代码以利用处理器的流水线和缓存。这可能会导致
i++和++i的性能差异变得不那么明显。 
总的来说,现代编译器会尽量优化代码,以提高性能并减少不必要的性能差异。因此,开发者通常不需要太过担心 i++ 和 ++i 的性能问题,除非在非常性能敏感的场景下。