C++ の i++ と ++i の間にパフォーマンスの違いはありますか?
-
09-06-2019 - |
質問
質問があります 間に性能差はありますか i++
そして ++i
Cで?
C++ の答えは何でしょうか?
解決
[エグゼクティブサマリー: i ++
を使用する特別な理由がない場合は、 ++ i
を使用します。]
C ++の場合、答えはもう少し複雑です。
i
が(C ++クラスのインスタンスではなく)単純型である場合、それから与えられた答えCの場合(「パフォーマンスの違いはありません」)は、コンパイラがコードを生成しているためです。
ただし、 i
がC ++クラスのインスタンスである場合、 i ++
および ++ i
は、< code> 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
変数とそれに関連するコピーコンストラクターを最適化する方法はありません。コピーコンストラクタが高価な場合、これはパフォーマンスに大きな影響を与える可能性があります。
他のヒント
はい。あります。
++演算子は、関数として定義されている場合と定義されていない場合があります。プリミティブ型(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 ++();を使用します。この方法では、コピーを保存しません。多くの増分(大きなループ?)および/または大きなオブジェクトの場合に重要になる可能性があります。
以下は、インクリメント演算子が異なる翻訳単位にある場合のベンチマークです。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
.++i
言うincrement i, I am interested in the current value
またはincrement i, no interest in the previous value
. 。繰り返しますが、今はそうでなくても、すぐに慣れます。
クヌート。
時期尚早な最適化は諸悪の根源です。時期尚早の悲観も同様です。
postfixの場合、コンパイラが一時変数のコピーを最適化して削除できないと言うのは完全に正しいわけではありません。 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)]
したがって、確かにpostfixバージョンが遅くなる可能性はありますが、使用していない場合はオプティマイザーが一時コピーを削除するのに十分である可能性があります。
プリインクリメントとプリデクリメント
インクリメント演算子とデクリメント演算子のプレフィックス形式(++ i)を使用します イテレータおよびその他のテンプレートオブジェクト。
定義:変数がインクリメント(++ iまたはi ++)またはデクリメント(--iまたは i--)式の値が使用されていない場合、決定する必要があります プレインクリメント(デクリメント)またはポストインクリメント(デクリメント)するかどうか。
長所:戻り値が無視されると、&quot; pre&quot;フォーム(++ i)は決して小さくない 「投稿」よりも効率的フォーム(i ++)であり、多くの場合、より効率的です。 これは、ポストインクリメント(またはデクリメント)にiのコピーが必要なためです 行われます。これは式の値です。 iが反復子である場合、または 他の非スカラー型、コピーiは高価になる可能性があります。 2つ以来 値が無視される場合、増分のタイプは同じように動作します。 常に常に事前インクリメントしますか?
短所: Cで開発された伝統は、 特にforループでは、式の値は使用されません。いくつか見つける 「件名」はポストインクリメントなので読みやすい(i)に先行する 「動詞」 (++)、英語のように。
決定:単純なスカラー(非オブジェクト)値の場合、優先する理由はありません フォームといずれかを許可します。イテレータおよびその他のテンプレートタイプには、 プリインクリメント。
ごく最近、Code Talkに関するAndrew Koenigの素晴らしい投稿を指摘したいと思います。
当社では、++ iterの規則を使用して、必要に応じて一貫性とパフォーマンスを実現しています。しかし、アンドリューは意図とパフォーマンスに関する見落とされがちな詳細を提起します。 ++ iterの代わりにiter ++を使用したい場合があります。
したがって、最初に意図を決定し、事前または事柄が重要でない場合は、余分なオブジェクトの作成を回避してスローすることでパフォーマンス上のメリットがあるため、事前に進みます。
@Ketan
...意図とパフォーマンスに関する見落とされがちな詳細を上げます。 ++ iterの代わりにiter ++を使用したい場合があります。
ポストとプリインクリメントのセマンティクスが異なることは明らかです。結果を使用する場合は適切な演算子を使用する必要があることは誰もが同意するはずです。問題は( for
ループのように)結果が破棄されたときに何をすべきかだと思います。 this の質問(IMHO)に対する答えは、パフォーマンスに関する考慮事項はせいぜい無視できるので、より自然なことをすべきだということです。自分にとっては ++ i
はより自然ですが、私の経験では、私は少数派であり、 i ++
を使用すると most あなたのコードを読んでいる人。
結局のところ、言語が&quot; ++ C
&quot;と呼ばれない理由です。[*]
[*] ++ C
がより論理的な名前であるという義務的な議論を挿入します。
マーク:operator ++はインライン化するのに適した候補であり、コンパイラーがインライン化を選択した場合、ほとんどの場合、冗長なコピーが削除されることを指摘したかっただけです。 (たとえば、通常は反復子であるPODタイプ)
とはいえ、ほとんどの場合は++ iterを使用する方がより良いスタイルです。 :-)
++ i
と i ++
のパフォーマンスの違いは、演算子を値を返す関数とその実装方法と考えると、より明確になります。何が起こっているのかを理解しやすくするために、次のコード例では struct
であるかのように int
を使用します。
++ i
は変数をインクリメントし、 then は結果を返します。これはインプレースで最小限のCPU時間で実行でき、多くの場合、必要なコードは1行のみです。
int& int::operator++() {
return *this += 1;
}
しかし、 i ++
についても同じことが言えません。
ポストインクリメント i ++
は、インクリメントする前に 元の値を返すとよく見られます。ただし、関数は、終了した場合にのみ結果を返すことができます。その結果、元の値を含む変数のコピーを作成し、変数をインクリメントし、元の値を保持しているコピーを返すことが必要になります。
int int::operator++(int& _Val) {
int _Original = _Val;
_Val += 1;
return _Original;
}
プリインクリメントとポストインクリメントの間に機能的な違いがない場合、コンパイラは最適化を実行して、2つのパフォーマンスの違いがないようにすることができます。ただし、 struct
や class
などの複合データ型が関係する場合、コピーコンストラクターはポストインクリメントで呼び出され、これを実行することはできません。ディープコピーが必要な場合の最適化。そのため、プリインクリメントは一般に高速であり、ポストインクリメントよりも少ないメモリで済みます。
- ++ i -戻り値を使用しない使用しない
- i ++ -戻り値を使用する使用
戻り値を使用しない場合、コンパイラは ++ i の場合に一時を使用しないことが保証されます。速くなることは保証されませんが、遅くならないことが保証されます。
使用すると、戻り値 i ++ により、プロセッサは両方の 互いに依存していないため、インクリメントとパイプラインへの左側。 ++ iはパイプラインを停止させる可能性があります。これは、プリインクリメント操作が完全に蛇行するまでプロセッサが左側を開始できないためです。繰り返しになりますが、パイプラインのストールは保証されていません。プロセッサが他の便利なものを使い続ける可能性があるためです。
パフォーマンス上の利点がない組み込み型でも++ iを使用する必要がある理由は、自分自身に良い習慣を付けるためです。
@マーク:以前の回答は少し反転したものであり、それだけで反対票を投じるに値するため、削除しました。実際、多くの人が何を考えているかを問うという意味では、これは良い質問だと思います。
通常の答えは、++i は i++ よりも高速であるということであり、それは間違いありません。しかし、より大きな問題は、「いつ気にする必要があるのか?」ということです。
イテレータのインクリメントに費やされる CPU 時間の割合が 10% 未満であれば、気にする必要はありません。
反復子のインクリメントに費やされる CPU 時間の割合が 10% を超える場合は、どのステートメントがその反復を行っているかを確認できます。イテレータを使用せずに整数をインクリメントできるかどうかを確認してください。可能性はありますし、ある意味ではあまり望ましくないかもしれませんが、基本的にこれらのイテレータに費やされるすべての時間を節約できる可能性はかなり高くなります。
イテレータのインクリメントに時間の 90% 以上が費やされている例を見てきました。その場合、整数インクリメントを行うと、実質的にその分だけ実行時間が短縮されます。(すなわち、10 倍以上の高速化)
意図した質問は、結果がいつ使用されないかについてでした(Cの質問から明らかです)。質問は「コミュニティwiki」なので、誰かがこれを修正できますか?
時期尚早な最適化については、クヌースがよく引用されます。そのとおり。しかし、ドナルド・クヌースは、あなたが最近見ることができる恐ろしいコードを決して擁護しませんでした。 Java整数(intではない)でa = b + cを見たことはありますか?それは3つのボクシング/アンボクシングコンバージョンに相当します。そのようなものを避けることは重要です。そして、++ iの代わりにi ++を無駄に書くことも同じ間違いです。 編集:phresnelがコメントにうまく入れているので、これは「早すぎる悲観化と同様に、早すぎる最適化は悪である」と要約できます。
人々がi ++により慣れているという事実でさえ、K&amp; Rによる概念上の間違いによって引き起こされる不幸なCの遺産です(意図的な議論に従えば、それは論理的な結論です;そして彼らはK&amp; R K&amp; Rは意味がなく、素晴らしいですが、言語デザイナーとしては素晴らしいものではありません.gets()からstrcpy()、strncpy()APIに至るまで、Cデザインには無数の間違いが存在します1日目以降のstrlcpy()API)。
ところで、私は++ iを読むのが面倒だと思うほどC ++に慣れていない人の一人です。それでも、私はそれが正しいことを認めているのでそれを使用します。
@wilhelmtell
コンパイラは一時ファイルを削除できます。他のスレッドからの逐語:
C ++コンパイラは、プログラムの動作を変更したとしても、スタックベースの一時を削除できます。 VC 8のMSDNリンク:
http://msdn.microsoft.com/ en-us / library / ms364057(VS.80).aspx
知恵の宝石を人々に提供する時間;)-C ++ postfix incrementをprefix incrementとほぼ同じように動作させる簡単なトリックがあります(これを自分で発明しましたが、他の人のコードでも見ました)独りじゃない)
基本的には、戻り値の後にインクリメントを延期するためにヘルパークラスを使用するのがコツです。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つの参照であり、これがカスタムオペレータが頻繁に移動する場合、プレフィックスとポストフィックスは同じランタイムをもたらしました。
どちらも高速です;) プロセッサで同じ計算が必要な場合は、実行される順序だけが異なります。
たとえば、次のコード:
#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 <*>x0,-0x4(%rbp) 0x0000000100000f2f <main+11>: incl -0x4(%rbp) 0x0000000100000f32 <main+14>: movl <*>x0,-0x8(%rbp) 0x0000000100000f39 <main+21>: incl -0x8(%rbp) 0x0000000100000f3c <main+24>: mov <*>x0,%eax 0x0000000100000f41 <main+29>: leaveq 0x0000000100000f42 <main+30>: retq
a ++とb ++の場合、これはinclニーモニックであるため、同じ操作です;)
i ++
を記述すると、コンパイラがこの行またはループを終了した後にインクリメントするように指示しています。
++ i
は、 i ++
とは少し異なります。 i ++
では、ループの終了後に増分しますが、 ++ i
はループの終了前に直接増分します。
++ i
は、値の古いコピーを返さないため、 i ++
よりも高速です。
さらに直感的です:
x = i++; // x contains the old value of i
y = ++i; // y contains the new value of i
このCの例は&quot; 02&quot;を出力します。 「12」の代わりにあなたが期待するかもしれない:
#include <stdio.h>
int main(){
int a = 0;
printf("%d", a++);
printf("%d", ++a);
return 0;
}
#include <iostream>
using namespace std;
int main(){
int a = 0;
cout << a++;
cout << ++a;
return 0;
}