391

CとCの間にパフォーマンスの違いがあるのか​​という疑問がありますi++++i

C ++の答えは何ですか?

4

19 に答える 19

470

[エグゼクティブサマリー:使用++iする特別な理由がない場合に使用しますi++。]

C ++の場合、答えはもう少し複雑です。

iが単純な型(C ++クラスのインスタンスではない)の場合、コンパイラがコードを生成しているため、C(「パフォーマンスの違いはありません」)に対して与えられた答えが成り立ちます。

ただし、iがC ++クラスのインスタンスである場合、i++および++iは関数の1つを呼び出していoperator++ます。これらの関数の標準的なペアは次のとおりです。

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

コンパイラはコードを生成せず、関数を呼び出すだけなので、変数とそれに関連するコピーコンストラクタoperator++を最適化する方法はありません。tmpコピーコンストラクターが高価な場合、これはパフォーマンスに大きな影響を与える可能性があります。

于 2008-08-24T07:23:13.033 に答える
70

はい。がある。

++演算子は、関数として定義されている場合とされていない場合があります。プリミティブ型(int、double、...)の場合、演算子が組み込まれているため、コンパイラーはおそらくコードを最適化できます。ただし、++演算子を定義するオブジェクトの場合は、状況が異なります。

operator ++(int)関数はコピーを作成する必要があります。これは、postfix ++が保持している値とは異なる値を返すことが期待されるためです。つまり、一時変数に値を保持し、値をインクリメントして、一時変数を返す必要があります。operator ++()、プレフィックス++の場合、コピーを作成する必要はありません。オブジェクトはそれ自体をインクリメントしてから、単にそれ自体を返すことができます。

ポイントの図は次のとおりです。

struct C
{
    C& operator++();      // prefix
    C  operator++(int);   // postfix

private:

    int i_;
};

C& C::operator++()
{
    ++i_;
    return *this;   // self, no copy created
}

C C::operator++(int ignored_dummy_value)
{
    C t(*this);
    ++(*this);
    return t;   // return a copy
}

operator ++(int)を呼び出すたびにコピーを作成する必要があり、コンパイラーはそれについて何もできません。選択肢が与えられたら、operator ++();を使用します。このように、コピーを保存しません。多くの増分(大きなループ?)および/または大きなオブジェクトの場合に重要になる可能性があります。

于 2008-08-24T07:44:11.017 に答える
57

インクリメント演算子が異なる翻訳単位にある場合のベンチマークを次に示します。g++ 4.5 のコンパイラ。

今のところ、スタイルの問題は無視してください

// a.cc
#include <ctime>
#include <array>
class Something {
public:
    Something& operator++();
    Something operator++(int);
private:
    std::array<int,PACKET_SIZE> data;
};

int main () {
    Something s;

    for (int i=0; i<1024*1024*30; ++i) ++s; // warm up
    std::clock_t a = clock();
    for (int i=0; i<1024*1024*30; ++i) ++s;
    a = clock() - a;

    for (int i=0; i<1024*1024*30; ++i) s++; // warm up
    std::clock_t b = clock();
    for (int i=0; i<1024*1024*30; ++i) s++;
    b = clock() - b;

    std::cout << "a=" << (a/double(CLOCKS_PER_SEC))
              << ", b=" << (b/double(CLOCKS_PER_SEC)) << '\n';
    return 0;
}

O(n) インクリメント

テスト

// b.cc
#include <array>
class Something {
public:
    Something& operator++();
    Something operator++(int);
private:
    std::array<int,PACKET_SIZE> data;
};


Something& Something::operator++()
{
    for (auto it=data.begin(), end=data.end(); it!=end; ++it)
        ++*it;
    return *this;
}

Something Something::operator++(int)
{
    Something ret = *this;
    ++*this;
    return ret;
}

結果

仮想マシンで g++ 4.5 を使用した結果 (タイミングは秒単位):

Flags (--std=c++0x)       ++i   i++
-DPACKET_SIZE=50 -O1      1.70  2.39
-DPACKET_SIZE=50 -O3      0.59  1.00
-DPACKET_SIZE=500 -O1    10.51 13.28
-DPACKET_SIZE=500 -O3     4.28  6.82

O(1) インクリメント

テスト

次のファイルを見てみましょう。

// c.cc
#include <array>
class Something {
public:
    Something& operator++();
    Something operator++(int);
private:
    std::array<int,PACKET_SIZE> data;
};


Something& Something::operator++()
{
    return *this;
}

Something Something::operator++(int)
{
    Something ret = *this;
    ++*this;
    return ret;
}

インクリメントでは何もしません。これは、増分が一定の複雑さを持つ場合をシミュレートします。

結果

結果は大きく異なります。

Flags (--std=c++0x)       ++i   i++
-DPACKET_SIZE=50 -O1      0.05   0.74
-DPACKET_SIZE=50 -O3      0.08   0.97
-DPACKET_SIZE=500 -O1     0.05   2.79
-DPACKET_SIZE=500 -O3     0.08   2.18
-DPACKET_SIZE=5000 -O3    0.07  21.90

結論

パフォーマンス面

前の値が必要ない場合は、プレインクリメントを使用する習慣をつけてください。組み込み型であっても一貫性を保つようにしてください。慣れると、組み込み型をカスタム型に置き換えた場合に不必要にパフォーマンスが低下するリスクがなくなります。

意味的に

  • i++と言いincrement i, I am interested in the previous value, thoughます。
  • ++iincrement i, I am interested in the current valueまたはと言いincrement i, no interest in the previous valueます。繰り返しになりますが、今は慣れていなくても、慣れるでしょう。

クヌート。

時期尚早の最適化は諸悪の根源です。時期尚早の悲観化も同様です。

于 2012-03-01T15:38:23.530 に答える
22

後置の場合、コンパイラが一時変数のコピーを最適化できないというのは完全に正しいとは言えません。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)] 

したがって、後置バージョンの方が遅くなる可能性があるのは確かですが、使用していない場合は、オプティマイザーが一時コピーを取り除くのに十分である可能性があります.

于 2008-08-24T14:41:21.530 に答える
21

Google C++ スタイル ガイドには次のように書かれています。

プリインクリメントとプリデクリメント

インクリメント演算子とデクリメント演算子のプレフィックス形式 (++i) を反復子やその他のテンプレート オブジェクトと共に使用します。

定義:変数がインクリメント (++i または i++) またはデクリメント (--i または i--) され、式の値が使用されない場合、プレインクリメント (デクリメント) またはポストインクリメント (デクリメント) を決定する必要があります。

長所:戻り値が無視される場合、「前」形式 (++i) は「後」形式 (i++) よりも効率が低くなることはなく、多くの場合より効率的です。これは、ポスト インクリメント (またはデクリメント) では、式の値である i のコピーを作成する必要があるためです。i が反復子またはその他の非スカラー型である場合、i をコピーするとコストが高くなる可能性があります。値が無視された場合、2 種類のインクリメントは同じように動作するため、常にプリインクリメントしないのはなぜでしょうか?

短所:特に for ループで、式の値が使用されていない場合に後置インクリメントを使用するという C での伝統が発展しました。英語と同じように、「主語」(i) が「動詞」(++) の前にあるため、後置インクリメントの方が読みやすいと考える人もいます。

決定事項:単純なスカラー (非オブジェクト) 値の場合、1 つの形式を優先する理由はなく、いずれかを許可します。イテレータやその他のテンプレート タイプの場合は、プレインクリメントを使用します。

于 2008-08-29T19:18:21.700 に答える
5

@ケタン

...意図とパフォーマンスに関する見過ごされがちな詳細を取り上げます。++iter の代わりに iter++ を使用したい場合があります。

ポストインクリメントとプリインクリメントはセマンティクスが異なることは明らかであり、結果を使用する場合は適切な演算子を使用する必要があることに誰もが同意するはずです。for問題は、(ループのように)結果が破棄されたときに何をすべきかだと思います。この質問 (IMHO)に対する答えは、パフォーマンスに関する考慮事項はせいぜい無視できるので、より自然なことを行うべきだということです。私自身++iはより自然ですが、私の経験では、私は少数派であり、使用すると、コードを読むほとんどのi++人にとって金属のオーバーヘッドが少なくなります。

結局のところ、それが言語が " ++C" と呼ばれない理由です。[*]

++C[*]より論理的な名前であることについての必須の議論を挿入します。

于 2008-09-03T18:37:26.030 に答える
5

ごく最近、Andrew Koenig による Code Talk での優れた投稿を指摘したいと思います。

http://dobbscodetalk.com/index.php?option=com_myblog&show=Efficiency-versus-intent.html&Itemid=29

当社でも、一貫性とパフォーマンスのために ++iter の規約を適宜使用しています。しかし、Andrew は意図とパフォーマンスに関する見過ごされがちな詳細を挙げています。++iter の代わりに iter++ を使用したい場合があります。

したがって、最初に意図を決定し、事前または事後が重要でない場合は、余分なオブジェクトの作成を回避してスローすることでパフォーマンス上の利点があるため、事前に行ってください。

于 2008-09-03T04:02:31.983 に答える
3

とのパフォーマンスの違いは++ii++演算子を値を返す関数と見なし、その実装方法を考えると、より明確になります。何が起こっているのかを理解しやすくするために、次のコード例intではstruct.

++i変数をインクリメントし、結果返します。これはインプレースで最小限の CPU 時間で実行でき、多くの場合、1 行のコードしか必要としません。

int& int::operator++() { 
     return *this += 1;
}

しかし、同じことは言えませんi++

後置インクリメント は、多くの場合、インクリメントする前i++に元の値を返すと見なされます。ただし、関数は終了時にのみ結果を返すことができます。その結果、元の値を含む変数のコピーを作成し、変数をインクリメントしてから、元の値を保持するコピーを返す必要があります。

int int::operator++(int& _Val) {
    int _Original = _Val;
    _Val += 1;
    return _Original;
}

プリインクリメントとポストインクリメントに機能上の違いがない場合、コンパイラは、両者の間にパフォーマンスの違いがないように最適化を実行できます。structただし、 orなどの複合データ型classが含まれる場合、コピー コンストラクターはポスト インクリメントで呼び出され、ディープ コピーが必要な場合にこの最適化を実行することはできません。そのため、事前インクリメントは一般に、事後インクリメントよりも高速であり、必要なメモリも少なくて済みます。

于 2012-11-06T18:19:06.363 に答える
3

Mark: 演算子 ++ はインライン化の良い候補であり、コンパイラがそうすることを選択した場合、冗長なコピーはほとんどの場合に排除されることを指摘したかっただけです。(例えば、通常イテレータである POD タイプ。)

とはいえ、ほとんどの場合、++iter を使用する方がより良いスタイルです。:-)

于 2008-09-16T11:21:27.013 に答える
2

@Mark:以前の回答は少しひっくり返ったため削除し、それだけで反対票を投じるに値しました。実際、多くの人が何を考えているかを問うという意味で、これは良い質問だと思います。

通常の答えは、++i は i++ よりも速いというものであり、間違いなくそうですが、より大きな問題は、「いつ気にする必要があるのか​​?」ということです。

イテレータのインクリメントに費やされる CPU 時間の割合が 10% 未満であれば、気にする必要はありません。

反復子のインクリメントに費やされた CPU 時間の割合が 10% を超えている場合は、どのステートメントがその反復を行っているかを確認できます。イテレータを使用するのではなく、整数をインクリメントできるかどうかを確認してください。可能性はありますし、ある意味ではあまり望ましくないかもしれませんが、これらの反復子で費やされるすべての時間を本質的に節約できる可能性はかなり高いです。

イテレータのインクリメントが 90% 以上の時間を消費している例を見てきました。その場合、整数インクリメントに行くと、本質的にその量だけ実行時間が短縮されました。(つまり、10 倍以上の高速化)

于 2009-08-23T19:04:56.670 に答える
1

@wilhelmtell

コンパイラは一時的なものを削除できます。他のスレッドからの逐語的:

C ++コンパイラでは、プログラムの動作が変更された場合でも、スタックベースの一時的なものを排除できます。VC 8のMSDNリンク:

http://msdn.microsoft.com/en-us/library/ms364057(VS.80).aspx

于 2008-08-27T18:03:38.837 に答える
0

パフォーマンス上の利点がない組み込み型でも++iを使用する必要がある理由は、自分自身に良い習慣を身に付けるためです。

于 2008-08-25T04:52:45.850 に答える
0

意図した質問は、結果が未使用の場合に関するものでした (これは、C の質問から明らかです)。質問は「コミュニティwiki」なので、誰かがこれを修正できますか?

時期尚早の最適化については、Knuth がよく引用されます。それは正しい。しかし、ドナルド・クヌースは、最近目にする恐ろしいコードをそれで擁護することは決してありません。Java 整数 (int ではない) の中で a = b + c を見たことがありますか? これは、3 回のボックス化/ボックス化解除のコンバージョンに相当します。そのようなものを避けることが重要です。そして、無駄に ++i の代わりに i++ を書くのも同じ間違いです。編集: phresnel がコメントでうまく表現しているように、これは「時期尚早の悲観化と同様に、時期尚早の最適化は悪である」と要約できます。

人々がより i++ に慣れているという事実でさえ、K&R による概念上の誤りによって引き起こされた不幸な C の遺産です (意図的な議論に従えば、それは論理的な結論です。すばらしいですが、言語設計者としては優れていません; C 設計には、gets() から strcpy()、strncpy() API (初日から strlcpy() API があったはずです) に至るまで、数え切れないほどの間違いが存在します。 )。

ところで、私は ++i を読むのが面倒だと感じるほど C++ に慣れていない人の 1 人です。それでも、それが正しいと認めているので、私はそれを使用します。

于 2009-01-15T00:54:24.547 に答える
0

どちらも同じくらい高速です ;) プロセッサで同じ計算をしたい場合は、実行される順序が異なるだけです。

たとえば、次のコード:

#include <stdio.h>

int main()
{
    int a = 0;
    a++;
    int b = 0;
    ++b;
    return 0;
}

次のアセンブリを作成します。

 0x0000000100000f24 <main+0>: push   %rbp
 0x0000000100000f25 <main+1>: mov    %rsp,%rbp
 0x0000000100000f28 <main+4>: movl   $0x0,-0x4(%rbp)
 0x0000000100000f2f <main+11>:    incl   -0x4(%rbp)
 0x0000000100000f32 <main+14>:    movl   $0x0,-0x8(%rbp)
 0x0000000100000f39 <main+21>:    incl   -0x8(%rbp)
 0x0000000100000f3c <main+24>:    mov    $0x0,%eax
 0x0000000100000f41 <main+29>:    leaveq 
 0x0000000100000f42 <main+30>:    retq

a++ と b++ の場合、これは incl ニーモニックであるため、同じ操作であることがわかります ;)

于 2011-04-11T05:09:00.753 に答える
-2

人々に知恵の宝石を提供する時が来ました ;) - C++ の接尾辞のインクリメントを接頭辞のインクリメントとほとんど同じように動作させる簡単なトリックがあります (これは自分で発明しましたが、他の人のコードでも同様に見られたので、私は知りません1人)。

基本的には、ヘルパー クラスを使用して復帰後のインクリメントを延期するのがコツであり、RAII が助けに来ます

#include <iostream>

class Data {
    private: class DataIncrementer {
        private: Data& _dref;

        public: DataIncrementer(Data& d) : _dref(d) {}

        public: ~DataIncrementer() {
            ++_dref;
        }
    };

    private: int _data;

    public: Data() : _data{0} {}

    public: Data(int d) : _data{d} {}

    public: Data(const Data& d) : _data{ d._data } {}

    public: Data& operator=(const Data& d) {
        _data = d._data;
        return *this;
    }

    public: ~Data() {}

    public: Data& operator++() { // prefix
        ++_data;
        return *this;
    }

    public: Data operator++(int) { // postfix
        DataIncrementer t(*this);
        return *this;
    }

    public: operator int() {
        return _data;
    }
};

int
main() {
    Data d(1);

    std::cout <<   d << '\n';
    std::cout << ++d << '\n';
    std::cout <<   d++ << '\n';
    std::cout << d << '\n';

    return 0;
}

Invented は、いくつかの重いカスタム イテレータ コード用であり、実行時間を短縮します。接頭辞と接尾辞のコストは現在 1 つの参照です。これが大量の移動を行うカスタム オペレータである場合、接頭辞と接尾辞は同じ実行時間をもたらしました。

于 2018-03-29T20:48:43.800 に答える