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

时间:2020-03-05 18:42:33  来源:igfitidea点击:

我们有一个问题,C语言中的" i ++"和" ++ i"之间是否存在性能差异?

C ++的答案是什么?

解决方案

回答

[执行摘要:如果没有特定原因使用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,...),运算符是内置的,因此编译器可能将能够优化代码。但是,对于定义++运算符的对象而言,情况有所不同。

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 ++();。这样我们就不会保存副本。在许多增量(大循环?)和/或者大对象的情况下,这可能很重要。

回答

说在后缀的情况下编译器无法优化掉临时变量副本是不完全正确的。使用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 ++编译器消除基于堆栈的临时变量,即使这样做会改变程序的行为。 VC 8的MSDN链接:

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

回答

Google C ++样式指南说:

Preincrement and Predecrement
  
  Use prefix form (++i) of the increment and decrement operators with
  iterators and other template objects.
  
  Definition: When a variable is incremented (++i or i++) or decremented (--i or
  i--) and the value of the expression is not used, one must decide
  whether to preincrement (decrement) or postincrement (decrement).
  
  Pros: When the return value is ignored, the "pre" form (++i) is never less
  efficient than the "post" form (i++), and is often more efficient.
  This is because post-increment (or decrement) requires a copy of i to
  be made, which is the value of the expression. If i is an iterator or
  other non-scalar type, copying i could be expensive. Since the two
  types of increment behave the same when the value is ignored, why not
  just always pre-increment?
  
  Cons: The tradition developed, in C, of using post-increment when the
  expression value is not used, especially in for loops. Some find
  post-increment easier to read, since the "subject" (i) precedes the
  "verb" (++), just like in English.
  
  Decision: For simple scalar (non-object) values there is no reason to prefer one
  form and we allow either. For iterators and other template types, use
  pre-increment.

回答

我想指出安德鲁·科尼格(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,因为它可以避免创建额外的对象并将其抛出,从而对性能有所帮助。

回答

@基丹

...raises over-looked detail regarding intent vs performance. There are times when we want to use iter++ instead of ++iter.

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

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

[*]强制性地讨论" ++ C"是一个更合逻辑的名称。

回答

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

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

回答

预期的问题是有关何时未使用结果的(对于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令人讨厌阅读的人之一。尽管如此,我还是使用它,因为我承认这是正确的。

回答

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

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

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

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

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