在c++中i++和++i之间有性能差异吗?

103062 次浏览

[执行摘要:如果没有特定的理由使用i++,请使用++i。]

对于c++来说,答案有点复杂。

如果i是一个简单类型(不是c++类的实例),则然后给出C的答案(“不,没有性能差异”)保持不变,因为编译器正在生成代码。

然而,如果i是c++类的实例,则i++++i将调用operator++函数之一。下面是这些函数的标准组合:

Foo& Foo::operator++()   // called for ++i
{
this->data += 1;
return *this;
}


Foo Foo::operator++(int ignored_dummy_value)   // called for i++
{
Foo tmp(*this);   // variable "tmp" cannot be optimized away by the compiler
++(*this);
return tmp;
}

由于编译器不生成代码,而只是调用operator++函数,因此没有办法优化掉tmp变量及其相关的复制构造函数。如果复制构造函数的开销很大,则会对性能产生重大影响。

是的。有。

++操作符可以定义为函数,也可以不定义为函数。对于基本类型(int, double,…),操作符是内置的,因此编译器可能能够优化您的代码。但对于定义了++运算符的对象,情况就不一样了。

操作符++(int)函数必须创建一个副本。这是因为postfix ++被期望返回一个与它所保存的值不同的值:它必须将其值保存在一个临时变量中,自增其值并返回临时值。在操作符++(),前缀++的情况下,不需要创建一个副本:对象可以自增,然后简单地返回自己。

下面是关于这一点的一个例子:

struct C
{
C& operator++();      // prefix
C  operator++(int);   // postfix


private:


int i_;
};


C& C::operator++()
{
++i_;
return *this;   // self, no copy created
}


C C::operator++(int ignored_dummy_value)
{
C t(*this);
++(*this);
return t;   // return a copy
}

每次调用操作符++(int)都必须创建一个副本,编译器对此无能为力。当有选择时,使用运算符++();这样就不需要保存副本。在很多增量(大循环?)和/或大对象的情况下,它可能很重要。

说编译器不能优化掉后缀情况下的临时变量副本是不完全正确的。用VC进行的快速测试表明,至少在某些情况下,它可以做到这一点。

在下面的例子中,生成的代码对于前缀和后缀是相同的,例如:

#include <stdio.h>


class Foo
{
public:


Foo() { myData=0; }
Foo(const Foo &rhs) { myData=rhs.myData; }


const Foo& operator++()
{
this->myData++;
return *this;
}


const Foo operator++(int)
{
Foo tmp(*this);
this->myData++;
return tmp;
}


int GetData() { return myData; }


private:


int myData;
};


int main(int argc, char* argv[])
{
Foo testFoo;


int count;
printf("Enter loop count: ");
scanf("%d", &count);


for(int i=0; i<count; i++)
{
testFoo++;
}


printf("Value: %d\n", testFoo.GetData());
}

无论您使用的是++testFoo还是testfoo++,都将得到相同的结果代码。事实上,无需从用户读取计数,优化器将整个事情归结为一个常数。所以这个:

for(int i=0; i<10; i++)
{
testFoo++;
}


printf("Value: %d\n", testFoo.GetData());

结果如下:

00401000  push        0Ah
00401002  push        offset string "Value: %d\n" (402104h)
00401007  call        dword ptr [__imp__printf (4020A0h)]

因此,虽然后缀版本肯定会更慢,但如果你不使用它,优化器可能会足够好,可以摆脱临时副本。

即使在没有性能优势的内置类型上也应该使用++i的原因是为了给自己养成一个好习惯。

@wilhelmtell

编译器可以省略临时对象。从另一个线程逐字逐句:

c++编译器允许消除基于堆栈的临时对象,即使这样做会改变程序行为。MSDN链接vc8:

http://msdn.microsoft.com/en-us/library/ms364057(VS.80).aspx

谷歌c++风格指南表示:

预增量和前任

使用自增和自减操作符的前缀形式(++i) with

.迭代器和其他模板对象

定义:当变量被递增(++i或i++)或递减(——i或 I——),表达式的值没有被使用,必须自己决定 是前递增(递减)还是后递增(递减)

优点:当返回值被忽略,"pre"形式(++i)从未减少 比“邮政”更有效率。Form (i++),并且通常更高效。 这是因为后递增(或递减)需要i to的副本 Be made,也就是表达式的值。如果i是一个迭代器或 对于其他非标量类型,复制I可能代价很高。因为这两个 当值被忽略时,增量类型的行为是相同的,为什么不呢 总是预增量?< / p > 缺点:在C语言中,传统的使用后增量当 表达式值不被使用,特别是在for循环中。一些人发现 后增量更容易阅读,因为“;subject"(i)在 “verb"

.

.

. 对于简单的标量(非对象)值,没有理由选择一个 形式和我们都允许。对于迭代器和其他模板类型,使用 pre-increment . < / p >

我想指出Andrew Koenig最近在Code Talk上发表的一篇出色的文章。

http://dobbscodetalk.com/index.php?option=com_myblog&show=Efficiency-versus-intent.html&Itemid=29

在我们公司,我们也在适用的情况下使用++iter的一致性和性能。但Andrew提出了关于意图与性能的忽略细节。有时我们想用iter++而不是++iter。

所以,首先决定你的意图,如果pre或post不重要,那么使用pre,因为它将有一些性能优势,避免创建额外的对象并抛出它。

@Ketan

...加薪忽略了关于意图和性能的细节。有时我们想用iter++而不是++iter。

显然,后增量和前增量有不同的语义,我相信每个人都同意在使用结果时应该使用适当的操作符。我认为问题是当结果被丢弃(如for循环)时应该做什么。问题(恕我直言)的答案是,由于性能考虑因素充其量可以忽略不计,你应该做更自然的事情。对我自己来说,++i更自然,但我的经验告诉我,我是少数人,使用i++将导致更少的金属开销大多数人阅读你的代码。

毕竟,这就是语言不被称为“++C”的原因。[*]

[*]插入关于++C是一个更合乎逻辑的名称的必要讨论。

Mark:只是想指出操作符++是很好的内联候选者,如果编译器选择这样做,在大多数情况下多余的拷贝将被消除。(例如POD类型,迭代器通常是这种类型。)

也就是说,在大多数情况下使用++iter仍然是更好的风格。: -)

有意的问题是关于什么时候结果是未使用的(这从C的问题中很明显)。有人能解决这个问题吗,因为这个问题是“社区维基”?

关于过早的优化,Knuth经常被引用。这是正确的。但是Donald Knuth永远不会用你现在看到的那些可怕的代码来辩护。见过Java整数(不是int)中的a = b + c吗?这相当于3次装箱/开箱转换。避免这样的事情很重要。无用地写i++而不是++i也是同样的错误。 编辑:正如phresnel在评论中所说的那样,这可以总结为“过早的优化是邪恶的,过早的悲观也是邪恶的”

甚至人们更习惯于i++这一事实也是C语言的不幸遗产,是由K&R的一个概念错误造成的(如果你遵循意图论点,这是一个合乎逻辑的结论;为kr辩护,因为它们是kr是没有意义的,它们很伟大,但作为语言设计者,它们并不伟大;C设计中存在无数错误,从gets()到strcpy(),再到strncpy() API(它应该从第一天开始就有strlcpy() API)。

顺便说一句,我是那些不太习惯c++的人之一,觉得c++ I读起来很烦人。尽管如此,我仍然使用它,因为我承认它是正确的。

@Mark:我删除了我之前的答案,因为它有点轻率,仅凭这一点就应该被打反对票。实际上,我认为这是一个很好的问题,因为它问了很多人的想法。

通常的答案是++i比i++快,毫无疑问,但更大的问题是“什么时候应该关心?”

如果增量迭代器所花费的CPU时间小于10%,那么您可能不会在意。

如果增量迭代器所花费的CPU时间百分比大于10%,则可以查看哪些语句正在进行该迭代。看看你是否可以只增加整数而不是使用迭代器。你有可能做到,虽然在某种意义上可能不太理想,但很有可能你会节省花在那些迭代器上的所有时间。

我曾经见过一个例子,其中迭代器增量消耗了超过90%的时间。在这种情况下,采用整数递增法将执行时间减少了这么多。(即优于10倍加速)

两者都一样快;) 如果你想在处理器上进行相同的计算,只是计算的顺序不同

例如,以下代码:

#include <stdio.h>


int main()
{
int a = 0;
a++;
int b = 0;
++b;
return 0;
}

生产以下组件:

 0x0000000100000f24 <main+0>: push   %rbp
0x0000000100000f25 <main+1>: mov    %rsp,%rbp
0x0000000100000f28 <main+4>: movl   $0x0,-0x4(%rbp)
0x0000000100000f2f <main+11>:    incl   -0x4(%rbp)
0x0000000100000f32 <main+14>:    movl   $0x0,-0x8(%rbp)
0x0000000100000f39 <main+21>:    incl   -0x8(%rbp)
0x0000000100000f3c <main+24>:    mov    $0x0,%eax
0x0000000100000f41 <main+29>:    leaveq
0x0000000100000f42 <main+30>:    retq

你可以看到,对于a++和b++,它是一个包含助记符,所以它是相同的操作;)

下面是自增操作符位于不同转换单元时的基准测试。g++ 4.5编译器。

现在先忽略样式问题

// a.cc
#include <ctime>
#include <array>
class Something {
public:
Something& operator++();
Something operator++(int);
private:
std::array<int,PACKET_SIZE> data;
};


int main () {
Something s;


for (int i=0; i<1024*1024*30; ++i) ++s; // warm up
std::clock_t a = clock();
for (int i=0; i<1024*1024*30; ++i) ++s;
a = clock() - a;


for (int i=0; i<1024*1024*30; ++i) s++; // warm up
std::clock_t b = clock();
for (int i=0; i<1024*1024*30; ++i) s++;
b = clock() - b;


std::cout << "a=" << (a/double(CLOCKS_PER_SEC))
<< ", b=" << (b/double(CLOCKS_PER_SEC)) << '\n';
return 0;
}

O (n)增加

测试

// b.cc
#include <array>
class Something {
public:
Something& operator++();
Something operator++(int);
private:
std::array<int,PACKET_SIZE> data;
};




Something& Something::operator++()
{
for (auto it=data.begin(), end=data.end(); it!=end; ++it)
++*it;
return *this;
}


Something Something::operator++(int)
{
Something ret = *this;
++*this;
return ret;
}

结果

在虚拟机上使用g++ 4.5的结果(计时以秒为单位):

Flags (--std=c++0x)       ++i   i++
-DPACKET_SIZE=50 -O1      1.70  2.39
-DPACKET_SIZE=50 -O3      0.59  1.00
-DPACKET_SIZE=500 -O1    10.51 13.28
-DPACKET_SIZE=500 -O3     4.28  6.82

O(1)增加

测试

现在让我们看看下面的文件:

// c.cc
#include <array>
class Something {
public:
Something& operator++();
Something operator++(int);
private:
std::array<int,PACKET_SIZE> data;
};




Something& Something::operator++()
{
return *this;
}


Something Something::operator++(int)
{
Something ret = *this;
++*this;
return ret;
}

它对增量没有任何影响。这模拟了增量具有恒定复杂度的情况。

结果

结果现在变化很大:

Flags (--std=c++0x)       ++i   i++
-DPACKET_SIZE=50 -O1      0.05   0.74
-DPACKET_SIZE=50 -O3      0.08   0.97
-DPACKET_SIZE=500 -O1     0.05   2.79
-DPACKET_SIZE=500 -O3     0.08   2.18
-DPACKET_SIZE=5000 -O3    0.07  21.90

结论

属性

如果不需要前一个值,请养成使用预递增的习惯。即使与内置类型保持一致,您也会习惯它,如果您用自定义类型替换内置类型,也不会有遭受不必要性能损失的风险。

Semantic-wise

  • i++表示increment i, I am interested in the previous value, though
  • ++i表示increment i, I am interested in the current valueincrement i, no interest in the previous value。再说一次,你会习惯的,即使你现在还不习惯。

Knuth。

过早的优化是万恶之源。过早的悲观也是如此。

当你将操作符视为值返回函数以及它们的实现方式时,++ii++之间的性能差异将更加明显。为了更容易理解发生了什么,下面的代码示例将使用int,就好像它是struct一样。

++i对变量加1,然后返回结果。这可以就地完成,并且只需要最少的CPU时间,在许多情况下只需要一行代码:

int& int::operator++() {
return *this += 1;
}

但是i++却不能这样说。

后递增,i++,通常被视为返回原始值之前递增。然而,函数只能在完成时返回结果。因此,有必要创建一个包含原始值的变量的副本,增加变量,然后返回包含原始值的副本:

int int::operator++(int& _Val) {
int _Original = _Val;
_Val += 1;
return _Original;
}

当增量前和增量后之间没有功能差异时,编译器可以执行优化,使两者之间没有性能差异。然而,如果涉及组合数据类型,如structclass,复制构造函数将在增量后调用,如果需要深度复制,则不可能执行此优化。因此,前增量通常比后增量更快,需要的内存更少。

++ii++快,因为它不返回值的旧副本。

它也更直观:

x = i++;  // x contains the old value of i
y = ++i;  // y contains the new value of i

这个C语言的例子打印“02”而不是你可能期望的“12”:

#include <stdio.h>


int main(){
int a = 0;
printf("%d", a++);
printf("%d", ++a);
return 0;
}

c++也是一样:

#include <iostream>
using namespace std;


int main(){
int a = 0;
cout << a++;
cout << ++a;
return 0;
}
  1. + +我 -更快的不使用返回值
  2. 我+ + -更快的使用返回值

不使用返回值时,编译器保证不会在+ +我的情况下使用临时类型。不保证更快,但保证不会变慢。

使用返回值时,我+ +允许处理器推送两个 增量和左侧进入管道,因为它们彼此不依赖。i可能会使管道停止,因为处理器无法启动左侧,直到增量前操作已经蜿蜒完成。同样,也不保证会出现管道中断,因为处理器可能会找到其他有用的东西插入

是时候给人们提供智慧的宝石了;)-有一个简单的技巧可以让c++的后缀增量表现得和前缀增量几乎一样(为自己发明的,但我在其他人的代码中也看到了它,所以我不是一个人)。

基本上,诀窍是在返回后使用helper类来延迟增量,然后RAII来拯救

#include <iostream>


class Data {
private: class DataIncrementer {
private: Data& _dref;


public: DataIncrementer(Data& d) : _dref(d) {}


public: ~DataIncrementer() {
++_dref;
}
};


private: int _data;


public: Data() : _data{0} {}


public: Data(int d) : _data{d} {}


public: Data(const Data& d) : _data{ d._data } {}


public: Data& operator=(const Data& d) {
_data = d._data;
return *this;
}


public: ~Data() {}


public: Data& operator++() { // prefix
++_data;
return *this;
}


public: Data operator++(int) { // postfix
DataIncrementer t(*this);
return *this;
}


public: operator int() {
return _data;
}
};


int
main() {
Data d(1);


std::cout <<   d << '\n';
std::cout << ++d << '\n';
std::cout <<   d++ << '\n';
std::cout << d << '\n';


return 0;
}

Invented用于一些繁重的自定义迭代器代码,它减少了运行时间。前缀vs后缀的成本现在是一个参考,如果这是自定义操作符做大量的移动,前缀和后缀产生了相同的运行时为我。

++ii = i +1快,因为在i = i + 1中发生了两个操作,第一次递增,第二次将其赋值给一个变量。但是在i++中只进行增量操作。

既然你也要求c++,下面是java的基准测试(用jmh制作):

private static final int LIMIT = 100000;


@Benchmark
public void postIncrement() {
long a = 0;
long b = 0;
for (int i = 0; i < LIMIT; i++) {
b = 3;
a += i * (b++);
}
doNothing(a, b);
}


@Benchmark
public void preIncrement() {
long a = 0;
long b = 0;
for (int i = 0; i < LIMIT; i++) {
b = 3;
a += i * (++b);
}
doNothing(a, b);
}

结果表明,即使在某些计算中实际使用了增量变量(b)的值,迫使需要存储额外的值以防止后增量,每个操作的时间也完全相同:

Benchmark                         Mode  Cnt  Score   Error  Units
IncrementBenchmark.postIncrement  avgt   10  0,039   0,001  ms/op
IncrementBenchmark.preIncrement   avgt   10  0,039   0,001  ms/op

i++有时比++ I快!

对于使用ILP(指令级并行)的x86架构,i++在某些情况下可能优于++i。

为什么?因为数据依赖关系。现代cpu可以并行化很多东西。如果接下来的几个CPU周期对i的< >强劲增加< / >强值没有任何直接依赖,CPU可能会省略微码以延迟i的增量,并将其推入“空闲插槽”。这意味着你实际上得到了一个“免费”;增量。

我不知道ILE在这种情况下走多远,但我认为如果迭代器变得太复杂,并做指针解引用,这可能不会工作。

下面是Andrei Alexandrescu解释这个概念的演讲:https://www.youtube.com/watch?v=vrfYLlR8X8k&list=WL&index=5