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

如果没有使用结果值,i++++i之间是否有性能差异?

128299 次浏览

执行摘要:没有。

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

我的C有点生疏了,所以我提前道歉。就速度而言,我可以理解结果。但是,我对这两个文件是如何得到相同的MD5哈希感到困惑。也许for循环也可以运行,但是下面两行代码不会生成不同的程序集吗?

myArray[i++] = "hello";

vs

myArray[++i] = "hello";

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

这只是我的个人意见。

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

然而,在c++中,如果使用提供自己的++操作符的其他类型,前缀版本可能比后缀版本更快。因此,如果不需要后缀语义,最好使用前缀操作符。

借鉴Scott Meyers, 更有效的c++ 项目6:区分递增和递减操作的前缀和后缀形式

对于对象,尤其是对于迭代器,前缀版本总是优于后缀版本。

原因是,如果你看一下操作符的调用模式。

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


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

看看这个例子,很容易看出前缀操作符总是比后缀操作符更有效率。因为需要在临时对象中使用后缀。

这就是为什么当你看到使用迭代器的例子时,他们总是使用前缀版本。

但正如你所指出的,对于int型,实际上没有什么区别,因为编译器优化可以发生。

更好的答案是++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++的习惯仍然是一个很好的实践,这样结果不会受到影响。

如果你担心微观优化,这里有一个额外的观察。递减循环“可能”比递增循环更有效(取决于指令集架构,例如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系统开发人员指南。

来自Andrew Koenig的效率与意图:

首先,++i是否比i++更有效还远远不明显,至少在涉及整型变量时是这样。

和:

所以人们应该问的问题不是这两种操作中哪一种更快,而是这两种操作中哪一种更准确地表达了你想要完成的事情。我认为,如果你不使用表达式的值,永远没有理由使用i++而不是++i,因为永远没有理由复制变量的值,增加变量,然后扔掉复制。

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

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

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

我总是喜欢预增量,然而……

我想指出的是,即使在调用运算符++函数的情况下,如果函数得到内联,编译器将能够优化掉临时函数。由于操作符++通常很短,并且经常在头文件中实现,因此它很可能被内联。

因此,出于实际目的,这两种形式的性能之间可能没有太大差异。然而,我总是喜欢预增量,因为它似乎更好地直接表达我想说的,而不是依赖于优化器来解决它。

此外,给优化器更少的任务可能意味着编译器运行得更快。

< p > @Mark 即使编译器允许优化(基于堆栈的)变量的临时副本,并且gcc(在最近的版本中)正在这样做, 并不意味着所有编译器总是这样做

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

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

如果你的代码中没有一个操作符的愚蠢实现:

我喜欢++i胜过i++。

我可以想到一种情况,后缀比前缀增量慢:

想象一个处理器使用寄存器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++;语句,编译器可以(并且确实)简单地生成一个增量指令,而不管后缀或前缀的使用。


作为旁注,我想提一下,一个有b++的表达式不能简单地转换为有++b的表达式,而不需要任何额外的努力(例如通过添加- 1)。因此,如果它们是某个表达式的一部分,那么比较两者是不正确的。通常,当你在表达式中使用b++时,你不能使用++b,所以即使++b可能更有效,它也会是错误的。当然,如果表达式要求异常(例如a = b++ + 1;可以更改为a = ++b;)。

首先:在C语言中,i++++i之间的差异是可以忽略的。


到细节。

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

在c++中,如果i是某种带有重载增量操作符的对象,则++i更有效。

< p >为什么?
++i中,对象首先被递增,随后可以作为const引用传递给任何其他函数。如果表达式是foo(i++),这是不可能的,因为现在需要在调用foo()之前完成增量,但旧的值需要传递给foo()。因此,编译器被迫在对原i执行自增操作符之前创建一个i的副本。额外的构造函数/析构函数调用是不好的部分

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

2. 鲜为人知的事实:i++ 五月更快

如果不需要调用构造函数/析构函数,这在C中总是如此,++ii++应该同样快,对吗?不。他们的速度几乎一样快,但可能有一些小的差异,这是大多数其他答案的错误理解。

i++如何更快?
关键在于数据依赖关系。如果需要从内存中加载该值,则需要对其进行两个后续操作:递增它和使用它。对于++i,增量需要完成之前的值可以使用。对于i++,使用不依赖于增量,CPU可以对增量操作执行使用操作并行。差异最多是一个CPU周期,所以实际上可以忽略不计,但它确实存在。这与许多人所期望的正好相反

简短的回答:

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

长一点的回答:

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

for(i=0; i<n; i++)的情况下,i++在它自己的表达式中是单独的:在i++之前有一个序列点,在它之后有一个。因此唯一生成的机器代码是“用__abc4增加i”;它很好地定义了这是如何与程序的其他部分进行排序的。因此,如果你将它更改为前缀++,这一点也不重要,你仍然会得到机器代码“increase i by __abc4”。

++ii++之间的差异只在诸如array[i++] = x;array[++i] = x;这样的表达式中起作用。有些人可能会争辩说,后缀在这样的操作中会更慢,因为i所在的寄存器稍后必须重新加载。但请注意,编译器可以自由地以任何它喜欢的方式对你的指令进行排序,只要它不“破坏抽象机器的行为”。这是C标准的说法。

因此,虽然你可以假设array[i++] = x;被转换为机器代码为:

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

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

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

只是因为你作为一个C程序员被训练成认为后缀++发生在结尾,机器代码不需要以这种方式排序。

因此,在C语言中前缀和后缀++之间没有区别。现在,作为C程序员,你应该看到的是,有些人在某些情况下不一致地使用前缀,而在其他情况下不一致地使用后缀,没有任何理由。这表明他们不确定C语言是如何工作的,或者他们对这门语言的了解不正确。这总是一个不好的迹象,它反过来表明他们在他们的计划中做出了其他有问题的决定,基于迷信或“宗教教条”。

“前缀++总是更快”;的确是这样一个错误的教条,在想成为C程序员的人中很常见。

我一直在阅读这里的大部分答案和许多评论,我没有看到任何引用一个实例,我能想到哪里i++++i更有效(也许令人惊讶的是--i i--更有效)。这是针对DEC PDP-11的C编译器!

PDP-11的汇编指令用于寄存器的前减和后增,但没有相反的指令。这些指令允许任何“通用”寄存器用作堆栈指针。因此,如果你使用类似*(i++)的东西,它可以被编译成一个汇编指令,而*(++i)不能。

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