最近在用 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
的性能问题,除非在非常性能敏感的场景下。