代码之家  ›  专栏  ›  技术社区  ›  cschol

我的效率比我低,怎么显示这个?

  •  15
  • cschol  · 技术社区  · 15 年前

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

    从理论上讲,这是有意义的:i++需要能够返回未经修改的原始值,因此存储它,而++我可以返回递增的值,而不存储以前的值。

    但在实践中是否有一个很好的例子来证明这一点?

    我尝试了以下代码:

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

    我使用GCC4.4.0编译它,如下所示:

    gcc -Wa,-adhls -O0 myfile.cpp
    

    我又这样做了,后缀增量改为前缀增量:

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

    在这两种情况下,结果都是相同的程序集代码。

    这有点出乎意料。通过关闭优化(使用-o0),我应该看到展示这个概念的不同之处。我错过了什么?有没有更好的例子来说明这一点?

    9 回复  |  直到 12 年前
        1
  •  23
  •   Michael Burr    15 年前

    一般的 在这种情况下,后增量将导致复制,而预增量不会。当然,这将在大量情况下被优化掉,在没有优化的情况下,复制操作可以忽略不计(例如,对于内置类型)。

    下面是一个小例子,展示了后增量的潜在低效性。

    #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 “,这使得通常的语义差异消失了。

        2
  •  8
  •   anon    15 年前

    你不会看到整数有什么不同。您需要使用迭代器,或者在post和prefix确实有不同之处的地方使用迭代器。你需要改变所有的优化 ,不要关机!

        3
  •  5
  •   Jason C    15 年前

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

    ++i 简单地递增。 i++ 增量 具有特殊的、非直观的评估结果。我只使用 i++ 如果我明确想要这种行为,并使用 +i 在所有其他情况下。如果你遵循这个惯例,当你看到 i++ 在代码中,很明显后增量行为是有意的。

        4
  •  4
  •   Stack Overflow is garbage    15 年前

    几点:

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

    如果您想显示区别,最简单的选择就是简单地导入两个操作符,并指出一个需要额外的副本,另一个不需要。

        5
  •  0
  •   Nathan Fellman    15 年前

    这段代码及其注释应该说明两者之间的区别。

    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
  •   anon    15 年前

    作为对Mihail的回应,这是一个更为便携的版本HIS代码:

    #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
  •   Mikhail Churbanov    15 年前

    尝试使用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 <
    

    不过,这很具体。但毕竟,我看不出有任何理由改变我的想法,认为没有什么区别:)

        8
  •  0
  •   Ice    15 年前

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

    如果这个例子是针对不熟悉x86指令集的学生的,那么您可以考虑使用mips32指令集——出于一些奇怪的原因,许多人认为它比x86程序集更容易理解。

        9
  •  -4
  •   Artyom    15 年前

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

    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 .