C ++中的i ++和++ i在性能上有区别吗?


352

我们有一个问题,C之间是否存在性能差异?i++++i

C ++的答案是什么?


我重新标记,因为这两个标记是查找此类问题的最简单方法。我还浏览了其他没有粘性标签的标签,并给了他们粘性标签。
George Stocker

104
使用C ++和++ C之间有性能差异吗?
new123456 2011年

2
文章:对迭代器使用前缀增量运算符++ it而不是后缀运算符it ++是否合理?- viva64.com/en/b/0093

Answers:


426

[执行摘要:++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变量及其关联的副本构造函数。如果复制构造函数很昂贵,那么这可能会对性能产生重大影响。


3
编译器可以避免的是第二个返回tmp的副本,方法是通过NRVO在调用方中分配tmp,如另一条评论所述。
Blaisorblade

7
如果将operator ++内联,则编译器无法完全避免这种情况吗?
爱德华-加布里埃尔·蒙蒂亚努(Gabriel Munteanu),2009年

16
是的,如果操作符++内联并且从未使用过tmp,则可以将其删除,除非tmp对象的构造函数或析构函数有副作用。
Zan Lynx

5
@kriss:C和C ++之间的区别在于,在C中,您可以保证操作符将被内联,并且到那时,一个不错的优化程序将能够消除该区别;相反,在C ++中,您不能假设内联-并非总是如此。
Blaisorblade 2012年

3
如果答案中提到了一些类,这些类持有指向动态分配(堆)内存的指针(无论是自动的,智能的还是原始的),则+1,如果副本构造函数必须执行深层副本。在这种情况下,没有论据,++ i的效率可能比i ++高一个数量级。它们的关键是要养成在算法实际上不需要后增量语义时使用前增量的习惯,然后您将习惯于编写本质上可以提高效率的代码,无论如何编译器可以优化。
phonetagger 2012年

64

是。有。

++运算符可以定义也可以不定义为函数。对于原始类型(int,double,...),运算符是内置的,因此编译器可能能够优化您的代码。但是,对于定义++运算符的对象而言,情况有所不同。

operator ++(int)函数必须创建一个副本。这是因为期望后缀++返回的值与其持有的值不同:它必须将其值保存在temp变量中,递增其值并返回temp。对于运算符++()(前缀++),无需创建副本:对象可以递增自身,然后简单地返回自身。

这是要点的说明:

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
}

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


2
“前置增量运算符在代码中引入了数据依赖关系:CPU必须等待增量操作完成才能在表达式中使用其值。在深度流水线化的CPU上,这会导致停顿。没有数据依赖关系。对于职位递增运算符。” (Game Engine Architecture(2nd edition))因此,如果后期增量的副本在计算上不是很密集,那么它仍然可以胜过前置增量。
马赛厄斯

在后缀代码中,这是如何工作的C t(*this); ++(*this); return t;。在第二行中,您将向右递增此指针,因此,如果递增此指针,将如何t更新它。这个值是否已经复制到其中t
rasen58

The operator++(int) function must create a copy.不它不是。不超过operator++()
Severin Pappadeux,

47

这是增量运算符使用不同转换单位时的基准。用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

结论

性能方面

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

语义上

  • i++increment i, I am interested in the previous value, though
  • ++iincrement i, I am interested in the current valueincrement i, no interest in the previous value。同样,即使您现在还不习惯,您也会习惯它。

努斯

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


1
有趣的测试。现在,差不多两年半之后,gcc 4.9和Clang 3.4呈现出类似的趋势。两者的Clang都快一点,但是prefix和postfix之间的差异比gcc还要糟糕。
嚼袜子

我真正想看到的是一个真实的示例,其中++ i / i ++有所作为。例如,它对任何std迭代器有影响吗?
Jakob Schou Jensen

@JakobSchouJensen:这些很容易成为现实世界的例子。考虑一个大型应用程序,它具有复杂的树结构(例如kd树,四叉树)或在表达式模板中使用大容器(以使SIMD硬件上的数据吞吐量最大化)。如果在那里有所作为,我真的不确定为什么如果不需要语义的话,为什么在特定情况下会退回到后增量。
塞巴斯蒂安·马赫2015年

@phresnel:我不认为operator ++在您的日常工作中不是表达式模板-您是否有实际的例子?operator ++的典型用法是用于整数和迭代器。那就是我认为知道是否存在任何差异(整数没有差异,但迭代器没有差异)会很有趣。
Jakob Schou Jensen 2015年

@JakobSchouJensen:没有实际的业务示例,但是有些数字运算应用程序可以用来计数。Wrt迭代器,请考虑以惯用的C ++样式编写的ray跟踪器,并且您有一个用于深度优先遍历的迭代器,例如for (it=nearest(ray.origin); it!=end(); ++it) { if (auto i = intersect(ray, *it)) return i; },不用担心实际的树结构(BSP,kd,Quadtree,Octree Grid等)。这样的迭代器将需要保持一定的状态,例如parent nodechild nodeindex和类似的东西。总而言之,即使只有很少的例子,我的立场还是...
Sebastian Mach 2015年

20

说在后缀的情况下编译器无法优化掉临时变量副本是不完全正确的。使用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)] 

因此,虽然后缀版本肯定会变慢,但如果您不使用它,则很可能是优化程序足以消除临时副本。


8
您忘记了重要的一点,这里所有内容都是内联的。如果操作员的定义不可用,则无法避免使用离线代码完成复制;内联优化是非常明显的,因此任何编译器都可以做到。
Blaisorblade

14

谷歌C ++风格指南说:

预先增加和减少

将增量和减量运算符的前缀形式(++ i)与迭代器和其他模板对象一起使用。

定义:当变量递增(++ i或i ++)或递减(--i或i--)且不使用表达式的值时,必须决定是先递增(递减)还是后递增(递减)。

优点:忽略返回值时,“前置”格式(++ i)的效率永远不会比“后置”窗体(i ++)的效率低,并且通常效率更高。这是因为后递增(或递减)要求制作i的副本,这是表达式的值。如果我是迭代器或其他非标量类型,则复制i可能会很昂贵。由于两种类型的增量在忽略该值时的行为相同,为什么不总是预增量呢?

缺点:在C语言中,不使用表达式值时,尤其是在for循环中,使用后递增的传统。有些人发现后递增很容易阅读,因为“主题”(i)在“动词”(++)之前,就像英语一样。

决策:对于简单的标量(非对象)值,没有理由偏爱一种形式,我们都允许。对于迭代器和其他模板类型,请使用预增量。


1
“决定:对于简单的标量(非对象)值,没有理由偏爱一种形式,我们允许两种形式。对于迭代器和其他模板类型,请使用预增量。”
Nosredna

2
嗯,那是什么?
塞巴斯蒂安·马赫

答案中提到的链接当前已断开
karol

4

我想指出Andrew Koenig最近在Code Talk上的精彩文章。

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

在我们公司,如果适用,我们也会使用++ iter约定来确保一致性和性能。但是安德鲁提出了有关意图与绩效的被忽视的细节。有时我们想使用iter ++而不是++ iter。

因此,首先确定您的意图,如果pre或post无关紧要,那么请使用pre,因为它可以避免创建额外的对象并将其抛出,从而带来一些性能上的好处。



4

@基丹

...提出了有关意图与绩效的被忽略的细节。有时我们想使用iter ++而不是++ iter。

显然,post和pre-increment具有不同的语义,并且我确信每个人都同意,使用结果时,应使用适当的运算符。我认为问题是当丢弃结果(如for循环)时应该怎么做。这个问题(IMHO)的答案是,由于性能方面的考虑最多可以忽略不计,因此您应该做更自然的事情。对我自己++i来说更自然,但是我的经验告诉我,我是少数派,并且使用i++它将减少大多数阅读您代码的人的金属开销。

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

[*]插入关于++C更合理名称的强制性讨论。


4
@Motti :(开个玩笑)如果您记得Bjarne Stroustrup C ++最初将其编码为生成C程序的预编译器,则C ++名称是合乎逻辑的。因此,C ++返回了旧的C值。或者可能是为了增强C ++从一开始就在概念上存在缺陷。
kriss

4
  1. ++ i-更快不使用返回值
  2. i ++ - 使用返回值更快

不使用返回值时,对于++ i,保证编译器不使用临时值。不保证更快,但不能保证慢。

使用返回值时,i ++允许处理器将增量和左侧都推入管线,因为它们彼此不依赖。++ i可能会使流水线停顿,因为在预增量操作一直到最后之前,处理器无法启动左侧。同样,由于处理器可能会发现其他有用的东西,因此不能保证流水线停顿。


3

马克:只是想指出运算符++是内联的很好的选择,如果编译器选择这样做,则在大多数情况下都将消除冗余副本。(例如,POD类型,通常是迭代器。)

也就是说,在大多数情况下使用++ iter还是更好的样式。:-)


3

之间的性能差异++ii++当你认为运营商作为返回值的功能,以及它们是如何实现将更加明显。为了更容易理解正在发生的事情,下面的代码示例将int像使用一样使用struct

++i递增变量,然后返回结果。可以就地完成此任务,并使用最少的CPU时间,在许多情况下,只需要一行代码即可:

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

但是不能说同样的话i++

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

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

当预增量和后增量之间没有功能差异时,编译器可以执行优化,从而使两者之间没有性能差异。但是,如果涉及到诸如a struct或a的复合数据类型class,则复制构造函数将在增量后调用,如果需要深度复制,将无法执行此优化。因此,与后增量相比,前增量通常更快,并且所需的内存更少。


1

@Mark:我删除了先前的答案,因为它有点翻转,仅此而已就值得一票。实际上,我认为这是一个好问题,因为它询问了很多人的想法。

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

如果在递增迭代器上花费的CPU时间比例小于10%,那么您可能不在乎。

如果在递增迭代器上花费的CPU时间分数大于10%,则可以查看哪些语句正在执行该迭代。看看是否可以增加整数而不是使用迭代器。您可以,虽然在某种意义上可能不太理想,但您的机会将是相当不错的,您基本上可以节省在这些迭代器上花费的所有时间。

我看过一个示例,其中迭代器增量消耗的时间超过90%。在那种情况下,使用整数递增将执行时间实质上减少了该数量。(即优于10倍加速)


1

@wilhelmtell

编译器可以忽略该临时文件。从另一个线程逐字查看:

即使这样做会改变程序行为,也允许C ++编译器消除基于堆栈的临时变量。VC 8的MSDN链接:

http://msdn.microsoft.com/zh-CN/library/ms364057(VS.80).aspx


1
没关系 NRVO避免了将“ CC :: operator ++(int)”中的t复制回调用方的需要,但是i ++仍将旧值复制到调用方的堆栈中。如果没有NRVO,i ++将创建2个副本,一个到t,一个副本回到调用方。
Blaisorblade

0

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


3
对不起,但是那困扰着我。当几乎没有关系的时候,谁会说这是“好习惯”?如果人们希望使其成为自己学科的一部分,那很好,但是让我们将重要原因与个人品味区分开来。
Mike Dunlavey,

@MikeDunlavey好的,那么当没关系时,您通常使用哪一侧?xD要么是一个,要么不是另一个!post ++(如果您以一般含义使用它。更新它,返回旧的)完全不如++ pre(更新它,返回),所以没有任何理由要降低性能。如果您想在以后进行更新,那么程序员甚至根本不会执行post ++。我们已经有了它,不会浪费时间进行复制。使用后更新它。然后编译器就具有您想要的常识。
水坑

@Puddle:当我听到以下消息时:“没有任何理由要降低性能”,我知道我听到的是“一分钱明智-愚蠢”。您需要对涉及的数量有所了解。仅当这占所涉及时间的1%以上时,您才考虑一下。通常,如果您正在考虑此问题,那么您就不会考虑到数百万倍的较大问题,而这正是导致软件运行缓慢的原因。
Mike Dunlavey '18

@MikeDunlavey反驳废话,以满足您的自我。您试图听起来像是一些明智的和尚,但您什么也没说。涉及的幅度...如果仅超过1%的时间,您应该关心... xD绝对运球。如果效率低下,则值得了解和修复。正是出于这个确切的原因,我们在这里思考!我们并不担心我们将从这些知识中获得多少收益。当我说您不想降低性能时,请继续,然后解释一个该死的场景。怀斯先生!
水坑

0

两者都一样快;)如果您希望对处理器进行相同的计算,则只是顺序不同而已。

例如,以下代码:

#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 ++,这是一个incl助记符,因此它是相同的操作;)


它是C,而OP询问C ++。在C中是相同的。在C ++中,++ i更快。由于其对象。但是,某些编译器可能会优化后递增运算符。
Wiggler Jtag 2015年

0

预期的问题是有关何时未使用结果的(对于C来说很清楚)。由于问题是“社区Wiki”,因此有人可以解决此问题吗?

关于过早的优化,经常引用Knuth。那就对了。但是唐纳德·克努斯(Donald Knuth)永远都不会用这些日子里看到的可怕代码来辩护。在Java整数(不是int)中见过a = b + c吗?总计3次装箱/拆箱转换。避免这样的事情很重要。而无用地用i ++代替++ i是同样的错误。编辑:正如phresnel很好地在评论中指出的那样,这可以归纳为“过早的优化是邪恶的,过早的悲观也是邪恶的”。

甚至人们更习惯于i ++的事实都是不幸的C遗留问题,这是由K&R的概念错误引起的(如果您遵循Intent的论点,那是合乎逻辑的结论;捍卫K&R因为他们是K&R是没有意义的,他们很好,但是他们不适合作为语言设计师; C设计中存在无数错误,从gets()到strcpy()到strncpy()API(从第一天开始就应该拥有strlcpy()API) )。

顺便说一句,我是那些不习惯C ++来发现++ i令人讨厌阅读的人之一。不过,我还是使用它,因为我承认这是正确的。


我看到您正在攻读博士学位。对编译器优化和类似的东西感兴趣。这是伟大的,但不要忘了学术界的回音室,和常识常常被留在门外,至少在CS你可能在这个感兴趣:stackoverflow.com/questions/1303899/...
麦克Dunlavey

我从未发现++ii++它更令人讨厌(实际上,我发现它更酷),但是您的其余帖子得到了我的充分肯定。也许要加点“过早的优化是邪恶的,过早的悲观也是如此”
Sebastian Mach

strncpy在他们当时使用的文件系统中达到了目的;文件名是一个8个字符的缓冲区,并且不必以空字符结尾。您不能责怪他们没有看到语言发展的未来40年。
2014年

@MattMcNabb:不是8个字符的文件名是MS-DOS专有的吗?C是Unix发明的。无论如何,即使strncpy指出了要点,也没有充分说明缺乏strlcpy的理由:即使原始C语言也具有不应该溢出的数组,而这需要strlcpy。最多,他们只是缺少打算利用这些漏洞的攻击者。但是不能说预测这个问题是微不足道的,所以如果我改写我的文章,我不会使用相同的语气。
Blaisorblade

@Blaisorblade:我记得,早期的UNIX文件名限制为14个字符。缺少strlcpy()它的事实是它还没有被发明出来。
基思·汤普森

-1

是时候给人们提供智慧;)-有一个简单的技巧可以使C ++后缀增量的行为与前缀增量几乎相同(是我自己发明的,但是在其他人的代码中也看到了,所以我不是单独)。

基本上,诀窍是使用帮助程序类在返回后推迟增量,而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;
}

发明是针对一些繁重的自定义迭代器代码的,它可以减少运行时间。前缀与后缀的成本现在是一个参考,如果这是自定义操作员进行的繁琐工作,则前缀和后缀对我产生的运行时间相同。


-5

++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;
}
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.