在C语言中,i++和++i之间有性能差异吗?

532

如果结果值未使用,i++++i之间是否存在性能差异?


2
相关的,关于C++的同样问题是https://dev59.com/tHVD5IYBdhLWcg3wTJvF - FluxLemur
14个回答

461

执行摘要:不。

i++可能比++i慢,因为i的旧值可能需要保存以供以后使用, 但在实践中,所有现代编译器都会优化掉这个问题。

我们可以通过查看此函数的代码来证明这一点, 使用++ii++两种方式。

$ cat i++.c
extern void g(int i);
void f()
{
    int i;

    for (i = 0; i < 100; i++)
        g(i);

}

文件是一样的,只是 ++ii++ 不同。
$ diff i++.c ++i.c
6c6
<     for (i = 0; i < 100; i++)
---
>     for (i = 0; i < 100; ++i)

我们将编译它们,并获得生成的汇编代码。
$ gcc -c i++.c ++i.c
$ gcc -S i++.c ++i.c

我们可以看到生成的目标文件和汇编文件是相同的。
$ md5 i++.s ++i.s
MD5 (i++.s) = 90f620dda862cd0205cd5db1f2c8c06e
MD5 (++i.s) = 90f620dda862cd0205cd5db1f2c8c06e

$ md5 *.o
MD5 (++i.o) = dd3ef1408d3a9e4287facccec53f7d22
MD5 (i++.o) = dd3ef1408d3a9e4287facccec53f7d22

12
虽然此问题涉及C语言,但我很想知道浏览器是否可以针对JavaScript执行此优化。请你将其翻译成通俗易懂的中文,但不要改变原本的意思。 - TM.
84
所以,“不”对于你测试过的那个编译器是正确的。 - Andreas
214
@Andreas:好问题。我曾经是一名编译器编写者,并有机会在许多CPU、操作系统和编译器上测试这段代码。唯一一个我发现没有优化 i++ 的编译器(事实上,正是这个编译器让我专业地注意到这一点)是由Walt Bilofsky编写的Software Toolworks C80编译器。那个编译器是为Intel 8080 CP/M系统设计的。可以肯定的是,任何没有包括这种优化的编译器都不适用于一般用途。 - Mark Harrison
32
尽管性能差异微不足道,并且在许多情况下已经被优化掉了 - 请注意,使用 ++i 而不是 i++ 仍然是一个好的实践。毫无理由不这么做,如果你的软件通过了一个不能将其优化的工具链,那么你的软件将更加高效。考虑到输入 ++ii++ 同样容易,没有理由一开始就不使用 ++i - monokrome
8
由于许多编程语言允许开发人员为前缀和后缀运算符提供自己的实现,因此编译器在做出决策之前可能需要比较这些函数的实现,这可能并不容易。 - pickypg
显示剩余14条评论

129

根据Andrew Koenig的文章 《效率与意图》

首先,至少在涉及整数变量时,++ii++更有效并不明显。

而且:

因此,人们应该问的问题不是这两个操作中哪个更快,而是这两个操作中哪个更准确地表达了您要完成的任务。我认为,如果您不使用表达式的值,则永远没有理由使用i++而不是++i,因为永远没有必要复制变量的值、增加变量的值,然后丢弃复制品。

因此,如果结果值不被使用,我会使用++i,但不是因为它更有效,而是因为它正确表达了我的意图。


5
不要忘记其他一元运算符也是前缀的。我认为++i是使用一元运算符的“语义”方式,而i++存在是为了适应特定需求(在加法之前进行评估)。 - TM.
12
如果结果值未被使用,则语义上没有区别:即没有理由偏好一种或另一种构建方式。 - Eamon Nerbonne
14
按照Koenig的建议,我将i++i += ni = i + n编码方式一样,即按照目标 动词 对象 的形式编码,其中目标操作数位于动词运算符的左侧。在i++的情况下,没有右边的对象,但规则仍然适用,将目标保持在动词运算符的左侧。 - David R Tribble
4
如果你想要递增 i,那么 i++ 和 ++i 都能正确表达你的意图。没有理由偏好其中任何一个。作为读者,我看不出任何区别,你的同事们在看到 i++ 时是否会感到困惑,认为这可能是个打字错误,他并没有想要递增 i? - dan carter
3
如果你想要“增加i”,读起来比“i增加”更自然。因此,即使忽略编译器优化,这也是优化可读性。 - Andreas
显示剩余5条评论

53
更好的答案是++i有时会更快,但永远不会更慢。每个人似乎都在假设i是普通的内置类型,比如int。在这种情况下,没有可测量的差异。然而,如果i是复杂类型,则可能会发现可测量的差异。对于i++,您必须在递增之前复制类。根据复制涉及的内容,它可能确实较慢,因为对于++i,您只需返回最终值。
Foo Foo::operator++()
{
  Foo oldFoo = *this; // copy existing value - could be slow
  // yadda yadda, do increment
  return oldFoo;
}

另一个区别是,使用++i时,您可以选择返回引用而不是值。同样,这取决于复制对象所涉及的内容,这可能会更慢。

一个实际的例子是迭代器的使用。复制迭代器不太可能成为应用程序的瓶颈,但养成在结果不受影响时使用++i而不是i++的习惯仍然是个好习惯。


52
题目明确指出使用C语言,没有提到C++。 - Dan Cristoloveanu
5
这个问题(虽然有些旧)是关于C语言的,而不是C++语言。但我认为值得一提的是,在C++中,即使一个类实现了后缀和前缀运算符,它们也不一定相关。例如,bar++可能会增加一个数据成员,而++bar可能会增加另一个数据成员,在这种情况下,你不能选择使用其中任何一种,因为语义是不同的。 - Kevin
虽然这是对C++程序员的好建议,但它根本没有回答标记为C的问题。在C中,使用前缀还是后缀完全没有区别。 - Lundin
1
@Pacerier 这个问题标记为 C 语言,只有 C 语言。你为什么会认为他们对其他编程语言不感兴趣呢?考虑到 SO 的运作方式,是否聪明的做法是假设他们只对 C 语言感兴趣,而非 Java、C#、COBOL 或其他不相关的编程语言? - Lundin

23

简短回答:

在速度方面,i++++i 没有任何区别。一个好的编译器不应该在这两种情况下生成不同的代码。

详细回答:

其他回答都没有提到的是,在表达式中找到++ii++之间的区别只有在表达式内才有意义。

for(i=0; i<n; i++)的情况下,i++独立于其自己的表达式中: 在i++之前和之后都有序列点。因此,唯一生成的机器码是“将 i 增加1”,并且定义了如何在程序的其余部分中对其进行排序。因此,如果您将其更改为前缀++,那么毫无影响,您仍然只会得到机器码“增加 i 1”。

仅在表达式array[i++] = x;array[++i] = x;等表达式中,++ii++之间的区别才有意义。有人可能会争论说,后缀在这些操作中会更慢,因为必须稍后重新加载 i 所在的寄存器。但是请注意,编译器可以随意按任何顺序排列您的指令,只要它不会“破坏抽象机器的行为”,正如C标准所称。

因此,尽管您可能认为array[i++] = x;将被转换为机器码:

  • i的值存储在寄存器A中。
  • 将数组地址存储在寄存器B中。
  • 将A和B相加,将结果存储在A中。
  • 在由A表示的新地址上,存储x的值。
  • i的值存储在寄存器A中 // 这是低效的,因为这里有额外的指令,我们已经执行过一次了。
  • 增加寄存器A的值。
  • 将寄存器A的值存储在i中。

编译器可能会更有效地生成代码,例如:

  • i的值存储在寄存器A中。
  • 将数组的地址存储在寄存器B中。
  • 将A和B相加,将结果存储在B中。
  • 增加寄存器A的值。
  • 将寄存器A的值存储在i中。
  • ... // 其余的代码。

只是因为你作为C程序员习惯于认为后缀++发生在末尾,机器码不必按照那种方式排序。

所以在C语言中,前缀和后缀++没有区别。现在,作为C程序员,你应该注意的是,有些人在某些情况下不一致地使用前缀,在其他情况下使用后缀,而没有任何合理的解释。这表明他们对C语言的工作原理不确定,或者他们对该语言的知识不正确。这总是一个坏迹象,它反过来表明他们在程序中做出了其他可疑的决策,基于迷信或“宗教教条”。

“前缀++总是更快”的说法确实是一种错误的教条,在许多几乎相同的情况下,前缀和后缀都具有相同的速度。


6
没有人说过“前缀++总是更快”。这是引用错误。他们所说的是“后缀++永远不会更快”。 - Pacerier
3
@Pacerier 我没有引用任何特定的人,只是一种普遍而不正确的信念。 - Lundin
3
@rbaleksandar 的代码片段 "@rbaleksandar c++ == c && ++c != c" 可以翻译为 "当且仅当c++等于c并且++c不等于c时,表达式为真。" - sadljkfhalskdjfh

20

借鉴Scott Meyers的More Effective c++第6条:区分递增和递减操作的前缀和后缀形式。

对于对象,特别是迭代器,始终优先使用前缀版本而非后缀版本。

原因在于运算符的调用模式。

// Prefix
Integer& Integer::operator++()
{
    *this += 1;
    return *this;
}

// Postfix
const Integer Integer::operator++(int)
{
    Integer oldValue = *this;
    ++(*this);
    return oldValue;
}

看这个例子,很容易看出前缀运算符总是比后缀更有效率,因为后缀需要使用临时对象。
这就是为什么当你看到使用迭代器的例子时,它们总是使用前缀版本。
但正如你指出的那样,对于int来说实际上没有区别,因为编译器可以进行优化。

5
我认为他的问题是针对C语言的,但对于C ++,你说得完全正确,而且C程序员也应该采用这种方法,因为他们也可以在C ++中使用。我经常看到C程序员过于频繁地使用后缀语法;-) - Anders Rune Jensen
虽然这是对C++程序员的好建议,但它根本没有回答标记为C的问题。在C中,使用前缀还是后缀完全没有区别。 - Lundin
1
在C语言中,前缀和后缀确实很重要,正如Andreas的回答所述。你不能假设编译器会进行优化,而且对于任何语言来说,始终首选++i而不是i++是一个好习惯。 - JProgrammer
@J程序员,根据您真诚的回答,它们并不重要 :) 如果您发现它们确实产生了不同的代码,那么要么是因为您未能启用优化,要么是因为编译器有问题。无论如何,您的回答与问题无关,因为问题是关于C语言的。 - Lundin

19

如果您担心微观优化,以下是一个额外的观察。在一些指令集结构(例如ARM)中,递减循环可能比递增循环更有效率:

这取决于以下因素:

for (i = 0; i < 100; i++)

在每次循环中,您将为以下各项中的每一项提供一条指令:

  1. i 增加 1
  2. 比较 i 是否小于 100
  3. 如果 i 小于 100,则进行条件分支。

而对于一个递减循环:

for (i = 100; i != 0; i--)

循环将有一个指令来执行以下操作:

  1. 递减i,设置CPU寄存器状态标志。
  2. 根据CPU寄存器状态(Z==0)进行条件分支。

当递减到零时才能正常工作!

来自ARM系统开发人员指南的回忆。


有一种来自代码优化书籍的古怪技巧,结合了零测试分支和增加地址的优点。以下是高级语言的示例(可能没什么用,因为许多编译器足够聪明,可以将其替换为效率更低但更常见的循环代码):int a[N]; for (i = -N; i; ++i) a[N + i] += 123; - noop
@noop 你能否详细说明一下你所指的代码优化书籍是哪些?我一直在寻找好的书。 - intrigued_66
8
这个回答根本不是问题的答案。 - monokrome
@noop:循环迭代中的任何效率提升都会因为需要执行a[N+i]而失去,而不是a[i] - Lawrence Dol
@SoftwareMonkey 如果N是常量,a[N+i]和a[i]在Intel架构上会生成一条指令。此外,如果N不是常量,只需将a[N + i]替换为a_end[i],其中int *a_end = a + N。由于a + N的值在循环体内部不会改变,因此优化器可能会为您执行此替换操作。 - noop
显示剩余2条评论

19
首先:在C语言中,i++++i之间的区别可以忽略不计。
细节如下。

1. 众所周知的C++问题:++i更快

在C++中,当i是某种具有重载递增运算符的对象时,++i更高效。

为什么?
++i中,对象首先进行递增,然后可以作为const引用传递给任何其他函数。如果表达式为foo(i++),则不可能这样做,因为现在需要在调用foo()之前执行递增,但旧值需要传递给foo()。因此,在执行原始值上的递增运算符之前,编译器被迫对i进行复制。附加的构造函数/析构函数调用是糟糕的部分。

如上所述,这不适用于基本类型。

2. 很少被人知道的事实:i++可能更快

如果不需要调用构造函数/析构函数(这在C中始终如一),++ii++应该同样快,对吗?不。它们在实质上几乎一样快,但可能存在轻微的差异,大多数其他答案都弄反了。

i++如何更快?
关键在于数据依赖性。如果需要从内存中加载值,则需要对其进行两个后续操作,即递增和使用。对于++i,必须在使用值之前进行递增。对于i++,使用并不依赖于递增,CPU可以将使用操作与递增操作“并行”执行。差异最多为一个CPU周期,因此它真的可以忽略不计,但是确实存在。而且这与许多人的想法相反。


1
关于您提到的第二点:如果++ii++在另一个表达式中使用,那么在它们之间进行更改会改变表达式的语义,因此任何可能的性能增益/损失都不成立。如果它们是独立的,即操作的结果不会立即使用,则任何合理的编译器都会将其编译为相同的内容,例如一个INC汇编指令。 - Shahbaz
2
@Shahbaz 这是完全正确的,但不是重点。1)尽管语义不同,但通过调整循环常量来使它们等价,几乎可以在几乎所有可能的情况下交替使用i++++i,因此它们在程序员所做的事情上是相近等价的。2)尽管两者都编译为相同的指令,但它们对CPU的执行方式有所不同。在i++的情况下,CPU可以并行计算增量和使用相同值的其他指令(CPU确实会这样做!),而在++i的情况下,CPU必须在增量之后安排其他指令的执行 - cmaster - reinstate monica
7
举例来说:if(++foo == 7) bar();if(foo++ == 6) bar(); 在功能上是等效的。然而,第二个可能会更快一周期,因为CPU可以并行计算比较和增量。虽然单个周期并不重要,但差异确实存在。 - cmaster - reinstate monica
1
不错的观点。常量经常出现(例如 <<=),而 ++ 通常被使用,因此它们之间的转换通常很容易实现。 - Shahbaz
1
我喜欢第二点,但仅当该值被使用时才适用,对吧?问题说“如果结果值未被使用?”,所以可能会令人困惑。 - jinawee
如果 ++ii++ 的值没有被使用,那么它们是完全等价的,并且应该编译成完全相同的代码。(除非 i 是一个具有非标准、不同的前/后增量运算符的类对象)。 - cmaster - reinstate monica

10
请不要让“哪个更快”这个问题成为决定使用哪一个的决定因素。很可能你永远不会在意那么多,此外,程序员的阅读时间比机器时间昂贵得多。
使用对人类阅读代码最有意义的方式。

4
我认为,将含糊的可读性改进放在实际效率提升和总体意图的清晰度之上是错误的。 - noop
5
我的术语“程序员阅读时间”大致相当于“意图的清晰度”。“实际效率收益”通常是无法衡量的,足够接近于零,可以将它们视为零。对于原帖提出的问题,在没有进行代码分析以发现++i是瓶颈之前,关于哪个更快的问题是浪费时间和程序员思考单位的。 - Andy Lester
2
++i和i++之间的可读性差异只是个人偏好的问题,但是尽管在优化编译器中涉及到简单情况和简单数据类型时结果是相等的,++i明显意味着更简单的操作。因此,在不需要使用后增量特性的情况下,++i对我来说是一个胜利者。 - noop
你说的和我说的一样。比起担心“效率”,展示意图和提高可读性更为重要。 - Andy Lester
3
我仍然不能同意。如果易读性在你的优先事项列表中排名更高,那么你选择的编程语言可能是错误的。C/C++ 的主要目的是编写高效的代码。 - noop

7
我已经阅读了这里的大部分答案和许多评论,但我没有看到任何提到一种情况:其中i++++i更有效(也许令人惊讶的是--ii--更有效)。这适用于DEC PDP-11的C编译器!
PDP-11具有寄存器的预减和后增汇编指令,但不能反之。这些指令允许使用任何“通用”寄存器作为堆栈指针。因此,如果你使用类似于*(i++)的语句,则可以将其编译为单个汇编指令,而*(++i)则不能。
显然,这只是一个非常奥妙的例子,但它确实提供了一个特例,即后增效率更高(或者应该说曾经更高,因为这些天很少有人需要PDP-11的C代码)。

@daShier。虽然我不同意,但这并不是那么晦涩难懂,或者至少不应该是。C语言是在70年代初由AT&T贝尔实验室与Unix一起开发的,当时的目标处理器是PDP-11。在这个时代的Unix源代码中,后自增运算符“i++”更为普遍,部分原因是因为开发人员知道当值被赋值“j = i++”或用作索引“a[i++] = n”时,代码会稍微快一些(而且更小)。看来他们养成了使用后自增的习惯,除非需要前自增。其他人通过阅读他们的代码也养成了这个习惯。 - jimhark
68000具有相同的功能,后增和前减在硬件上得到支持(作为寻址模式),但反过来则不行。摩托罗拉团队受DEC PDP-11的启发而开发了这个功能。 - jimhark
2
@jimhark,是的,我是那些从PDP-11过渡到68000并仍然使用--ii ++的程序员之一。 - daShier

7

@Mark 虽然编译器允许优化掉(基于堆栈的)临时变量的副本,而且gcc(在最近版本中)也确实这样做了, 但这并不意味着所有编译器都会始终这样做。

我刚刚测试了我们当前项目中使用的编译器,其中有3个没有进行优化。

永远不要假设编译器是正确的,特别是当可能更快但从未更慢的代码同样易读时。

如果你的代码中没有一个运算符的实现真的很愚蠢:

始终首选++i而不是i++。


1
只是好奇...为什么在一个项目中使用4种不同的C编译器?或者在一个团队或公司中,会有这样的情况吗? - Lawrence Dol
4
在为游戏机创建游戏时,每个平台都有自己的编译器/工具链。在理想的情况下,我们可以为所有目标使用gcc/clang/llvm,但在现实世界中,我们必须应对Microsoft、Intel、Metroworks、Sony等公司。 - Andreas

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