結果の値が使用されない場合との間i++
にパフォーマンスの違いはありますか?++i
14 に答える
エグゼクティブサマリー:いいえ。
i++
++i
後で使用するために古い値をi
保存する必要があるため、より遅くなる可能性がありますが、実際には、すべての最新のコンパイラがこれを最適化します。
++i
この関数のコードをとで確認することで、これを実証できますi++
。
$ cat i++.c
extern void g(int i);
void f()
{
int i;
for (i = 0; i < 100; i++)
g(i);
}
++i
およびi++
:を除いて、ファイルは同じです。
$ diff i++.c ++i.c
6c6
< for (i = 0; i < 100; i++)
---
> for (i = 0; i < 100; ++i)
それらをコンパイルし、生成されたアセンブラーも取得します。
$ gcc -c i++.c ++i.c
$ gcc -S i++.c ++i.c
そして、生成されたオブジェクトファイルとアセンブラファイルの両方が同じであることがわかります。
$ md5 i++.s ++i.s
MD5 (i++.s) = 90f620dda862cd0205cd5db1f2c8c06e
MD5 (++i.s) = 90f620dda862cd0205cd5db1f2c8c06e
$ md5 *.o
MD5 (++i.o) = dd3ef1408d3a9e4287facccec53f7d22
MD5 (i++.o) = dd3ef1408d3a9e4287facccec53f7d22
まず、少なくとも整数変数が関係している場合、それ
++i
がより効率的であることは明らかではありません。i++
と :
したがって、質問する必要があるのは、これら2つの操作のどちらが速いかではなく、これら2つの操作のどちらがあなたが達成しようとしていることをより正確に表現するかということです。式の値を使用していない場合は、変数の値をコピーして変数をインクリメントしてから破棄する理由がないため、
i++
の代わりに使用する理由はありません。++i
したがって、結果の値が使用されない場合は、を使用します++i
。しかし、それがより効率的だからではありません。それは私の意図を正しく述べているからです。
より良い答えは、++i
時には速くなりますが、決して遅くなることはないということです。
i
誰もがそれがのような通常の組み込みタイプであると想定しているようint
です。この場合、測定可能な違いはありません。
ただし、i
が複雑なタイプの場合は、測定可能な違いが見つかる可能性があります。クラスをインクリメントする前に、クラスのコピーを作成するi++
必要があります。++i
コピーに含まれるものによっては、最終的な値を返すことができるため、実際には遅くなる可能性があります。
Foo Foo::operator++()
{
Foo oldFoo = *this; // copy existing value - could be slow
// yadda yadda, do increment
return oldFoo;
}
もう1つの違いは++i
、値の代わりに参照を返すオプションがあることです。繰り返しますが、オブジェクトのコピーの作成に関係するものによっては、これが遅くなる可能性があります。
これが発生する可能性のある実際の例は、イテレータの使用です。イテレータをコピーすることがアプリケーションのボトルネックになる可能性は低いですが、結果に影響を与えない場所では++i
なく、使用する習慣を身に付けることをお勧めします。i++
簡潔な答え:
速度に関してはi++
との間に違いはありません。++i
優れたコンパイラは、2 つのケースで異なるコードを生成するべきではありません。
長い答え:
++i
他のすべての回答が言及していないのは、 vsの違いは、i++
それが見つかった表現内でのみ意味があるということです。
の場合、for(i=0; i<n; i++)
はi++
独自の表現で単独です。 の前にシーケンス ポイントがあり、i++
その後にシーケンス ポイントがあります。したがって、生成される唯一の機械語コードは「increase i
by 1
」であり、プログラムの残りの部分との関係でこれがどのように順序付けられるかが明確に定義されています。したがって、これを prefix++
に変更しても、まったく問題にはなりません。マシンコードは「増加」するだけi
です1
。
との違いは、 vsなどの表現++i
でのみ重要です。一部の人は、存在するレジスタを後でリロードする必要があるため、そのような操作では後置が遅くなると主張し、言うかもしれません。ただし、コンパイラは、C 標準が呼び出すように「抽象マシンの動作を壊さない」限り、任意の方法で命令を自由に順序付けできることに注意してください。i++
array[i++] = x;
array[++i] = x;
i
array[i++] = x;
したがって、次のようにマシンコードに変換されると想定するかもしれません。
i
の値をレジスタ A に格納します。- 配列のアドレスをレジスタ B に格納します。
- A と B を加算し、結果を A に格納します。
- A で表されるこの新しいアドレスに、x の値を格納します。
- の値をレジスタ A に格納
i
する // 非効率的です。これは、ここで余分な命令を実行したためです。 - レジスタ A をインクリメントします。
- レジスタ A を に格納し
i
ます。
コンパイラは、次のようなコードをより効率的に生成することもできます。
i
の値をレジスタ A に格納します。- 配列のアドレスをレジスタ B に格納します。
- A と B を加算し、結果を B に格納します。
- レジスタ A をインクリメントします。
- レジスタ A を に格納し
i
ます。 - ... // コードの残りの部分。
C プログラマーとして、後置++
が最後にあると考えるように訓練されているからといって、マシン コードをそのように並べる必要はありません。
そのため、C では接頭辞と接尾辞に違いはありません++
。C プログラマーとして異なるのは、理由もなく、場合によっては接頭辞を使用し、他の場合では後置詞を矛盾して使用する人々です。これは、彼らが C のしくみについて不確かであるか、言語について間違った知識を持っていることを示唆しています。これは常に悪い兆候であり、迷信や「宗教的教義」に基づいて、プログラムで他の疑わしい決定を下していることを示唆しています。
「プレフィックス++
は常に高速です」は、C プログラマー志望者の間で一般的な誤った定説の 1 つです。
スコットマイヤーズから葉を取り、より効果的なc ++ 項目6:インクリメントおよびデクリメント操作のプレフィックス形式とポストフィックス形式を区別します。
オブジェクトに関しては、特にイテレータに関しては、プレフィックスバージョンがポストフィックスよりも常に優先されます。
オペレーターの呼び出しパターンを見ると、この理由がわかります。
// Prefix
Integer& Integer::operator++()
{
*this += 1;
return *this;
}
// Postfix
const Integer Integer::operator++(int)
{
Integer oldValue = *this;
++(*this);
return oldValue;
}
この例を見ると、プレフィックス演算子が常にポストフィックスよりも効率的であることが簡単にわかります。接尾辞の使用に一時オブジェクトが必要なため。
これが、イテレータを使用した例を見ると、常にプレフィックスバージョンを使用している理由です。
しかし、intについて指摘しているように、コンパイラの最適化が行われる可能性があるため、事実上違いはありません。
マイクロ最適化について心配している場合は、ここに追加の所見があります。ループをデクリメントすることは、ループをインクリメントするよりも「おそらく」効率的である可能性があります(ARMなどの命令セットアーキテクチャによって異なります)。
for (i = 0; i < 100; i++)
各ループには、次の命令が1つずつあります。
- に追加
1
しi
ます。 i
未満かどうかを比較し100
ます。i
が。未満の場合の条件分岐100
。
一方、デクリメントループ:
for (i = 100; i != 0; i--)
ループには、次のそれぞれの命令があります。
- デクリメント
i
し、CPUレジスタステータスフラグを設定します。 - CPUレジスタの状態に応じた条件分岐(
Z==0
)。
もちろん、これはゼロにデクリメントする場合にのみ機能します。
ARMシステム開発者ガイドから覚えています。
「どちらが速いか」という質問で、どちらを使用するかを決定しないでください。おそらく、それほど気にすることはないでしょう。さらに、プログラマーの読み取り時間は、マシンの時間よりもはるかに高価です。
コードを読む人間にとって最も意味のあるものを使用してください。
@Markコンパイラは変数の(スタックベースの)一時コピーを最適化することが許可されており、gcc(最近のバージョンでは)がそうしていますが、すべてのコンパイラが常にそうするわけではありません。
現在のプロジェクトで使用しているコンパイラでテストしたところ、4 つのうち 3 つが最適化されませんでした。
特に、おそらく高速であるが決して低速ではないコードが読みやすい場合は、コンパイラがそれを正しく行うと想定しないでください。
コード内のいずれかの演算子の本当にばかげた実装がない場合:
常に i++ よりも ++i を優先します。
C では、結果が使用されていない場合、コンパイラは通常、それらを同じになるように最適化できます。
ただし、C++ では、独自の ++ 演算子を提供する他の型を使用する場合、プレフィックス バージョンはポストフィックス バージョンよりも高速になる可能性があります。したがって、後置セマンティクスが必要ない場合は、前置演算子を使用することをお勧めします。
ここでのほとんどの回答と多くのコメントを読んでいますが、どこよりも効率的であると考えることができる1つのインスタンスへの参照は見当たりませんでした(そしておそらく驚くべきことに、より効率的でした)。これは、DEC PDP-11 用の C コンパイラ用です。i++
++i
--i
i--
PDP-11 には、レジスタのプリ デクリメントとポスト インクリメントのアセンブリ命令がありましたが、その逆はありませんでした。この命令により、任意の「汎用」レジスタをスタック ポインタとして使用できるようになりました。したがって、そのようなものを使用*(i++)
した場合、単一のアセンブリ命令にコンパイルできますが、*(++i)
できませんでした。
これは明らかに非常に難解な例ですが、ポストインクリメントがより効率的であるという例外を提供します (または、最近は PDP-11 C コードの需要があまりないため、wasと言うべきです)。
接尾辞が接頭辞のインクリメントよりも遅い状況を考えることができます:
レジスタを備えたプロセッサがA
アキュムレータとして使用され、それが多くの命令で使用される唯一のレジスタであると想像してください (いくつかの小さなマイクロコントローラは実際にこのようなものです)。
ここで、次のプログラムとそれらの仮想アセンブリへの変換を想像してください。
プレフィックスの増分:
a = ++b + c;
; increment b
LD A, [&b]
INC A
ST A, [&b]
; add with c
ADD A, [&c]
; store in a
ST A, [&a]
後置インクリメント:
a = b++ + c;
; load b
LD A, [&b]
; add with c
ADD A, [&c]
; store in a
ST A, [&a]
; increment b
LD A, [&b]
INC A
ST A, [&b]
の値b
が強制的に再ロードされたことに注意してください。プレフィックスインクリメントを使用すると、コンパイラは値をインクリメントして使用を続行できます。インクリメント後に目的の値が既にレジスタにあるため、再ロードを回避できます。ただし、後置インクリメントでは、コンパイラは 2 つの値を処理する必要があります。
もちろん、単一のi++;
ステートメントなど、インクリメントの値が使用されていない場合、コンパイラは、後置または接頭辞の使用に関係なく、単純にインクリメント命令を生成できます (実際に生成します)。
補足として、 a がある式は、追加の努力なしに (たとえば、 a を追加することによって)b++
単に式に変換することはできないことに言及したいと思います。したがって、それらが何らかの式の一部である場合、2 つを比較することは実際には有効ではありません。多くの場合、式の中で を使用する場所では を使用できません。もちろん、式が物乞いをしている場合は例外です (たとえば、which を に変更できます)。++b
- 1
b++
++b
++b
a = b++ + 1;
a = ++b;
私は常に事前インクリメントを好みますが...
operator++ 関数を呼び出す場合でも、関数がインライン化されると、コンパイラは一時的なものを最適化できることを指摘したいと思います。通常、operator++ は短く、ヘッダーに実装されることが多いため、インライン化される可能性があります。
したがって、実際には、2 つの形式のパフォーマンスに大きな違いはないようです。ただし、オプティマイザに頼って理解するよりも、自分が言おうとしていることを直接表現する方が良いと思われるため、常に事前インクリメントを好みます。
また、オプティマイザの処理を少なくすると、コンパイラの実行速度が向上する可能性があります。
私のCは少し錆びていますので、あらかじめお詫び申し上げます。スピードワイズ、結果は理解できます。しかし、両方のファイルがどのようにして同じ MD5 ハッシュになったのか、私は混乱しています。おそらく for ループは同じように実行されますが、次の 2 行のコードは異なるアセンブリを生成するのではないでしょうか?
myArray[i++] = "hello";
対
myArray[++i] = "hello";
最初のものは値を配列に書き込み、次に i をインクリメントします。2 番目のインクリメント i は、配列に書き込みます。私はアセンブリの専門家ではありませんが、これら 2 つの異なるコード行によって同じ実行可能ファイルがどのように生成されるのかわかりません。
ちょうど私の2セント。