我们的问题是,在C语言中i++和++i的性能有区别吗?

c++的答案是什么?


当前回答

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

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

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

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

其他回答

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

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

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

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

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

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

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

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

但是,如果i是c++类的实例,则i++和++i将调用其中一个操作符++函数。下面是这些函数的标准组合:

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

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

既然你也要求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

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

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

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

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