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

时间:2008-08-24 07:14:22

标签: c++ performance oop post-increment pre-increment

我们有问题is there a performance difference between i++ and ++i in C?

C ++的答案是什么?

19 个答案:

答案 0 :(得分:394)

[执行摘要:如果您没有特定原因使用++i,请使用i++。]

对于C ++,答案有点复杂。

如果i是一个简单类型(不是C ++类的实例),then the answer given for C ("No there is no performance difference")成立,因为编译器正在生成代码。

但是,如果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变量及其关联的复制构造函数。如果复制构造函数很昂贵,那么这会对性能产生重大影响。

答案 1 :(得分:56)

是。有。。

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

operator ++(int)函数必须创建一个副本。这是因为postfix ++应该返回一个与它所拥有的值不同的值:它必须在temp变量中保存其值,增加其值并返回temp。对于operator ++(),前缀++,不需要创建副本:对象可以自行递增,然后只返回自己。

以下是一个例子:

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 :(得分:41)

这是增量运算符在不同翻译单元中的基准。用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。再说一遍,即使你现在不熟悉,你也会习惯它。

Knuth的。

过早优化是万恶之源。和过早的悲观情绪一样。

答案 3 :(得分: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)] 

因此,虽然后缀版本的速度肯定会慢一点,但如果您不使用它,优化器可能会很好地摆脱临时副本。

答案 4 :(得分:13)

Google C++ Style Guide说:

  

预先增量和预先减少

     

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

     

定义:当变量递增(++ i或i ++)或递减(--i或   i--)并且没有使用表达式的值,必须决定   是否预先增量(减量)或后增量(减量)。

     

优点:当忽略返回值时,“pre”表单(++ i)永远不会少   比“post”形式(i ++)更有效,并且通常更有效率。   这是因为后递增(或递减)需要i的副本   制作,这是表达的价值。如果我是迭代器或   其他非标量类型,复制我可能是昂贵的。既然这两个   当忽略该值时,增量类型的行为相同,为什么不呢   只是总是预先增加?

     

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

     

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

答案 5 :(得分:5)

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

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

在我们公司,我们也使用++ iter的惯例来保证一致性和性能。但安德鲁提出了关于意图与表现的过度细节。有时我们想使用iter ++而不是++ iter。

所以,首先要确定你的意图,如果前置或后置无关紧要,那么请选择pre,因为它可以通过避免创建额外的对象并抛出它来获得一些性能上的好处。

答案 6 :(得分:5)

@Ketan

  

......提出了有关意图与表现的详细信息。有时我们想使用iter ++而不是++ iter。

显然post和pre-increment有不同的语义,我相信每个人都同意,当使用结果时你应该使用适当的运算符。我认为问题是当结果被丢弃时应该怎么做(如在for循环中)。 这个问题(恕我直言)的答案是,由于性能考虑因素最多可以忽略不计,所以你应该做更自然的事情。对于我自己++i更自然,但我的经验告诉我,我是少数人,使用i++会减少大多数人阅读代码的金属开销。

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

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

答案 7 :(得分:3)

  1. ++ i - 更快不使用返回值
  2. i ++ - 更快使用返回值
  3. 不使用返回值时,编译器保证在 ++ i 的情况下不使用临时值。不保证更快,但保证不会慢。

    使用时,返回值 i ++ 允许处理器同时推送  增量和左侧进入管道,因为它们不相互依赖。 ++我可能会停止管道,因为处理器无法启动左侧,直到预增量操作一直蜿蜒。同样,管道停滞不能保证,因为处理器可能会发现其他有用的东西。

答案 8 :(得分:3)

Mark:只想指出operator ++是很好的内联候选者,如果编译器选择这样做,大多数情况下都会消除冗余副本。 (例如POD类型,迭代器通常是。)

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

答案 9 :(得分: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;
}

当预增量和后增量之间没有功能差异时,编译器可以执行优化,使得两者之间没有性能差异。但是,如果涉及复合数据类型,例如structclass,则复制构造函数将在后递增时调用,如果深层复制,则无法执行此优化需要。因此,预增量通常比后增量更快并且需要更少的内存。

答案 10 :(得分:1)

预期的问题是关于何时未使用结果(从C的问题中可以清楚地看出)。有人可以解决这个问题,因为问题是“社区维基”吗?

关于过早优化,经常引用Knuth。那就对了。但唐纳德克努特永远不会用这些日子里你能看到的可怕代码来辩护。曾经见过Java Integers中的a = b + c(不是int)吗?这相当于3次装箱/拆箱转换。避免这样的事情很重要。并且无用地编写i ++而不是++我是同样的错误。 编辑:正如phresnel很好地将其置于评论中,这可以概括为“过早的优化是邪恶的,就像过早的悲观化”。

即使是人们更习惯于i ++这一事实也是一个令人遗憾的C遗产,由K&amp; R的概念性错误引起(如果你遵循意图论证,这是一个合乎逻辑的结论;并且捍卫K&amp; R因为他们是K&amp; R是没有意义的,它们很棒,但它们并不像语言设计师那么好; C设计中存在无数错误,从gets()到strcpy(),到strncpy()API(应该有) strlcpy()API从第1天起))。

顺便说一句,我是那些没有用到C ++的人之一,因为我觉得很难读懂++。不过,我使用它,因为我承认它是正确的。

答案 11 :(得分:1)

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

基本上,诀窍是使用帮助程序类在返回后推迟增量,并且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用于一些重型自定义迭代器代码,它减少了运行时。前缀和后缀的成本现在是一个参考,如果这是自定义操作员进行大量移动,前缀和后缀为我产生相同的运行时间。

答案 12 :(得分:1)

@Mark:我删除了我以前的答案,因为它有点翻转,并且应该为此单独进行下调。我认为这是一个很好的问题,因为它会询问很多人的想法。

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

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

如果在递增迭代器中花费的CPU时间比例大于10%,则可以查看正在进行迭代的语句。看看你是否可以增加整数而不是使用迭代器。你可能有机会,虽然它可能在某种程度上不太可取,但你很可能会节省在这些迭代器上花费的所有时间。

我已经看到了一个例子,其中迭代器递增在90%以上的时间内消耗得很好。在这种情况下,进行整数递增会将执行时间减少大致相应的量。 (即优于10倍加速)

答案 13 :(得分:1)

@wilhelmtell

编译器可以忽略临时。从另一个帖子逐字逐句:

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

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

答案 14 :(得分:1)

为什么你应该使用++ i,即使在没有性能优势的内置类型上也是为自己创造一个好习惯。

答案 15 :(得分: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

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

答案 16 :(得分:-1)

当您编写i++时,您告诉编译器在完成此行或循环后递增。

++ii++略有不同。在i++中,在完成循环后递增但++i在循环结束之前直接递增。

答案 17 :(得分:-1)

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

答案 18 :(得分:-4)

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

它也更直观:

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

This C example打印&#34; 02&#34;而不是&#34; 12&#34;你可能会期待:

#include <stdio.h>

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

Same for C++

#include <iostream>
using namespace std;

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