Postfix Increment演算子を避ける


25

パフォーマンス上の理由で(特定の場合に)後置インクリメント演算子を避けるべきだと読みました。

しかし、これはコードの可読性に影響しませんか?私の考えでは:

for(int i = 0; i < 42; i++);
    /* i will never equal 42! */

より良く見える:

for(int i = 0; i < 42; ++i);
    /* i will never equal 42! */

しかし、これはおそらく単なる習慣ではありません。確かに、私は多くの使用を見ていません++i

この場合、パフォーマンスは可読性を犠牲にするのに悪いですか?または私はただ盲目であり、++iより読みやすいですi++か?


1
i++それがパフォーマンスに影響を与える可能性があることを知る前に使用した++iので、切り替えました。最初は後者は少し奇妙に見えましたが、しばらくすると慣れてしまい、今ではのように自然に感じていi++ます。
ギャブリン

15
++ii++特定のコンテキストで異なることを行いますが、それらが同じであると仮定しないでください。
11

2
これはCまたはC ++についてですか?これらは非常に異なる2つの言語です!:-) C ++では、慣用的なforループはfor (type i = 0; i != 42; ++i)です。できるだけでなく、operator++過負荷状態にあってもよいが、そうoperator!=operator<。接頭辞の増分は、接尾辞よりも高くなく、不等号は、より小さいよりも高くありません。どれを使うべきですか?
ボーパーソン

7
++ Cと呼ばれるべきではありませんか?
アルマン

21
@Stephen:C ++は、Cを取得して追加し、古いものを使用することを意味します。
supercat

回答:


58

事実:

  1. i ++と++ iも同様に読みやすいです。あなたはそれに慣れていないので好きではありませんが、あなたがそれを誤解することは本質的にないので、読み書きするのはもはや仕事ではありません。

  2. 少なくともいくつかの場合、後置演算子の効率は低下します。

  3. ただし、99.99%の場合、(a)いずれにせよ単純またはプリミティブ型で動作し、大きなオブジェクトをコピーする場合にのみ問題になるため(b)パフォーマンスに影響しないため、問題にはなりません。コードの重要な部分(c)コンパイラが最適化するかどうかわからない場合は、それを行うことができます。

  4. したがって、特に(a)他のことを正確に行うのが良い習慣であり、(b)後置詞を使用する予定の青い月に1回という理由だけで、特に接尾辞が必要な場合を除いて接頭辞を使用することをお勧めします間違った方法でやり直してください:常にあなたが意味することを書いているなら、それはあまりありません。パフォーマンスと最適化の間には常にトレードオフがあります。

必要になるまで微最適化するのではなく、常識を使用する必要がありますが、そのためにひどく非効率にならないでください。通常、これは次のことを意味します。最初に、タイムクリティカルでないコード(通常、理由なしに500MBのオブジェクトを値で渡すような基本的な概念エラーを表すもの)でさえ許容できないほど非効率なコード構築を除外します。次に、コードを記述する他のすべての方法のうち、最も明確な方法を選択します。

しかし、ここでは、答えは簡単だと思います:特に接尾辞が必要な場合を除いて接頭辞を書くことは(a)非常にわずかに明確で、(b)非常にわずかに効率的である可能性が高いと思うので、常にデフォルトで書く必要がありますが、忘れても心配しないでください。

半年前、私はあなたと同じように思っていました。i++はもっと自然ですが、それはあなたが慣れているものです。

編集1:私が一般的にこのことを信頼している「より効果的なC ++」のスコット・マイヤーズは、ユーザー定義型でpostfix演算子を使用することを一般的に避けるべきだと言います(postfix increment関数の唯一の正気な実装はオブジェクトのコピー、プレフィックスインクリメント関数を呼び出してインクリメントを実行し、コピーを返しますが、コピー操作は高価になる可能性があります)。

そのため、(a)それが今日当てはまるかどうか、(b)組み込み型にも当てはまるかどうか(c)で「++」を使用すべきかどうかについて、一般的なルールがあるかどうかはわかりません軽量のイテレータークラス以上のもの。しかし、私が上で説明したすべての理由のために、それは重要ではありません。

編集2:これは一般的な慣行を指します。特定のインスタンスで問題があると思われる場合は、プロファイルして確認する必要があります。プロファイリングは簡単で安価で機能します。第一原則から何を最適化する必要があるかを推測するのは難しく、費用がかかり、機能しません。


あなたの投稿はお金に正しさです。aClassInst = someOtherClassInst + yetAnotherClassInst ++のように、中置+演算子と後増分++がオーバーロードされている式では、パーサーは、後増分操作を実行するコードを生成する前に、加算操作を実行するコードを生成します。一時コピーを作成します。ここでのパフォーマンスキラーはポストインクリメントではありません。オーバーロードされた中置演算子の使用です。中置演算子は新しいインスタンスを生成します。
ビットツイダー

2
人々はに「使用」されている理由という私は非常に疑わしいi++というよりは++i...ので、この質問/答えで参照特定の一般的なプログラミング言語の名前である

61

常に最初にプログラマー用に、次にコンピューター用にコーディングしてください。

パフォーマンスの差がある場合、コンパイラは、コードの上にその専門家の目を投げた後、あなたはそれを測定することができます、それは重要-あなたはそれを変更することができます。


7
SUPERBステートメント!!!
デイブ

8
@Martin:これがまさにプレフィックスインクリメントを使用する理由です。Postfixセマンティクスは、古い値を保持することを意味し、その必要がない場合、それを使用することは不正確です。
マチューM.

1
より明確なループインデックスの場合-ただし、ポインタをインクリメントして配列を反復処理し、開始前に不正なアドレスから開始することを意味するプレフィックスを使用する場合、パフォーマンスブーストに関係なく悪いことになります
Martin Beckett

5
@Matthew:ポストインクリメントが古い値のコピーを保持することを意味するということは、単に真実ではありません。コンパイラーが出力を表示するまで、コンパイラーが中間値をどのように処理するかはわかりません。注釈付きのGCCで生成されたアセンブリ言語リストを表示するために時間をかけると、GCCが両方のループに対して同じマシンコードを生成することがわかります。より効率的であるため、ポストインクリメントよりもプレインクリメントを優先するというこのナンセンスは、推測にすぎません。
ビットツイダー

2
@Mathhieu:投稿したコードは、最適化をオフにして生成されました。C ++仕様では、ポストインクリメントが使用される場合、コンパイラは値の一時的なインスタンスを生成する必要があるとは述べていません。それは単に、プリインクリメント演算子とポストインクリメント演算子の優先順位を示しているだけです。
ビットツイダー

13

GCCは、両方のループに対して同じマシンコードを生成します。

Cコード

int main(int argc, char** argv)
{
    for (int i = 0; i < 42; i++)
            printf("i = %d\n",i);

    for (int i = 0; i < 42; ++i)
        printf("i = %d\n",i);

    return 0;
}

アセンブリコード(私のコメント付き)

    cstring
LC0:
    .ascii "i = %d\12\0"
    .text
.globl _main
_main:
    pushl   %ebp
    movl    %esp, %ebp
    pushl   %ebx
    subl    $36, %esp
    call    L9
"L00000000001$pb":
L9:
    popl    %ebx
    movl    $0, -16(%ebp)  // -16(%ebp) is "i" for the first loop 
    jmp L2
L3:
    movl    -16(%ebp), %eax   // move i for the first loop to the eax register 
    movl    %eax, 4(%esp)     // push i onto the stack
    leal    LC0-"L00000000001$pb"(%ebx), %eax // load the effective address of the format string into the eax register
    movl    %eax, (%esp)      // push the address of the format string onto the stack
    call    L_printf$stub    // call printf
    leal    -16(%ebp), %eax  // make the eax register point to i
    incl    (%eax)           // increment i
L2:
    cmpl    $41, -16(%ebp)  // compare i to the number 41
    jle L3              // jump to L3 if less than or equal to 41
    movl    $0, -12(%ebp)   // -12(%ebp) is "i" for the second loop  
    jmp L5
L6:
    movl    -12(%ebp), %eax   // move i for the second loop to the eax register 
    movl    %eax, 4(%esp)     // push i onto the stack
    leal    LC0-"L00000000001$pb"(%ebx), %eax // load the effective address of the format string into the eax register
    movl    %eax, (%esp)      // push the address of the format string onto the stack
    call    L_printf$stub     // call printf
    leal    -12(%ebp), %eax  // make eax point to i
    incl    (%eax)           // increment i
L5:
    cmpl    $41, -12(%ebp)   // compare i to 41 
    jle L6               // jump to L6 if less than or equal to 41
    movl    $0, %eax
    addl    $36, %esp
    popl    %ebx
    leave
    ret
    .section __IMPORT,__jump_table,symbol_stubs,self_modifying_code+pure_instructions,5
L_printf$stub:
    .indirect_symbol _printf
    hlt ; hlt ; hlt ; hlt ; hlt
    .subsections_via_symbols

最適化をオンにしてはどうですか?
serv-inc

2
@user:おそらく変更はありませんが、実際にビットトゥイドルがすぐに戻ってくると期待していますか?
デデュプリケーター

2
注意:Cにはオーバーロードされた演算子を持つユーザー定義型はありませんが、C ++にはあり、基本型からユーザー定義型への一般化は単に無効です。
デデュプリケーター

@Deduplicator:この回答はユーザー定義型に一般化しないことを指摘してくれてありがとう。私は尋ねる前に彼のユーザーページを見ていませんでした。
serv-inc

12

パフォーマンスについて心配する必要はありません。97%の時間です。時期尚早の最適化はすべての悪の根源です。

-ドナルドクヌース

これで邪魔にならないので、選択を正気にしましょう。

  • ++i接頭辞インクリメント、現在の値をインクリメントし、結果を生成します
  • i++後置インクリメント、値のコピー、現在の値のインクリメント、コピーの生成

古い値のコピーが必要でない限り、後置インクリメントを使用することは、物事を成し遂げるためのラウンドアラウンドな方法です。

不正確は怠するものであり、常に最も直接的な方法で意図を表現する構成を使用します。将来のメンテナーが元の意図を誤解する可能性は低くなります。

ここでは(本当に)マイナーですが、コードを読んで本当に困惑している場合があります。意図と実際の表現が一致したかどうか、そしてもちろん、数か月後、彼ら(または私)どちらも覚えていませんでした...

だから、あなたにとって正しいかどうかは関係ありません。抱擁のKISS。数か月のうちに、古い慣行を避けることができます。


4

C ++では、あなたは可能性が関与演算子のオーバーロードがある場合は、あなたがしている書き込みがコードをテンプレートとイテレータが渡されるかもしれないのか分からない。任意のイテレータXの背後にあるロジックは、実質的かつsignificant-両方かもしれ場合は特に、かなりのパフォーマンスの違いを作りますつまり、低速であり、コンパイラによって最適化できません。

しかし、これはCでは当てはまりません。Cの型は些細なタイプにすぎず、パフォーマンスの違いは些細であり、コンパイラは簡単に最適化できます。

ヒント:CまたはC ++でプログラミングし、質問は両方ではなく、どちらか一方に関連しています。


2

どちらの操作のパフォーマンスも、基盤となるアーキテクチャに大きく依存します。メモリに保存されている値をインクリメントする必要があります。つまり、フォンノイマンのボトルネックが両方の場合の制限要因になります。

++ iの場合、

Fetch i from memory 
Increment i
Store i back to memory
Use i

i ++の場合、

Fetch i from memory
Use i
Increment i
Store i back to memory

++および-演算子は、その起源をPDP-11命令セットにトレースします。PDP-11は、レジスタで自動ポストインクリメントを実行できます。また、レジスタに含まれる有効なアドレスに対して自動プリデクリメントを実行できます。どちらの場合でも、問題の変数が「レジスタ」変数である場合にのみ、コンパイラはこれらのマシンレベルの操作を利用できます。


2

何かが遅いかどうかを知りたい場合は、テストしてください。BigIntegerまたはそれに相当するものを使用し、両方のイディオムを使用して類似のforループに貼り付け、ループの内部が最適化されていないことを確認し、両方の時間を計ります。

この記事を読んでも、3つの理由からあまり説得力がありません。1つは、コンパイラは使用されないオブジェクトの作成を最適化できる必要があります。2つ目は、i++概念が数値forループのイディオムであるため、実際に影響を受けるケースはに限定されます。3つ目は、純粋に理論的な議論を提供し、それを裏付ける数字はありません。

特に理由#1に基づいて、私の推測では、あなたが実際にタイミングをとるとき、それらは互いに隣り合っているでしょう。


-1

まず、読みやすさIMOには影響しません。それはあなたが見たことのあるものではありませんが、あなたがそれに慣れるまでにほんの少ししかありません。

第二に、コード内で大量の接尾辞演算子を使用しない限り、多くの違いは見られないでしょう。可能な場合にそれらを使用しない主な理由は、元のvarの値のコピーを、元のvarをまだ使用できる引数の最後まで保持する必要があるということです。これは、アーキテクチャに応じて32ビットまたは64ビットのいずれかです。これは、4または8バイトまたは0.00390625または0.0078125 MBに相当します。非常に高い確率で、非常に長期間保存する必要のある大量のデータを使用しない限り、今日のコンピューターリソースと速度では、接尾辞から接頭辞への切り替えを行っても違いに気付かないでしょう。

編集:私の結論が間違っていることが証明されたので、この残りの部分を忘れてください(++ iとi ++の一部が常に同じことをしているわけではありません...それはまだ真実です)。

また、ケースで同じことをしないことが以前に指摘されました。切り替える場合は、切り替えに注意してください。私はそれを試したことがないので(私はいつもpostfixを使用しました)、確かにわかりませんが、postfixからprefixに変更すると異なる結果になると思います:( 再び間違っている可能性があります...コンパイラに依存します/通訳も)

for (int i=0; i < 10; i++) //the set of i values here will be {0,1,2,3,4,5,6,7,8,9}
for (int i=0; i < 10; ++i) //the set of i values here will be {1,2,3,4,5,6,7,8,9,10}

4
インクリメント操作はforループの終わりに発生するため、まったく同じ出力になります。コンパイラ/インタープリターに依存しません。
jsternberg

@jsternberg ...ありがとう、私は実際にそれをテストする理由がなかったので、いつ増分が起こったのかわからなかった。大学でコンパイラーを作った時から長すぎました!笑
ケネス

間違って間違って間違って。
ルオホラ

-1

意味的に++iは、よりも理にかなってi++いると思うので、そうしないのが一般的であることを除いて、最初のものに固執します(Javaのように、i++広く使用されているため使用するべきです)。


-2

パフォーマンスだけではありません。

場合によっては、コピーがまったく意味をなさないため、コピーの実装をまったく避けたい場合があります。また、プレフィックスインクリメントの使用はこれに依存しないため、プレフィックス形式に固執する方が簡単です。

そして、プリミティブ型と複合型に異なる増分を使用しています...それは本当に読みにくいです。


-2

本当に必要でない限り、私は++ iに固執します。ほとんどの場合、これが目的です。i ++が必要になることはほとんどありません。そのような構造を読むときは、常によく考える必要があります。++ iを使用すると、簡単です。1を追加して使用すると、iは同じままです。

だから、私は@martin beckettに心から同意します:あなた自身のためにそれをもっと簡単にしてください。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.