C でのポインター演算と添字による配列値へのアクセス

この主張の背後にある理由を理解する必要があります。なぜ高速なのか疑問に思ったことはありませんか?いくつかのコードを比較してみましょう:

int i;
int a[20];

// Init all values to zero
memset(a, 0, sizeof(a));
for (i = 0; i < 20; i++) {
    printf("Value of %d is %d\n", i, a[i]);
}

それらはすべてゼロです。なんて驚きです :-P 問題は、a[i] の意味です。 実際に低レベルのマシンコードで?つまり

<オール> <リ>

a のアドレスを取る

<リ>

i を追加 a の 1 つのアイテムのサイズの倍 そのアドレスに (int は通常 4 バイトです)。

<リ>

そのアドレスから値を取得します。

a から値を取得するたびに 、 a のベースアドレス i の乗算の結果に加算されます 4で。ポインタを逆参照するだけの場合は、ステップ 1. と 2. を実行する必要はなく、ステップ 3 のみを実行してください。

以下のコードを検討してください。

int i;
int a[20];
int * b;

memset(a, 0, sizeof(a));
b = a;
for (i = 0; i < 20; i++) {
    printf("Value of %d is %d\n", i, *b);
    b++;
}

このコードは可能性があります より速くなる...しかし、たとえそうであっても、違いはごくわずかです。なぜそれはより速いのでしょうか? 「*b」は、上記の手順 3. と同じです。ただし、「b++」はステップ 1. およびステップ 2. と同じではありません。「b++」はポインターを 4 増やします。

わかりましたが、なぜそれが速いのでしょうか? i を乗算するよりもポインタに 4 を加算する方が速いため 4 で、それをポインターに追加します。どちらの場合も加算がありますが、2 番目の場合には乗算はありません (1 つの乗算に必要な CPU 時間を回避できます)。最新の CPU の速度を考えると、たとえ配列が 100 万要素だったとしても、本当に違いをベンチマークできるのだろうか.

最新のコンパイラがどちらも同じように高速になるように最適化できることは、コンパイラが生成するアセンブリ出力を確認することで確認できます。これを行うには、「-S」オプション (大文字の S) を GCC に渡します。

これが最初の C コードのコードです (最適化レベル -Os これは、コード サイズと速度を最適化することを意味しますが、-O2 とは異なり、コード サイズを著しく増加させる速度の最適化を行わないことを意味します。 -O3とはかなり異なります ):

_main:
    pushl   %ebp
    movl    %esp, %ebp
    pushl   %edi
    pushl   %esi
    pushl   %ebx
    subl    $108, %esp
    call    ___i686.get_pc_thunk.bx
"L00000000001$pb":
    leal    -104(%ebp), %eax
    movl    $80, 8(%esp)
    movl    $0, 4(%esp)
    movl    %eax, (%esp)
    call    L_memset$stub
    xorl    %esi, %esi
    leal    LC0-"L00000000001$pb"(%ebx), %edi
L2:
    movl    -104(%ebp,%esi,4), %eax
    movl    %eax, 8(%esp)
    movl    %esi, 4(%esp)
    movl    %edi, (%esp)
    call    L_printf$stub
    addl    $1, %esi
    cmpl    $20, %esi
    jne L2
    addl    $108, %esp
    popl    %ebx
    popl    %esi
    popl    %edi
    popl    %ebp
    ret

2 番目のコードと同じ:

_main:
    pushl   %ebp
    movl    %esp, %ebp
    pushl   %edi
    pushl   %esi
    pushl   %ebx
    subl    $124, %esp
    call    ___i686.get_pc_thunk.bx
"L00000000001$pb":
    leal    -104(%ebp), %eax
    movl    %eax, -108(%ebp)
    movl    $80, 8(%esp)
    movl    $0, 4(%esp)
    movl    %eax, (%esp)
    call    L_memset$stub
    xorl    %esi, %esi
    leal    LC0-"L00000000001$pb"(%ebx), %edi
L2:
    movl    -108(%ebp), %edx
    movl    (%edx,%esi,4), %eax
    movl    %eax, 8(%esp)
    movl    %esi, 4(%esp)
    movl    %edi, (%esp)
    call    L_printf$stub
    addl    $1, %esi
    cmpl    $20, %esi
    jne L2
    addl    $124, %esp
    popl    %ebx
    popl    %esi
    popl    %edi
    popl    %ebp
    ret

いや、違うよ、きっと。 104 と 108 の数の違いは、変数 b に由来します。 (最初のコードでは、スタック上の変数が 1 つ少なくなりましたが、現在はもう 1 つ、スタック アドレスを変更しています)。 for の実際のコードの違い ループは

movl    -104(%ebp,%esi,4), %eax

と比較して

movl    -108(%ebp), %edx
movl    (%edx,%esi,4), %eax

実際には、2 つのマシン コードを使用する代わりに、1 つの CPU マシン コードを発行してすべての作業を実行する (CPU がすべてを実行する) ため、最初のアプローチの方が高速 (!) に見えます。一方、以下の 2 つのアセンブリ コマンドは、全体として上記のコマンドよりも実行時間が短い可能性があります。

最後に、コンパイラと CPU の機能 (メモリにアクセスするために CPU が提供するコマンドとその方法) に応じて、結果はどちらかになる可能性があります。どちらかが速い/遅いかもしれません。正確に 1 つのコンパイラ (つまり 1 つのバージョン) と 1 つの特定の CPU に限定しない限り、確実なことは言えません。 CPU は 1 つのアセンブリ コマンドでより多くの処理を実行できるため (昔は、コンパイラはアドレスを手動でフェッチする必要があり、i を掛ける必要がありました)。 値を取得する前に両方を合計する)、以前は絶対的な真実であったステートメントは、今日ではますます疑わしいものになっています。また、CPUが内部でどのように機能するかを誰が知っていますか?上で、1 つの組み立て説明書を他の 2 つの組み立て説明書と比較しています。

命令の数が異なり、そのような命令に必要な時間も異なる可能性があることがわかります。また、これらの命令がマシンのプレゼンテーションで必要とするメモリの量 (最終的にはメモリから CPU キャッシュに転送する必要があります) も異なります。ただし、最新の CPU は、命令を与えたとおりに命令を実行しません。大きな命令 (CISC と呼ばれることが多い) を小さなサブ命令 (RISC と呼ばれることが多い) に分割します。これにより、プログラム フローを内部的に最適化して速度を向上させることもできます。実際、最初の単一の命令と以下の他の 2 つの命令は、同じサブ命令のセットになる可能性があります。 、この場合、測定可能な速度の違いはまったくありません.

Objective-C に関しては、C に拡張機能を追加しただけです。したがって、C に当てはまることはすべて、Objective-C にも当てはまり、ポインターと配列に関しても当てはまります。一方、オブジェクトを使用する場合 (たとえば、 NSArray または NSMutableArray )、これはまったく別の獣です。ただし、その場合はとにかくメソッドを使用してこれらの配列にアクセスする必要があり、選択できるポインター/配列アクセスはありません。


いや。どちらでも同じ操作です。添え字は、配列の開始アドレスに (要素サイズ * インデックス) を追加するための構文糖衣です。

とはいえ、配列内の要素を反復処理する場合、最初の要素へのポインターを取得してループを介して毎回増加させると、通常、ループ変数から現在の要素の位置を毎回計算するよりもわずかに高速になります。 (実際のアプリケーションでこれが重要になることはめったにありません。最初にアルゴリズムを調べてください。時期尚早の最適化は諸悪の根源などです)


実行速度に関するあなたの質問には答えていないため、これは少し話題から外れているかもしれませんが (申し訳ありません)、時期尚早の最適化はすべての悪の根源であることを考慮する必要があります。 (クヌート)。私の意見では、特に言語をまだ (再) 学習している場合は、必ず最初に読みやすい方法で記述してください。次に、プログラムが 正しく 実行される場合 、速度の最適化を検討してください。ほとんどの場合、コーディングはとにかく十分に高速です。