i ++比++ i效率低,如何展示?

时间:2009-07-12 19:40:29

标签: c++ optimization prefix postfix-operator

我试图通过示例显示前缀增量比后缀增量更有效。

理论上这是有道理的:i ++需要能够返回未增加的原始值并因此存储它,而++我可以返回增量值而不存储先前的值。

但是有一个很好的例子可以在实践中证明这一点吗?

我尝试了以下代码:

int array[100];

int main()
{
  for(int i = 0; i < sizeof(array)/sizeof(*array); i++)
    array[i] = 1;
}

我使用gcc 4.4.0编译它,如下所示:

gcc -Wa,-adhls -O0 myfile.cpp

我再次这样做,后缀增量更改为前缀增量:

for(int i = 0; i < sizeof(array)/sizeof(*array); ++i)

在两种情况下,结果都是相同的汇编代码。

这有些出乎意料。似乎通过关闭优化(使用-O0)我应该看到显示概念的差异。我错过了什么?有没有更好的例子来展示这个?

9 个答案:

答案 0 :(得分:23)

一般的情况下,后增量将导致一个副本,其中预增量不会。当然,这将在大量情况下进行优化,并且在不是复制操作的情况下,可以忽略不计(即,对于内置类型)。

这是一个小例子,显示了后增量的潜在低效率。

#include <stdio.h>

class foo 
{

public:
    int x;

    foo() : x(0) { 
        printf( "construct foo()\n"); 
    };

    foo( foo const& other) { 
        printf( "copy foo()\n"); 
        x = other.x; 
    };

    foo& operator=( foo const& rhs) { 
        printf( "assign foo()\n"); 
        x = rhs.x;
        return *this; 
    };

    foo& operator++() { 
        printf( "preincrement foo\n"); 
        ++x; 
        return *this; 
    };

    foo operator++( int) { 
        printf( "postincrement foo\n"); 
        foo temp( *this);
        ++x;
        return temp; 
    };

};


int main()
{
    foo bar;

    printf( "\n" "preinc example: \n");
    ++bar;

    printf( "\n" "postinc example: \n");
    bar++;
}

优化构建的结果(实际上由于RVO而在后增量情况下删除了第二个副本操作):

construct foo()

preinc example: 
preincrement foo

postinc example: 
postincrement foo
copy foo()

一般来说,如果你不需要后增量的语义,为什么要冒这个不必要的副本呢?

当然,记住自定义操作符++() - 前置或后置变量 - 可以随意返回它想要的任何内容(甚至可以做任何想做的事情),这是很好的,我想有的不少人不按照通常的规则行事。偶尔我会遇到返回“void”的实现,这会使通常的语义差异消失。

答案 1 :(得分:8)

你不会看到整数有任何区别。你需要使用迭代器或post和prefix真正做不同的东西。你需要将所有优化打开,而不是关闭!

答案 2 :(得分:5)

我喜欢遵循“说出你的意思”的规则。

++i只是递增。 i++增量具有特殊的,非直观的评估结果。如果我明确地想要这种行为,我只使用i++,而在所有其他情况下使用++i。如果您遵循这种做法,当您在代码中看到i++时,显然确实存在增量后行为。

答案 3 :(得分:4)

有几点:

  • 首先,你不可能以任何方式看到重大的性能差异
  • 其次,如果您禁用了优化,那么您的基准测试将毫无用处。我们想知道的是,如果这种改变给我们提供了更高效或更低效的代码,这意味着我们必须使用编译器能够生成的最有效代码。我们不关心在未经优化的构建中它是否更快,我们需要知道它在优化的构建中是否更快。
  • 对于像整数这样的内置数据类型,编译器通常能够优化差异。问题主要发生在具有重载增量迭代器的更复杂类型上,其中编译器不能轻易地看到这两个操作在上下文中是等效的。
  • 您应该使用最清晰表达您意图的代码。你想“在值中添加一个”,或者“在值中加一个,但继续处理原始值”?通常,前者是这种情况,然后预增量更能表达您的意图。

如果你想显示差异,最简单的选择就是强制两个操作员,并指出一个需要额外的副本,另一个不需要。

答案 4 :(得分:0)

尝试使用while或使用返回值执行某些操作,例如:

#define SOME_BIG_CONSTANT 1000000000

int _tmain(int argc, _TCHAR* argv[])
{
    int i = 1;
    int d = 0;

    DWORD d1 = GetTickCount();
    while(i < SOME_BIG_CONSTANT + 1)
    {
        d += i++;
    }
    DWORD t1 = GetTickCount() - d1;

    printf("%d", d);
    printf("\ni++ > %d <\n", t1);

    i = 0;
    d = 0;

    d1 = GetTickCount();
    while(i < SOME_BIG_CONSTANT)
    {
        d += ++i;

    }
    t1 = GetTickCount() - d1;

    printf("%d", d);
    printf("\n++i > %d <\n", t1);

    return 0;
}

使用/ O2或/ Ox编译VS 2005,在我的桌面和笔记本电脑上试用。

在笔记本电脑上稳定地获取一些东西,桌面数字有点不同(但速度大致相同):

i++ > 8xx < 
++i > 6xx <

xx表示数字不同,例如813 vs 640 - 仍然加速20%左右。

还有一点 - 如果用“d =”替换“d + =”,你会看到很好的优化技巧:

i++ > 935 <
++i > 0 <

但是,它非常具体。但毕竟,我认为没有任何改变主意的理由并认为没有区别:)

答案 5 :(得分:0)

此代码及其注释应证明两者之间的差异。

class a {
    int index;
    some_ridiculously_big_type big;

    //etc...

};

// prefix ++a
void operator++ (a& _a) {
    ++_a.index
}

// postfix a++
void operator++ (a& _a, int b) {
    _a.index++;
}

// now the program
int main (void) {
    a my_a;

    // prefix:
    // 1. updates my_a.index
    // 2. copies my_a.index to b
    int b = (++my_a).index; 

    // postfix
    // 1. creates a copy of my_a, including the *big* member.
    // 2. updates my_a.index
    // 3. copies index out of the **copy** of my_a that was created in step 1
    int c = (my_a++).index; 
}

您可以看到后缀有一个额外的步骤(步骤1),其中涉及创建对象的副本。这对内存消耗和运行时都有影响。 就是为什么前缀比非基本类型的后缀更有效。

根据some_ridiculously_big_type以及您对增量结果所做的任何事情,您将能够看到有或没有优化的区别。

答案 6 :(得分:0)

对Mihail的回应,他的代码是一个更便携的版本:

#include <cstdio>
#include <ctime>
using namespace std;

#define SOME_BIG_CONSTANT 100000000
#define OUTER 40
int main( int argc, char * argv[] ) {

    int d = 0;
    time_t now = time(0);
    if ( argc == 1 ) {
        for ( int n = 0; n < OUTER; n++ ) {
            int i = 0;
            while(i < SOME_BIG_CONSTANT) {
                d += i++;
            }
        }
    }
    else {
        for ( int n = 0; n < OUTER; n++ ) {
            int i = 0;
            while(i < SOME_BIG_CONSTANT) {
                d += ++i;
            }
        }
    }
    int t = time(0) - now;  
    printf( "%d\n", t );
    return d % 2;
}

外圈让我可以在我的平台上调整合适的时间。

我不再使用VC ++,所以我用(

)编译它(在Windows上)
g++ -O3 t.cpp

然后我通过交替运行它:

a.exe   

a.exe 1

我的时间结果两种情况大致相同。有时一个版本的速度最快可达20%,有时则更快。我猜这是由于我的系统上运行的其他进程。

答案 7 :(得分:0)

也许您可以通过用x86汇编指令写出两个版本来显示理论差异?正如许多人之前指出的那样,编译器总是会自己决定如何最好地编译/组装程序。

如果该示例适用于不熟悉x86指令集的学生,则可以考虑使用MIPS32指令集 - 由于某些奇怪的原因,许多人似乎发现它比x86汇编更易于理解。

答案 8 :(得分:-4)

好的,所有这些前缀/后缀“优化”只是......一些很大的误解。

i ++返回其原始副本的主要思想因此需要复制该值。

对于迭代器的一些低效实现,这可能是正确的。然而,在99%的情况下,即使使用STL迭代器也没有区别,因为编译器知道如何优化它,而实际的迭代器只是看起来像类的指针。当然,对于像指针上的整数这样的原始类型没有区别。

所以......忘掉它。

编辑:澄清

正如我所提到的,大多数 STL迭代器类只是用类包裹的指针,所有成员函数内联允许out-优化这种无关的副本。

是的,如果你有自己的没有内联成员函数的迭代器,那么它可能 工作慢。但是,你应该只了解编译器做什么和不做什么。

作为一个小证明,请使用此代码:

int sum1(vector<int> const &v)
{
    int n;
    for(auto x=v.begin();x!=v.end();x++)
            n+=*x;
    return n;
}

int sum2(vector<int> const &v)
{
    int n;
    for(auto x=v.begin();x!=v.end();++x)
            n+=*x;
    return n;
}

int sum3(set<int> const &v)
{
    int n;
    for(auto x=v.begin();x!=v.end();x++)
            n+=*x;
    return n;
}

int sum4(set<int> const &v)
{
    int n;
    for(auto x=v.begin();x!=v.end();++x)
            n+=*x;
    return n;
}

将其编译为汇编并比较sum1和sum2,sum3和sum4 ......

我可以告诉你... gcc使用-02提供完全相同的代码。