为什么n++比n=n+1执行更快?

33
在C语言中,为什么 n++ 的执行速度比 n=n+1 快?
(int n=...;  n++;)
(int n=...;  n=n+1;)

这是今天我们老师在课堂上提出的问题。(这不是作业)


9
你是如何发现这件事的?使用的编译器/操作系统/平台/架构是什么? - Mehrdad Afshari
77
一般情况下不行。你不能简单地说 "ab 快"(其中 ab 是 C 表达式)。只有在硬件 c 上,使用编译器 e 的版本 d 和优化标志 f 编译时才有意义地说 "ab 快"(以及其他一些要求)。 - Stephen Canon
11
从这个例子中可以明确看出正在被要求什么;即使答案似乎显而易见,也不应该关闭它。赞成重新打开。 - bdonlan
14
@Stephen: 因此,回答需要对提问者进行纠正。尽管如此,这仍然是一个好问题,因为其他人可能会遇到相同的错误信息,而这个问题将有助于澄清。 - bdonlan
5
@Stephen,另外,如果你真的认为这个问题不应该问一个错误的陈述,请随意编辑它为“n++n = n + 1更快,为什么?”这比仅仅关闭问题而没有讨论要有建设性得多 :) - bdonlan
显示剩余21条评论
10个回答

103

如果你正在使用一个"石器时代"的编译器,那么这是正确的......

"石器时代"中:
++nn++快,而n++又比n=n+1快。
计算机通常有增加x以及将常量加到x上

  • 对于n++,你只需要2次内存访问(读n、加n、写n)
  • 对于n=n+1,你需要3次内存访问(读n、读常量、将n和常量相加、写n)

但是今天的编译器会自动将n=n+1转换为++n,它会做更多超出你想象的事情!!

此外,在今天的乱序处理器上 - 即使是"石器时代"的编译器,许多情况下运行时间完全不会受影响!

相关链接


6
@gcc: 不,你不应该那么说。你应该说:"这两者之间根本不应该有任何区别。自从Wulf等人在《优化编译器的设计》一书中描述的Bliss-11编译器以来,优化编译器已经处理了类似这样的问题数十年了。" - John R. Strohm
4
在我学习这个的那台电脑上,++nn++速度完全相同,与load-increment-store无关,你会看到唯一的速度差异取决于n是存储在寄存器还是内存中(不是n现在是否在寄存器中,而是它分配的存储是否在寄存器中),例如move (n)+,r0将n移动到寄存器零中,并在移动后递增n。(VAX-11汇编) - Stephen P
2
+1 表示实际回答问题并提供有用的信息,而不是只说它们是相同的,虽然这也很有用,但这可能不是提问者想要的信息。 - Ben Zotto
3
说编译器会将“n=n+1”转换为“++n”是错误的,甚至是无意义的。这就好比说翻译会在将英语翻译成法语时将“it is”转换为“it's”。这两者之间的区别是源语言的产物。 - R.. GitHub STOP HELPING ICE
2
我曾经学过,++n可能更快,但永远不会比n++慢。对于任何给定的硬件来说都是如此。 - EnabrenTane
显示剩余10条评论

42
在x86上的GCC 4.4.3中,无论是否进行优化,它们都编译成完全相同的汇编代码,因此执行时间相同。正如您在汇编中看到的那样,GCC只是将 n++ 转换为 n=n+1,然后在 -O2 中将其优化为一条指令的加法。
您的导师建议 n++ 更快,只适用于非常旧的、不进行优化的编译器,这些编译器没有足够智能来选择适合 n = n + 1 的原地更新指令。这些编译器在PC世界已经过时多年,但可能仍可在奇怪的专有嵌入式平台上找到。
C代码:
int n;

void nplusplus() {
    n++;
}

void nplusone() {
    n = n + 1;
}

输出汇编代码(未经过优化):

    .file   "test.c"
    .comm   n,4,4
    .text
.globl nplusplus
    .type   nplusplus, @function
nplusplus:
    pushl   %ebp
    movl    %esp, %ebp
    movl    n, %eax
    addl    $1, %eax
    movl    %eax, n
    popl    %ebp
    ret
    .size   nplusplus, .-nplusplus
.globl nplusone
    .type   nplusone, @function
nplusone:
    pushl   %ebp
    movl    %esp, %ebp
    movl    n, %eax
    addl    $1, %eax
    movl    %eax, n
    popl    %ebp
    ret
    .size   nplusone, .-nplusone
    .ident  "GCC: (Ubuntu 4.4.3-4ubuntu5) 4.4.3"
    .section    .note.GNU-stack,"",@progbits

优化等级为-O2时的汇编输出:

    .file   "test.c"
    .text
    .p2align 4,,15
.globl nplusplus
    .type   nplusplus, @function
nplusplus:
    pushl   %ebp
    movl    %esp, %ebp
    addl    $1, n
    popl    %ebp
    ret
    .size   nplusplus, .-nplusplus
    .p2align 4,,15
.globl nplusone
    .type   nplusone, @function
nplusone:
    pushl   %ebp
    movl    %esp, %ebp
    addl    $1, n
    popl    %ebp
    ret
    .size   nplusone, .-nplusone
    .comm   n,4,4
    .ident  "GCC: (Ubuntu 4.4.3-4ubuntu5) 4.4.3"
    .section    .note.GNU-stack,"",@progbits

13

编译器会将 n + 1 优化成空值。

您是指 n = n + 1 吗?

如果是这样,它们将编译为相同的汇编代码。(假设开启了优化,并且它们是语句而不是表达式)


5
谁说这样做会有影响?实际上,你的编译器会将其全部优化掉,这就使得这个问题无关紧要了。

3
现代编译器应该能够将这两种形式识别为等价的,并将它们转换为最适合目标平台的格式。但是,有一个例外情况:具有副作用的变量访问。例如,如果n是某个内存映射硬件寄存器,则从中读取和写入可能不仅仅是传输数据值(例如读取可能会清除中断)。您需要使用volatile关键字来让编译器知道它需要小心地优化对n的访问,在这种情况下,编译器可能会从n++(增量操作)和n = n + 1(读取、添加和存储操作)生成不同的代码。然而,对于普通变量,编译器应该将这两种形式优化为相同的结果。

这个答案是错误的。volatile 对于 ++nn=n+1 之间没有任何区别。两者都会导致一次读取和一次写入,而且都不是原子操作。如果你想要原子性,volatile 无法提供;请等待 C1x 中的 _Atomic 类型。 - R.. GitHub STOP HELPING ICE
2
@R:我从未声称“volatile”提供了原子性。你评论中的其余部分在一般情况下并不正确(除了最后一句话);编译器优化代码的细节是特定于实现的。我的答案仅仅解释了如何使用“volatile”可能会影响编译器优化的一般情况。 - bta
@R..GitHubSTOPHELPINGICE:标准并不要求volatile提供原子性,但如果平台能够在硬件寄存器上执行一些原子操作而不支持C11原子所需的所有操作中的所有操作,则记录实现涉及volatile的一些构造以原子方式似乎是提供此类支持的最实用方法。比必须以破碎的方式实现所有操作且无法以硬件支持的有用方式实现任何操作的原子类型要好得多。 - supercat

2

实际上并没有。编译器会根据目标架构进行特定的更改。这样微小的优化通常效果不明显,但重要的是,肯定不值得程序员花费时间。


2
实际上,原因在于后缀运算符和前缀运算符的定义不同。 ++n 将增加 "n" 并返回对 "n" 的引用,而 n++ 将增加 "n" 并返回 "n" 的 const 副本。 因此,短语 n = n + 1 将更有效率。但我必须同意以上评论者们的观点。好的编译器应该能够优化未使用的返回对象。

1
@R 更重要的是,从历史记录来看,它被编辑了惊人的13次,包括标题更改、标签更改和问题更改。如果不考虑历史背景就给我打分,那就相当糟糕了。通常情况下,我不会这么自大,但这是否意味着我需要检查所有我的答案,以确保问题没有被编辑到几个月后我会被踩? - wheaties
抱歉,我不知道。如果可以的话,我会删除-1。 - R.. GitHub STOP HELPING ICE
@R 为什么你不能去掉那个-1呢? - Ergwun
如果答案被编辑过,那么在一定时间后,您只能更改您的投票。 - R.. GitHub STOP HELPING ICE

2
在C语言中,表达式n++的副作用“根据定义”等价于表达式n = n + 1的副作用。由于您的代码仅依赖于副作用,因此很明显这两个表达式的性能完全相同。(顺便提一下,无论编译器的优化设置如何,问题绝对与任何优化无关)。
任何实际的性能差异只有在编译器有意(且恶意!)尝试引入差异时才可能发生。但在这种情况下,它可以朝着编译器作者希望偏向的任何方向发展。

1
-1:旧编译器由于优化不足,会为不同的语句发出不同的指令... - RCIX
4
许多简单的编译器确实只进行直接翻译,例如始终使用ADD指令来代替“+”运算符,即使在INC更好的情况下也是如此。例如,过去常常手动使用“>>”来除以2的幂次方,因为如果使用“/”运算符,编译器会使用非常慢的DIV指令。 - Daniel Newby
2
@Daniel Newby:这意味着一个天真的非生产级编译器。也就是说,由那些不理解自己在做什么的人编写的编译器。那些实现了自己对C语言的错误理解而不是实际规范的人。为什么有人会使用这样的“编译器”超出了我的理解。实际上,在C语言中不存在所谓的“字面翻译”。语言规范中没有任何暗示+应该被实现为ADD,++应该通过INC实现的内容。 - AnT stands with Russia
3
@AndreyT: 不,它只是意味着一个简单的编译器,这在微控制器和奇怪的系统中仍然很常见。有一些为小众平台提供整个编译器的公司,其员工比Visual C++优化团队还要少,尽管机器代码效率略有欠缺,但它们的产品非常实用。此外,C语言被有意设计成运算符直接映射到通用机器指令,因此简单的直接翻译就可以生成完整的编译器。 - Daniel Newby
1
@AndreyT - 功能等效性是必须的。相同的实现(和性能)并非如此。不同的实现并不意味着不同的结果。编译器可以选择以不同的方式实现++和+=1,尤其是在历史上经常这样做。我模糊地记得有一个故事(准确性不保证),关于最初的C编译器专门包括前缀和后缀变量的++和--运算符,以利用目标平台机器代码的专门寻址模式,当用于指针时也进行了解引用,并使用[]进行索引。 - user180247
显示剩余5条评论

2

我觉得这更像是一个硬件问题,而不是软件问题... 如果我没记错的话,在旧的CPU中,n=n+1需要两个内存位置,而++n只是一个微控制器命令... 但我怀疑这是否适用于现代架构...


0
所有这些都取决于编译器/处理器/编译指令。因此,做出任何“一般情况下哪个更快”的假设都不是一个好主意。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接