在C中,i ++和++ i之间是否存在性能差异?


Answers:


396

内容提要:不。

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

9
我知道这个问题与C有关,但我想知道浏览器是否可以针对javascript进行此优化。
TM。

69
因此,“ No”对于您使用过的一个编译器是正确的。
Andreas

173
@Andreas:好问题。我曾经是一名编译器作者,并且有机会在许多CPU,操作系统和编译器上测试此代码。我发现的唯一不能优化i ++情况的编译器(实际上是引起我专业注意的编译器)是Walt Bilofsky编写的Software Toolworks C80编译器。该编译器适用于Intel 8080 CP / M系统。可以肯定地说,任何不包括此优化的编译器都不是用于一般用途的编译器。
马克·哈里森

22
即使性能差异可以忽略不计,并且在许多情况下都可以进行优化-请注意,使用++i而不是仍然是一种好习惯i++。绝对没有理由不这样做,而且如果您的软件通过了无法对其进行优化的工具链,那么您的软件将更加高效。考虑到键入++i和键入一样容易i++,所以实际上没有任何理由不使用++i它。
monokrome'7

7
@monokrome由于开发人员可以使用多种语言为前缀和后缀运算符提供自己的实现,因此,如果不先比较那些功能,这对于编译器而言并不总是可以接受的解决方案,这可能是不平凡的。
pickypg 2012年

112

效率与意图(作者Andrew Koenig):

首先,至少在涉及整数变量的情况下,++i要比效率更高还远不是显而易见的i++

和:

因此,一个人应该问的问题不是这两个操作中的哪个更快,而是这两个操作中的哪个更准确地表达了您要完成的任务。我认为,如果您不使用表达式的值,那么就没有理由使用i++而不是++i,因为没有理由要复制变量的值,增加变量然后扔掉副本。

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


5
我们不要忘记其他一元运算符也是前缀。我认为++ i是使用一元运算符的“语义”方式,而i ++则是为了满足特定需求(加法前的评估)而设计的。
TM。

9
如果不使用结果值,则语义上没有区别:即,没有理由偏向于使用一种或另一种构造。
伊蒙·尼邦

3
作为读者,我看到了不同。因此,作为一名作家,我会选择一个而不是另一个来表明我的意图。这只是一种习惯我有,尝试通过代码:)我的程序员朋友和队友进行沟通
塞巴斯蒂安RoccaSerra

11
继柯尼希的意见,我的代码i++以同样的方式我最好的代码i += n或者i = i + n,即在形式目标 动词 对象,与目标操作数左边的动词运营商。在情况下i++,没有右对象,但是规则仍然适用,将目标保持在动词运算符的左侧。
David R Tribble 2014年

4
如果您要增加i,则i ++和++ i都正确表达了您的意图。没有理由比另一个更偏爱一个。作为读者,我看不出有什么不同,当您的同事看到i ++并思考时,会不会感到困惑,也许这是一个错字,他不是故意增加i?
丹·卡特2014年

46

更好的答案是++i有时会更快,但永远不会慢。

每个人似乎都假设这i是一个常规的内置类型,例如int。在这种情况下,没有可测量的差异。

但是,如果i是复杂类型,则可能会发现可测量的差异。因为i++您必须在增加课程之前制作课程的副本。实际上,根据副本所涉及的内容,它可能会变慢,因为++it您只需返回最终值即可。

Foo Foo::operator++()
{
  Foo oldFoo = *this; // copy existing value - could be slow
  // yadda yadda, do increment
  return oldFoo;
}

另一个区别是++i您可以选择返回引用而不是值。同样,根据制作对象副本所涉及的内容,这可能会更慢。

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


36
该问题明确指出C,没有引用C ++。
Dan Cristoloveanu

5
这个(公认的古老问题)是关于C的,而不是C ++的,但是我认为值得一提的是,在C ++中,即使一个类实现了后缀和前缀运算符,它们也不一定是相关的。例如,bar ++可以增加一个数据成员,而++ bar可以增加另一个数据成员,在这种情况下,由于语义不同,您将不能选择使用其中任何一个。
凯文(Kevin)

-1尽管这对于C ++程序员是一个很好的建议,但丝毫没有回答这个被标记为C的问题。在C语言中,使用前缀还是后缀绝对没有区别。
伦丁

@Pacerier该问题被标记为C,并且仅标记为C。您为什么认为他们对此不感兴趣?鉴于SO的工作原理,假设他们对C感兴趣,而不对Java,C#,COBOL或任何其他非主题语言感兴趣,不是很明智吗?
隆丁

18

简短答案:

从未有任何区别i++,并++i在速度方面。好的编译器在两种情况下不应生成不同的代码。

长答案:

每隔什么其他答案没有提到的是,之间的区别++ii++只能使发现的表达中的意义。

在的情况下for(i=0; i<n; i++)i++本身在其自己的表达式中:在之前有一个序列点,在其i++之后有一个点。因此,唯一生成的机器代码是“ increment iby 1”,并且很好地定义了如何相对于程序的其余部分对其进行排序。因此,即使将其更改为prefix ++,也没有关系,您仍然只会获得机器代码“ increment iby 1”。

++i和之间的差异i++仅在诸如array[i++] = x;vs之类的表达式中起作用array[++i] = x;。有人可能会说,后缀在这种操作中会变慢,因为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的工作方式或对语言的了解不正确。这总是一个不好的信号,它确实表明他们正在基于迷信或“宗教教条”在程序中做出其他可疑的决定。

++实际上,“前缀总是更快”确实是这样一种错误的教条,在准C程序员中很常见。


3
没有人说“ Prefix ++总是更快”。那是错误的报价。他们说的是“ Postfix ++ 永远不会更快”。
Pacerier

2
@Pacerier我不是在引述任何特定的人,而只是引用广泛,错误的信念。
隆丁

@rbaleksandar C ++!= C.
Lundin

@rbaleksandar问与答都谈论C ++。这与C不同,因为它具有运算符重载和复制构造函数等
。– Lundin

3
@rbaleksandar c ++ == c && ++ c!= c
sadljkfhalskdjfh 2015年

17

摘自Scott Meyers的《更有效的c ++ 项目6》:区分增量和减量运算的前缀和后缀形式

在对象方面,尤其是在迭代器方面,始终优先使用前缀版本而不是后缀。

如果查看运营商的呼叫方式,则其原因是。

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

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

看这个例子,很容易看出前缀运算符将总是比后缀更有效率。由于在使用postfix时需要一个临时对象。

这就是为什么当您看到使用迭代器的示例时,它们始终使用前缀版本的原因。

但是正如您为int指出的那样,由于可以进行编译器优化,因此实际上没有区别。


4
我认为他的问题是针对C的,但是对于C ++来说,您是绝对正确的,此外C人士应该采用此方法,因为他们也可以将其用于C ++。我经常看到C程序员使用后缀语法;-)
Anders Rune Jensen

-1尽管这对于C ++程序员是一个很好的建议,但丝毫没有回答这个被标记为C的问题。在C语言中,使用前缀还是后缀绝对没有区别。
伦丁

1
根据Andreas的回答,@ Lundin前缀和postifx在C中确实很重要。你不能假设,编译器将优化掉,这是任何语言只是好的做法Alwas喜欢++我在我++
JProgrammer

@JProgrammer真诚地按照您的回答它们无关紧要:)如果您发现它们确实产生了不同的代码,那是因为您未能启用优化,或者是因为编译器不正确。无论如何,你的答案是题外话的问题是关于C.
伦丁

16

如果您担心微优化,这是另外一个观察结果。给定以下条件,递增循环“可能”比递增循环更有效(取决于指令集架构,例如ARM):

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

在每个循环中,您将分别具有一条指令:

  1. 添加1到中i
  2. 比较是否i小于100
  3. 条件分支,如果i小于100

而递减循环:

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

该循环将为每个指令提供一条指令:

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

当然,这仅在减为零时有效!

从《 ARM系统开发人员指南》中记住。


好一个 但这不会减少缓存命中吗?
AndreasT 2009年

代码优化书中有一个古老的怪异技巧,将零测试分支的优势与增加的地址相结合。这是高级语言的示例(可能无用,因为许多编译器足够聪明,可以用效率较低但更常见的循环代码替换它):int a [N]; for(i = -N; i; ++ i)a [N + i] + = 123;
noop 2010年

@noop您能否详细说明所参考的代码优化书?我一直在努力寻找好的。
变形2012年

7
这个答案根本不是问题的答案。
monokrome'7

@mezamorphic对于x86,我的资料是:英特尔优化手册,Agner Fog,Paul Hsieh,Michael Abrash。
2012年

11

请不要让“哪个更快”的问题成为使用哪个的决定因素。您可能永远不需要那么在意,此外,程序员的阅读时间比机器学习的时间要昂贵得多。

使用对人类阅读代码最有意义的方法。


3
我认为,对模糊的可读性进行改进而不是对实际效率的提高和意图的总体清晰度的理解是错误的。
noop 2010年

4
我的术语“程序员阅读时间”大致类似于“意图明确性”。“实际效率提升”通常是无法衡量的,接近于零就可以称为零。在OP的情况下,除非对代码进行了分析以发现++ i是瓶颈,否则哪个问题更快的问题是浪费时间和程序员的思想。
安迪·莱斯特

2
++ i和i ++之间的可读性差异仅是个人喜好问题,但++ i显然意味着比i ++更简单的操作,尽管在进行编译器优化时,结果等同于琐碎的情况和简单的数据类型。因此,当不需要后增量的特定属性时,++ i是我的赢家。
noop 2010年

你在说我的意思。显示意图并提高可读性比担心“效率”更为重要。
安迪·莱斯特

2
我还是不同意。如果在优先级列表中可读性更高,那么您对编程语言的选择可能是错误的。C / C ++的主要目的是编写高效的代码。
noop

11

首先:i++和之间的区别++i在C中可以忽略不计。


到细节。

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

在C ++中,++i如果i某种对象带有重载的增量运算符,则效率更高。

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

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

2.鲜为人知的事实:i++ 可能更快

如果不需要调用构造函数/析构函数,这在C语言中总是如此,++i并且i++应该同样快,对吗?不。它们的速度几乎一样快,但可能会有细微的差异,大多数其他应答者都绕错了方向。

怎么i++会更快?
关键是数据依赖性。如果需要从内存中加载该值,则需要对其进行两个后续操作,使其递增并使用它。使用时++i,需要先进行递增,然后才能使用该值。使用i++,使用不取决于增量,并且CPU可以与增量操作并行执行使用操作。区别最多是一个CPU周期,因此它确实可以忽略不计,但是确实存在。这是许多人期望的另一种方式。


关于您的观点2:如果在另一个表达式中使用++i或,i++则在它们之间进行更改会更改表达式的语义,因此毫无疑问会带来任何性能提升/损失。如果它们是独立的,即该操作的结果没有立即使用,则任何合适的编译器都可以将其编译为相同的东西,例如INC汇编指令。
Shahbaz

1
@Shahbaz完全正确,但不是重点。1)虽然语义是不同的,两者i++++i可以互换在几乎每一个可能的情况下,通过一个调整环常量使用,因此他们在他们的程序员做什么接近相等。2)即使两者都编译为同一条指令,但对于CPU它们的执行也有所不同。在的情况下i++,CPU可以其他使用相同值的指令并行计算增量(CPU确实会执行此操作!),而++iCPU必须在增量之后安排其他指令。
cmaster-恢复莫妮卡2015年

4
@Shahbaz例如:if(++foo == 7) bar();if(foo++ == 6) bar();在功能上等效。但是,第二个可能快一个周期,因为比较和增量可以由CPU并行计算。并不是说这单个周期有多大关系,而是有区别。
cmaster-恢复莫妮卡2015年

1
好点子。常量经常出现在很多地方(<例如vs <=++,因此,通常之间很容易实现转换。
Shahbaz 2015年

1
我喜欢第2点,但这仅在使用该值时适用,对吗?这个问题说“是否不使用结果值?”,因此可能会造成混淆。
jinawee

7

@Mark尽管允许编译器优化变量的(基于堆栈的)临时副本,而gcc(在最新版本中)正在这样做,但这并不意味着所有编译器都会一直这样做。

我只是用我们当前项目中使用的编译器对其进行了测试,而四分之三的代码并未对其进行优化。

永远不要假设编译器正确无误,尤其是在可能更快但永远不会慢的代码易于阅读的情况下。

如果您的代码中没有真正愚蠢的操作符实现:

比起i ++,Alwas更喜欢++ i。


只是好奇...为什么在一个项目中使用4个不同的C编译器?还是在一个团队或一个公司中?
劳伦斯·多尔

3
在为游戏机创建游戏时,每个平台都会带来自己的编译器/工具链。在理想环境中,我们可以对所有目标使用gcc / clang / llvm,但在这个环境中,我们不得不忍受Microsoft,Intel,Metroworks,Sony等。–
Andreas

5

在C语言中,如果未使用结果,则编译器通常可以将它们优化为相同。

但是,在C ++中,如果使用其他提供自己的++运算符的类型,则前缀版本可能比后缀版本快。因此,如果不需要后缀语义,最好使用前缀运算符。


4

我可以想到后缀比前缀增量慢的情况:

想象一下,带有寄存器的处理器A被用作累加器,并且它是许多指令中使用的唯一寄存器(某些小型微控制器实际上就是这样)。

现在,假设以下程序及其转换为假设的程序集:

前缀增量:

a = ++b + c;

; increment b
LD    A, [&b]
INC   A
ST    A, [&b]

; add with c
ADD   A, [&c]

; store in a
ST    A, [&a]

后缀增量:

a = b++ + c;

; load b
LD    A, [&b]

; add with c
ADD   A, [&c]

; store in a
ST    A, [&a]

; increment b
LD    A, [&b]
INC   A
ST    A, [&b]

请注意如何b强制重新加载的值。使用前缀递增,编译器可以仅递增该值并继续使用它,可能避免重新加载它,因为所需的值在递增之后已经在寄存器中。但是,使用后缀增量,编译器必须处理两个值,一个是旧值,另一个是增量值,正如我在上面显示的那样,这将导致更多的内存访问。

当然,如果不使用增量值(例如单个i++;语句),则编译器可以(并且确实)简单地生成增量指令,而不考虑后缀或前缀的用法。


附带说明一下,我想提到一个表达式,其中存在一个a的表达式b++不能简单地转换为一个表达式++b而无需付出额外的努力(例如,通过添加- 1)。因此,如果将两者作为某个表达式的一部分,则将它们进行比较是不正确的。通常,在b++表达式内部使用的地方就不能使用++b,因此即使++b效率更高,也完全是错误的。如果表达式请求它,当然是例外(例如a = b++ + 1;,可以将其更改为a = ++b;)。


4

我一直在读通过这里的大部分答案,许多的意见,我没有看到任何参考一个例子,我能想到的地方i++是不是更有效++i(也许令人惊讶的--i 不是更有效i--)。那是针对DEC PDP-11的C编译器!

PDP-11具有汇编指令,用于寄存器的递减和递增,但不能相反。该指令允许将任何“通用”寄存器用作堆栈指针。因此,如果您使用类似的东西*(i++),可以将其编译为单个汇编指令,而*(++i)不能这样做。

这显然是一个非常深奥的例子,但确实提供了例外,即后增量效率更高(或者,我应该说,因为这几天对PDP-11 C代码的需求不大)。


2
很深奥,但很有趣!
马克·哈里森

@daShier。+1,尽管我不同意,但这并不深奥,或者至少不应该如此。在70年代初,PDP-11是目标处理器时,C就与Unix在AT&T贝尔实验室与Unix共同开发。在这个时代以来的Unix源代码中,“ i ++”后递增更为普遍,部分原因是开发人员知道何时将值赋值“ j = i ++”或用作索引“ a [i ++] = n”,代码会稍微快一些(并且更小)。除非需要pre,否则他们似乎养成了使用post增量的习惯。其他人则通过阅读代码来学习,并养成了这种习惯。
jimhark

68000具有相同的功能,在硬件中(作为寻址模式)支持后递增和前递减,但反之则不行。摩托罗拉团队受到DEC PDP-11的启发。
jimhark

2
@jimhark,是的,我是那些过渡到68000并仍使用--i和的PDP-11程序员之一i++
daShier

2

我总是喜欢预增量,但是...

我想指出的是,即使在调用operator ++函数的情况下,如果函数被内联,编译器也将能够优化掉临时函数。由于operator ++通常很短,并且经常在标头中实现,因此很可能会内联。

因此,出于实际目的,这两种形式的性能可能没有太大区别。但是,我总是更喜欢预增量,因为直接表达我想说的话似乎要好一些,而不要依靠优化器来解决。

同样,减少优化器的工作量可能意味着编译器运行得更快。


你发帖是C ++ -具体而问题是关于C.在任何情况下,你的答案是错误的:定制后的增量运营商的编译器一般能够产生高效的代码。
康拉德·鲁道夫

他指出“函数是否内联”,这使他的推理正确。
Blaisorblade

由于C不提供运算符重载,因此前置和后置问题在很大程度上没有意义。编译器可以使用应用于任何其他未使用的原始计算值的相同逻辑来优化未使用的温度。请参阅所选答案以获取样本。

0

我的C有点生锈,所以我提前道歉。从速度上看,我可以理解结果。但是,我对这两个文件如何出现在相同的MD5哈希中感到困惑。也许for循环运行相同,但是以下两行代码不会生成不同的汇编吗?

myArray[i++] = "hello";

myArray[++i] = "hello";

第一个将值写入数组,然后递增i。然后,第二个增量i写入数组。我不是汇编专家,但我只是看不到这两行不同的代码将如何生成相同的可执行文件。

只是我的两分钱。


1
@Jason Z编译器优化是在完成程序集生成之前进行的,它会看到i变量未在同一行的其他任何地方使用,因此保留其值将是一种浪费,很可能有效地将其翻转为i ++。但这只是一个猜测。我等不及我的一位讲师试图说它更快了,而我成为了用实际证据纠正理论的人。我几乎可以感觉到仇恨了^ _ ^
Tarks

3
“我的C语言有点生疏,所以我先向您道歉。” C语言没什么问题,但是您没有完整阅读原始问题:“ 如果不使用结果值,i ++和++ i之间是否存在性能差异 ”请注意斜体。在您的例子中,我++ / ++我的“结果” 使用,但在惯用的for循环中,前/后置运营商的“结果”不使用这样的编译器可以做它喜欢。
罗迪'09

2
在您的示例中,代码将有所不同,因为您使用的是值。它们相同的示例仅使用++进行增量,而不使用任何一个返回的值。
约翰·加德纳

1
修复:“编译器会看到未使用i ++的返回值,因此会将其翻转到++ i”。您写的内容也是错误的,因为您无法将i和i ++之一放在同一行(语句)上,因此结果不确定。
Blaisorblade

1
更改foo[i++]foo[++i]没有其他任何更改显然会改变程序的语义,但是在某些处理器上使用编译器没有环冲顶优化逻辑时,递增pq一次,然后运行一个循环,其进行例如*(p++)=*(q++);会比使用一个循环执行得更快*(++pp)=*(++q);。对于某些处理器上非常紧密的循环,速度差异可能会很大(超过10%),但这可能是C语言中唯一的情况,后增量比预增量快得多。
超级猫2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.