16

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

理论上这是有道理的:i++ 需要能够返回未递增的原始值并因此存储它,而 ++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),我应该看到展示概念的不同之处。我错过了什么?有更好的例子来说明这一点吗?

4

9 回答 9

24

一般情况下,后增量将产生一个前增量不会的副本。当然,这将在大量情况下被优化掉,并且在不是复制操作的情况下可以忽略不计(即,对于内置类型)。

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

#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()

一般来说,如果您不需要后增量的语义,为什么要冒险发生不必要的副本呢?

当然,最好记住自定义 operator++() - 无论是前变体还是后变体 - 可以自由地返回它想要的任何东西(甚至做它想做的任何事情),我想有很多不遵循通常的规则。偶尔我会遇到返回“ void”的实现,这使得通常的语义差异消失了。

于 2009-07-12T20:28:10.343 回答
8

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

于 2009-07-12T19:45:30.257 回答
5

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

++i只是增加。i++增量具有特殊的、非直观的评估结果。我仅i++在明确需要该行为时使用,并++i在所有其他情况下使用。如果您遵循这种做法,当您确实i++在代码中看到时,很明显后增量行为确实是有意的。

于 2009-07-12T19:56:59.797 回答
4

几点:

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

如果要显示差异,最简单的选择是简单地实现两个运算符,并指出一个需要额外的副本,另一个不需要。

于 2009-07-12T20:05:59.997 回答
0

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

如果该示例适用于不熟悉 x86 指令集的学生,您可能会考虑使用 MIPS32 指令集——出于某种奇怪的原因,许多人似乎发现它比 x86 汇编更容易理解。

于 2009-07-13T09:09:21.410 回答
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%,有时另一个版本会快 20%。我猜这是由于我的系统上运行的其他进程。

于 2009-07-12T22:19:39.617 回答
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 与 640 - 仍然加快 20% 左右。

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

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

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

于 2009-07-12T19:44:59.180 回答
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并且还取决于您对增量结果所做的任何事情,您将能够看到有无优化的差异。

于 2009-07-12T20:05:02.620 回答
-4

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

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

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

所以……算了。

编辑:澄清

正如我所提到的,大多数STL 迭代器类只是用类包装的指针,它们具有内联的所有成员函数,允许对这种不相关的副本进行优化。

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

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

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.

于 2009-07-12T19:45:13.377 回答