16

私は例によって、接頭辞の増分が接尾辞の増分よりも効率的であることを示しようとしています。

理論的には、これは理にかなっています。i++は、インクリメントされていない元の値を返すことができる必要があるため、それを格納できますが、++ 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を使用)、概念を示すために違いが見られるはずです。私は何が欠けていますか?これを示すためのより良い例はありますか?

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が原因で、ポストインクリメントの場合に2番目のコピー操作が実際に削除されます):

construct foo()

preinc example: 
preincrement foo

postinc example: 
postincrement foo
copy foo()

一般に、ポストインクリメントのセマンティクスが必要ない場合、なぜ不要なコピーが発生する可能性があるのでしょうか。

もちろん、カスタムoperator ++()(preまたはpostバリアントのいずれか)は、必要なものを自由に返すことができる(または必要なことを実行する)ことを覚えておくとよいでしょう。かなりの数があると思います。通常のルールに従わない。時折、 ""を返す実装に出くわしました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

いくつかのポイント:

  • まず、パフォーマンスに大きな違いが見られる可能性はほとんどありません。
  • 次に、最適化を無効にしている場合、ベンチマークは役に立ちません。私たちが知りたいのは、この変更によって多かれ少なかれ効率的なコードが得られるかどうかです。つまり、コンパイラーが生成できる最も効率的なコードでそれを使用する必要があります。最適化されていないビルドで高速かどうかは関係ありません。最適化されたビルドで高速かどうかを知る必要があります。
  • 整数などの組み込みデータ型の場合、コンパイラーは通常、違いを最適化することができます。この問題は主に、オーバーロードされたインクリメントイテレータを使用するより複雑なタイプで発生します。コンパイラは、2つの操作がコンテキストで同等であることを簡単に確認できません。
  • 意図を最も明確に表現するコードを使用する必要があります。「値に1を追加」しますか、それとも「値に1を追加しますが、元の値をもう少し長く作業し続けます」ですか?通常、前者が当てはまり、プレインクリメントはあなたの意図をよりよく表現します。

違いを示したい場合、最も簡単なオプションは、両方の演算子を強制することであり、一方には追加のコピーが必要であり、もう一方には必要ないことを指摘します。

于 2009-07-12T20:05:59.997 に答える
0

このコードとそのコメントは、2つの違いを示しているはずです。

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 に答える
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を使用してVS2005でコンパイルし、デスクトップとラップトップで試してみました。

ラップトップでは安定して何かを手に入れましょう。デスクトップでは数字が少し異なります(ただし、レートはほぼ同じです)。

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

xxは、数値が異なることを意味します。たとえば、813と640です。それでも約20%高速化されます。

そしてもう1つのポイント-"d+="を"d="に置き換えると、最適化のトリックがわかります。

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

ただし、それは非常に具体的です。しかし、結局のところ、私は私の考えを変える理由は見当たらず、違いはないと思います:)

于 2009-07-12T19:44:59.180 に答える
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%速くなることもあれば、もう一方のバージョンが速くなることもあります。これは、私のシステムで実行されている他のプロセスが原因だと思います。

于 2009-07-12T22:19:39.617 に答える
0

おそらく、x86アセンブリ命令を使用して両方のバージョンを書き出すことで、理論上の違いを示すことができますか?多くの人が以前に指摘したように、コンパイラは常にプログラムをコンパイル/アセンブルするための最良の方法について独自の決定を下します。

この例がx86命令セットに精通していない学生を対象としている場合は、MIPS32命令セットの使用を検討してください。奇妙な理由で、多くの人がx86アセンブリよりも理解しやすいと感じているようです。

于 2009-07-13T09:09:21.410 に答える
-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 に答える