私たちは質問をしている間の性能差があるi++
と++i
Cでは?
C ++の答えは何ですか?
私たちは質問をしている間の性能差があるi++
と++i
Cでは?
C ++の答えは何ですか?
回答:
[エグゼクティブサマリー:使用++i
する特別な理由がない場合に使用しますi++
。]
C ++の場合、答えはもう少し複雑です。
場合はi
、単純なタイプ(C ++クラスのインスタンスではない)で、その後、Cのために与えられた答えが(「いいえパフォーマンスの違いはありません」)コンパイラがコードを生成しているため、保持しています。
ただし、i
がC ++クラスのインスタンスである場合、i++
および++i
はoperator++
関数の1つを呼び出しています。これらの関数の標準ペアは次のとおりです。
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 ++()、prefix ++の場合、コピーを作成する必要はありません。オブジェクトは自身をインクリメントしてから、それ自体を返すことができます。
ポイントの説明は次のとおりです。
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 ++();を使用します。この方法では、コピーを保存しません。これは、多くの増分(大きなループ?)や大きなオブジェクトの場合に重要になることがあります。
C t(*this); ++(*this); return t;
2行目では、thisポインターを右にインクリメントしているので、これをインクリメントしている場合はどのようt
に更新されますか。これの値はすでにコピーされていt
ませんか?
The operator++(int) function must create a copy.
いいえそうではありません。これ以上のコピーはありません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;
}
// 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
次のファイルを見てみましょう。
// 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
ます。繰り返しますが、今は慣れていなくても慣れます。時期尚早の最適化は、すべての悪の根源です。時期尚早の悲観論もそうです。
for (it=nearest(ray.origin); it!=end(); ++it) { if (auto i = intersect(ray, *it)) return i; }
実際のツリー構造(BSP、kd、Quadtree、Octree Gridなど)を気にしないように、深さ優先トラバーサルのイテレーターがあります。このようなイテレータは、例えば、いくつかの状態を維持する必要があるだろうparent node
、child node
、index
及びそのようなもの。全体として、私のスタンスは、たとえいくつかの例しか存在しなくても、...
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のバージョンが遅くなる可能性があることは確かですが、オプティマイザが一時的なコピーを使用しない場合はそれを取り除くのに十分な場合もあります。
GoogleのC ++スタイルガイドは言います:
プリインクリメントとプリデクリメント
イテレータや他のテンプレートオブジェクトでは、増分演算子と減分演算子のプレフィックス形式(++ i)を使用します。
定義:変数がインクリメント(++ iまたはi ++)またはデクリメント(--iまたはi--)され、式の値が使用されない場合、プリインクリメント(デクリメント)またはポストインクリメント(デクリメント)のいずれかを決定する必要があります。
長所:戻り値が無視される場合、「前」形式(++ i)は「後」形式(i ++)よりも効率が悪くなることはなく、多くの場合より効率的です。これは、ポストインクリメント(またはデクリメント)では、式の値であるiのコピーを作成する必要があるためです。iがイテレータまたはその他の非スカラー型の場合、iをコピーすると負荷がかかる可能性があります。値が無視された場合、2つのタイプの増分は同じように動作するので、なぜ常に事前増分ではないのですか?
短所: Cでは、式の値が使用されない場合、特にforループでポストインクリメントを使用するという伝統が発達しました。英語のように、「件名」(i)が「動詞」(++)の前にあるため、ポストインクリメントが読みやすくなる人もいます。
決定:単純なスカラー(非オブジェクト)値の場合、1つの形式を選択する理由はなく、どちらも許可します。イテレータおよびその他のテンプレートタイプの場合は、事前増分を使用します。
最近、Andrew KoenigによるCode Talkに関する優れた投稿を指摘したいと思います。
http://dobbscodetalk.com/index.php?option=com_myblog&show=Efficiency-versus-intent.html&Itemid=29
当社では、必要に応じて、一貫性とパフォーマンスのために++ iterの規則を使用しています。しかし、Andrewは意図とパフォーマンスに関して見過ごされている詳細を挙げています。++ iterの代わりにiter ++を使用したい場合があります。
そのため、最初に目的を決定し、事前または事後が問題ではない場合は、事前オブジェクトを作成します。これにより、余分なオブジェクトの作成を避け、それをスローすることでパフォーマンスが向上します。
@ケタン
...意図とパフォーマンスに関する見過ごされている詳細を引き上げます。++ iterの代わりにiter ++を使用したい場合があります。
明らかにpostとpre-incrementのセマンティクスは異なり、結果が使用される場合は適切な演算子を使用する必要があることに誰もが同意するに違いありません。問題は、(for
ループのように)結果が破棄されたときに何をすべきかだと思います。この質問(IMHO)への回答は、パフォーマンスの考慮はせいぜい無視できるため、より自然なことを行う必要があるということです。私自身の++i
ほうが自然ですが、私の経験から、私は少数派であり、使用i++
することで、コードを読むほとんどの人にとってメタルオーバーヘッドが少なくなることがわかります。
結局それが言語が「++C
」と呼ばれない理由です。[*]
[*] ++C
より論理的な名前であることについての強制的な議論を挿入します。
ときに使用していない、戻り値をコンパイラがある場合には一時的に使用しないことが保証されてI ++。高速であることが保証されていませんが、低速ではないことが保証されています。
戻り値を使用する場合、i ++は、プロセッサがインクリメントと左側の両方を互いに依存しないため、パイプラインにプッシュすることを許可します。++ iは、プリインクリメント操作が最後まで蛇行するまでプロセッサが左側を開始できないため、パイプラインを停止させる可能性があります。繰り返しになりますが、プロセッサは他の便利なものを見つける可能性があるため、パイプラインのストールは保証されません。
とのパフォーマンスの違いは++i
、i++
演算子を値を返す関数と見なし、それらを実装する方法を考えると、より明確になります。何が起こっているのかを理解しやすくするために、次のコード例では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;
}
プリインクリメントとポストインクリメントの間に機能的な違いがない場合、コンパイラーは最適化を実行して、2つの間にパフォーマンスの違いがないようにすることができます。ただし、struct
またはなどの複合データ型が含まれている場合class
、コピーコンストラクターはポストインクリメントで呼び出され、ディープコピーが必要な場合にこの最適化を実行することはできません。そのため、前置インクリメントは通常、後置インクリメントよりも高速で、必要なメモリも少なくなります。
@Mark:少しフリップだったので、以前の回答を削除しました。多くの人の頭にあることを問うという意味では、いい質問だと思います。
通常の答えは、++ iはi ++よりも速いということです、そして間違いなくそれは確かですが、より大きな問題は、「いつあなたは気にすべきですか?」
イテレータの増分に費やされたCPU時間の割合が10%未満の場合、気にする必要はありません。
イテレーターの増分に費やされたCPU時間の割合が10%を超える場合、どのステートメントがその反復を行っているかを確認できます。イテレータを使用するのではなく、整数をインクリメントできるかどうかを確認してください。可能性はあります。ある意味では望ましくないかもしれませんが、可能性はかなり高く、基本的にこれらのイテレータで費やされる時間をすべて節約できます。
イテレータの増分が90%以上の時間を消費している例を見ました。その場合、整数でインクリメントすると、実行時間は本質的にその分だけ短縮されます。(すなわち、10倍のスピードアップより良い)
あずきっく
コンパイラーは一時ファイルを省略できます。他のスレッドから逐語的:
C ++コンパイラは、プログラムの動作を変更する場合でも、スタックベースの一時を削除することができます。VC 8のMSDNリンク:
http://msdn.microsoft.com/en-us/library/ms364057(VS.80).aspx
パフォーマンス上の利点がない組み込み型でも++ iを使用する必要がある理由は、自分自身に良い習慣を作るためです。
どちらも同じくらい高速です;)プロセッサーで同じ計算をしたい場合は、実行される順序が異なるだけです。
たとえば、次のコード:
#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 ++の場合はニーモニックが含まれているので、同じ操作です;)
意図した質問は、結果が未使用の場合に関するものでした(Cの質問から明らかです)。質問は「コミュニティーwiki」なので、誰かがこれを修正できますか?
時期尚早の最適化については、Knuthがしばしば引用されます。そのとおり。しかし、ドナルド・クヌースはあなたが最近目にすることができるその恐ろしいコードを決して守らないでしょう。Java Integer(intではない)の間でa = b + cを見たことがありますか?これは、3つのボックス化/ボックス化解除の変換に相当します。そのようなものを避けることは重要です。そして、++ iの代わりにi ++を無駄に書くのも同じ間違いです。編集:フレネルはそれをコメントにうまく入れているので、これは「時期尚早な悲観化と同様に時期尚早な最適化は悪い」と要約することができます。
人々がi ++に慣れているという事実でさえ、K&Rによる概念上の誤りによって引き起こされた、残念なCの遺産です(意図的な議論に従えば、それは論理的な結論です。K&RであるK&Rを守ることは無意味なので、すばらしいですが、言語デザイナーとしてはすばらしいものではありません。gets()からstrcpy()からstrncpy()APIまで、Cデザインには無数の間違いがあります(1日目からstrlcpy()APIがあったはずです)。 )。
ところで、私はC ++が++ iを読むのが面倒だと気づくのに十分に慣れていない人の一人です。それでも、私はそれが正しいことを認めているので、それを使用します。
++i
よりもいらいらすることはありませんでしたi++
が、あなたの投稿の残りの部分は私の完全な承認を得ています。「時期尚早の悲観化と同様に時期尚早の最適化は悪である」というポイントを追加するかもしれません
strncpy
彼らが当時使用していたファイルシステムで目的を果たした。ファイル名は8文字のバッファであり、ヌル文字で終了する必要はありませんでした。言語進化の未来への40年を見ていないためにそれらを非難することはできません。
strlcpy()
まだ発明されていないという事実によって正当化されました。
人々に知恵の宝石を提供する時間です;)-C ++のポストフィックスのインクリメントをプレフィックスのインクリメントとほぼ同じように動作させる簡単なトリックがあります(これを自分で発明しましたが、他の人のコードでも見たので、私はそうではありません一人)。
基本的に、トリックはヘルパークラスを使用して戻り後のインクリメントを延期することであり、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;
}
発明されたのは、いくつかの重いカスタムイテレータコード用で、実行時間を短縮します。接頭辞と接尾辞のコストは1つの参照になりました。これが大量の移動を行うカスタムオペレーターの場合、接頭辞と接尾辞は同じ実行時間をもたらしました。
++i
i++
値の古いコピーを返さないためよりも高速です。
また、より直感的です。
x = i++; // x contains the old value of i
y = ++i; // y contains the new value of i
このCの例は、予想される "12"ではなく "02"を出力します。
#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;
}