Mar*_*son 378

执行摘要:不.

i++++i因为旧的值i 可能需要保存以供以后使用,所以可能比它慢,但实际上所有现代编译器都会优化它.

我们可以通过查看此函数的代码来证明这一点,包括++ii++.

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

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

}
Run Code Online (Sandbox Code Playgroud)

文件是相同的,除了++ii++:

$ diff i++.c ++i.c
6c6
<     for (i = 0; i < 100; i++)
---
>     for (i = 0; i < 100; ++i)
Run Code Online (Sandbox Code Playgroud)

我们将编译它们,并获得生成的汇编程序:

$ gcc -c i++.c ++i.c
$ gcc -S i++.c ++i.c
Run Code Online (Sandbox Code Playgroud)

我们可以看到生成的对象和汇编程序文件都是相同的.

$ md5 i++.s ++i.s
MD5 (i++.s) = 90f620dda862cd0205cd5db1f2c8c06e
MD5 (++i.s) = 90f620dda862cd0205cd5db1f2c8c06e

$ md5 *.o
MD5 (++i.o) = dd3ef1408d3a9e4287facccec53f7d22
MD5 (i++.o) = dd3ef1408d3a9e4287facccec53f7d22
Run Code Online (Sandbox Code Playgroud)

  • @Andreas:好问题.我曾经是一名编译器编写者,并有机会在许多CPU,操作系统和编译器上测试这段代码.我发现没有优化i ++案例的唯一编译器(事实上,专业地引起我注意的编译器)是Walt Bilofsky编写的Software Toolworks C80编译器.该编译器适用于Intel 8080 CP/M系统.可以肯定地说,任何不包含此优化的编译器都不适用于一般用途. (163认同)
  • 因此,对于您测试过的一个编译器,"否"是正确的. (65认同)
  • 即使性能差异可以忽略不计,并且在许多情况下进行了优化 - 请注意,使用`++ i`代替`i ++`仍然是一个好习惯.绝对没有理由不这样做,如果您的软件通过不优化它的工具链,那么您的软件将更有效率.考虑到键入`++ i`就像键入`i ++`一样容易,没有理由不首先使用`++ i`. (21认同)
  • 我知道这个问题是关于C的,但我有兴趣知道浏览器是否可以对javascript进行此优化. (9认同)
  • @monokrome由于开发人员可以在许多语言中为前缀和后缀运算符提供自己的实现,对于编译器而言,如果不首先比较这些函数(这可能是非平凡的),这可能并不总是可接受的解决方案. (7认同)
  • @pickypg请注意这里的问题.这个问题是关于C的,所以你的观点在这里讨论的背景下无效.关于C的一个好处是它不支持运算符重载. (5认同)
  • 即使编译器没有优化,你也不太可能注意到任何现代CPU的差异. (4认同)
  • @Andreas:大多数编译器都会对此进行优化.当你没有在任何地方使用它的返回值时,编译器使用i ++是没有意义的.但是,同样的逻辑也应该一直传播到程序员.依靠优化本身就是一个问题. (3认同)
  • 另外,'no`仅针对整数进行测试,但问题并未提及`i`的类型.然后你在第1行的概括.对不起,-1. (2认同)
  • 当然我相信对于任何非蹩脚的C编译器,答案是否定的.但是由于许多编程C编程的程序员也进行C++编程,所以默认使用++ i是首选.只是一个好习惯,即使它在C中毫无意义. (2认同)

Séb*_*rra 107

从安德鲁科尼希的效率与意图:

首先,至少在涉及整数变量的情况下,++i效率远非显而易见i++.

而且:

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

所以,如果没有使用结果值,我会使用++i.但不是因为它更有效:因为它正确地表明了我的意图.

  • 按照Koenig的建议,我编码`i ++'的方式与编码`i + = n`或`i = i + n`的方式相同,即以*target**verb**object*的形式编码,使用*target*操作数位于*verb*运算符的左侧.在"i ++"的情况下,没有正确的*object*,但规则仍然适用,将*target*保持在*verb*运算符的左侧. (10认同)
  • 如果未使用结果值,则语义中没有*差异*:即,没有依据优先选择一个或另一个构造. (8认同)
  • 我们不要忘记其他一元运算符也是前缀.我认为++ i是使用一元运算符的"语义"方式,而i ++是为了满足特定需求(添加前的评估). (4认同)
  • 如果你想增加i,那么i ++和++我都正确地表达你的意图.没有理由偏爱一个而不是另一个.作为一名读者,我认为没有区别,当他们看到i ++时,你的同事是否会感到困惑并思考,也许这是一个错字,他并不意味着增加我? (4认同)
  • 作为读者,我看到了一个区别.所以作为一个作家,我会通过选择一个而不是另一个来表明我的意图.这只是我的习惯,尝试通过代码与我的程序员朋友和队友沟通:) (3认同)
  • 为什么`i ++`会创建一个副本,而不是简单地推迟增量,直到引用变量的表达式完成之后?旧的编译器不够智能吗? (3认同)
  • @Ifalin,我们正在谈论独立的。正确的想法是,没有人在声明中使用“ i ++” /“ ++ i”。 (2认同)
  • 如果您尝试“递增i”,则阅读“递增i”比阅读“ i递增”自然得多。因此,即使忽略编译器优化。它优化了可读性。 (2认同)

And*_*ant 45

一个更好的答案是++i有时会更快但从不慢.

每个人似乎都认为这i是一个常规的内置类型,如int.在这种情况下,将没有可衡量的差异.

但是,如果i是复杂类型,那么您可能会发现可测量的差异.因为i++你必须在增加它之前复制你的课程.根据副本中涉及的内容,它确实可能会变慢,因为++it您可以返回最终值.

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

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

可能发生这种情况的一个真实示例是迭代器的使用.复制迭代器不太可能成为你应用程序的瓶颈,但是习惯使用++i而不是i++不影响结果的习惯仍然是一种好习惯.

  • 问题明确指出C,没有引用C++. (33认同)
  • 这个(不可否认的旧)问题是关于C,而不是C++,但我认为值得一提的是,在C++中,即使一个类实现了后置和预置操作符,它们也不一定是相关的.例如,bar ++可能会增加一个数据成员,而++ bar可能会增加不同的数据成员,在这种情况下,您将无法使用任何一个,因为语义不同. (5认同)

JPr*_*mer 17

摘自Scott Meyers,更有效的c ++ 项目6:区分增量和减量操作的前缀和后缀形式.

对于对象,前缀版本总是比postfix更受欢迎,特别是在迭代器方面.

如果你看一下运营商的呼叫模式,原因就在于此.

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

// Postfix
const Integer Integer::operator++(int)
{
    Integer oldValue = *this;
    ++(*this);
    return oldValue;
}
Run Code Online (Sandbox Code Playgroud)

看一下这个例子,很容易看出前缀运算符总是比后缀更高效.因为在使用postfix时需要一个临时对象.

这就是为什么当您看到使用迭代器的示例时,它们总是使用前缀版本.

但正如你指出的那样,由于可能发生的编译器优化,实际上没有区别.

  • 我认为他的问题是针对C的,但是对于C++来说,你是绝对正确的,而且C人们应该采用它,因为他们也可以将它用于C++.我经常看到C程序员使用postfix语法;-) (4认同)

ton*_*ylo 16

如果你担心微观优化,这是另外一个观察.递减循环"可能"比递增循环更有效(取决于指令集架构,例如ARM),给定:

for (i = 0; i < 100; i++)
Run Code Online (Sandbox Code Playgroud)

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

  1. 添加1i.
  2. 比较是否i小于a 100.
  3. 条件分支if i小于a 100.

而递减循环:

for (i = 100; i != 0; i--)
Run Code Online (Sandbox Code Playgroud)

循环将为每个:

  1. 递减i,设置CPU寄存器状态标志.
  2. 条件分支取决于CPU寄存器状态(Z==0).

当然这只有在递减到零时才有效!

记得ARM系统开发人员指南.

  • 这个答案根本不是问题的答案. (7认同)

Lun*_*din 16

简短回答:

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

答案很长:

每个其他答案都没有提到的是,++i对比之间的区别i++仅在它所找到的表达式中有意义.

在这种情况下for(i=0; i<n; i++),i++单独使用它自己的表达式:在它之前有一个序列点,i++并且在它之后有一个序列点.如此产生的唯一的机器代码是"增加i1",它是良好定义这是如何相对于测序,以该程序的其余部分.所以,如果你要改变它的前缀++,它不会有丝毫的关系,你仍然只得到本机代码"增加i1".

之间的差异++ii++唯一的事项表述如array[i++] = x;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程序员中常见的错误教条.

  • 没有人说"Prefix ++总是更快".这是错误引用的.他们所说的是"Postfix ++**永远不会**更快". (3认同)
  • @rbaleksandar c ++ == c && ++ c!= c (3认同)
  • @Pacerier 我不是在引用任何特定的人,而只是一个广泛的、不正确的信念。 (2认同)

And*_*ter 11

请不要让"哪一个更快"的问题成为决定使用哪个因素.你可能永远不会那么在意,而且程序员的阅读时间比机器时间要贵得多.

使用对读取代码最有意义的人.

  • 我的术语"程序员阅读时间"大致类似于"意图清晰度"."实际效率增益"通常是不可估量的,接近于零以将其称为零.在OP的情况下,除非代码被分析以发现++ i是一个瓶颈,否则哪个更快的问题是浪费时间和程序员思想单位. (4认同)
  • 我认为将模糊的可读性改进与实际效率提升和意图的整体清晰度相比是错误的. (3认同)
  • ++ i和i ++之间的可读性差异只是个人偏好的问题,但++我明显意味着比i ++更简单的操作,尽管在涉及优化编译器时,结果等同于简单的案例和简单的数据类型.因此,当我没有必要使用后增量的特定属性时,++ i是我的赢家. (2认同)
  • 我还是不能同意.如果您的优先级列表中的可读性更高,则可能您选择的编程语言是错误的.C/C++的主要目的是编写高效的代码. (2认同)

cma*_*ter 10

首先:之间的区别i++,并++i在C. neglegible


细节.

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

在C++中,++iiff i是一种具有重载增量运算符的某种对象,效率更高.

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

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

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

如果不需要调用构造函数/析构函数,这在C中总是如此,++i并且i++应该同样快,对吧?不.它们几乎同样快速,但可能存在细微差别,大多数其他答案者都错误地解决了这个问题.

怎么可能i++更快?
关键是数据依赖性.如果需要从内存加载该值,则需要对其进行两次后续操作,递增并使用它.使用时++i,需要可以使用值之前完成增量.因此i++,使用不依赖于增量,并且CPU可以与增量操作并行地执行使用操作.差异最多只有一个CPU周期,所以它确实是可以忽略不计的,但它确实存在.而这是许多人所期望的另一种方式.

  • @Shahbaz例如:`if(++ foo == 7)bar();`和`if(foo ++ == 6)bar();`在功能上是等价的.然而,第二个可以更快一个周期,因为比较和增量可以由CPU并行计算.并不是说这个单周期很重要,但差别就在那里. (3认同)
  • @Shahbaz 这是完全正确的,但不是重点。1) 尽管语义不同,但通过将循环常量调整一,“i++”和“++i”几乎可以在所有可能的情况下互换使用,因此它们对程序员的作用几乎相同。2) 尽管两者都编译为相同的指令,但它们的执行对于 CPU 来说是不同的。在“i++”的情况下,CPU 可以*并行*计算增量到使用相同值的其他指令(CPU 确实这样做!),而使用“++i”时,CPU 必须调度其他指令*之后*增量。 (2认同)

And*_*eas 7

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

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

永远不要假设编译器正确,特别是如果可能更快但从不慢的代码易于阅读.

如果您的代码中没有其中一个运算符的非常愚蠢的实现:

Alwas比i ++更喜欢++ i.

  • 在为游戏机创建游戏时,每个平台都会带来自己的编译器/工具链.在一个完美的世界中,我们可以将gcc/clang/llvm用于所有目标,但在这个世界上,我们必须忍受微软,英特尔,Metroworks,索尼等. (3认同)

Kri*_*son 5

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

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


daS*_*ier 5

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

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

这显然是一个非常深奥的例子,但它确实提供了一个例外,其中后增量更有效(或者我应该说,因为现在对 PDP-11 C 代码的需求并不多)。

  • 非常深奥,但是非常有趣! (2认同)
  • @jimhark,是的,我是那些转换到 68000 并仍然使用 `--i` 和 `i++` 的 PDP-11 程序员之一。 (2认同)