ポインタを介したメモリアクセスは、配列を介したメモリアクセスよりも効率的であると言われています。
コンパイラが比較的愚かな獣だった過去には、それは真実だったかもしれません。高最適化モードで出力されたコードの一部を見るだけでgcc
、それがもはや真実ではないことがわかります。そのコードのいくつかは理解するのが非常に難しいですが、一度理解すると、その輝きは明白です。
まともなコンパイラは、ポインタアクセスと配列アクセスに対して同じコードを生成するので、おそらくそのレベルのパフォーマンスについて心配する必要はありません。コンパイラーを作成する人々は、私たちが単なる人間よりも、ターゲットアーキテクチャーについてはるかによく知っています。コード(アルゴリズムの選択など)を最適化するときは、マクロレベルにさらに集中し、ツールメーカーに仕事を任せてください。
実際、コンパイラが全体を最適化していないことに驚いています
temp = a[0];
temp
次の行で別の値で上書きされ、a
マークが付けられていないため、行が存在しなくなりvolatile
ます。
競合他社を数桁上回った最新のVAXFortranコンパイラー(ここに私の年齢を示しています)のベンチマークについての昔からの都市伝説を覚えています。
コンパイラは、ベンチマーク計算の結果がどこにも使用されていないことを理解したため、計算ループ全体を忘却に最適化しました。したがって、実行速度が大幅に向上します。
更新:特定のケースで最適化されたコードがより効率的である理由は、場所を見つける方法のためです。a
リンク/ロード時に決定された固定位置にあり、同時にそれへの参照が固定されます。したがってa[0]
、または実際a[any constant]
に固定された場所になります。
またp
、同じ理由でそれ自体も固定された場所にあります。ただし、 *p
(の内容p
は可変であるため、正しいメモリ位置を見つけるために追加のルックアップが必要になります。
x
さらに別の変数を0(ではなくconst
)に設定して使用a[x]
すると、余分な計算が発生することに気付くでしょう。
あなたのコメントの1つで、あなたは次のように述べています。
あなたが提案したように行うと、配列を介したメモリアクセスのための3つの命令も得られました(インデックスのフェッチ、配列要素の値のフェッチ、一時的な保存)。しかし、私はまだ効率を見ることができません。:-(
それに対する私の反応は、ポインタを使用する際の効率が見られない可能性が非常に高いということです。最新のコンパイラーは、配列操作とポインター操作を同じ基になるマシンコードに変換できることを理解するだけではありません。
実際、最適化をオンにしないと、ポインターコードの効率が低下する可能性があります。次の翻訳を検討してください。
int *pa, i, a[10];
for (i = 0; i < 10; i++)
a[i] = 100;
/*
movl $0, -16(%ebp) ; this is i, init to 0
L2:
cmpl $9, -16(%ebp) ; from 0 to 9
jg L3
movl -16(%ebp), %eax ; load i into register
movl $100, -72(%ebp,%eax,4) ; store 100 based on array/i
leal -16(%ebp), %eax ; get address of i
incl (%eax) ; increment
jmp L2 ; and loop
L3:
*/
for (pa = a; pa < a + 10; pa++)
*pa = 100;
/*
leal -72(%ebp), %eax
movl %eax, -12(%ebp) ; this is pa, init to &a[0]
L5:
leal -72(%ebp), %eax
addl $40, %eax
cmpl -12(%ebp), %eax ; is pa at &(a[10])
jbe L6 ; yes, stop
movl -12(%ebp), %eax ; get pa
movl $100, (%eax) ; store 100
leal -12(%ebp), %eax ; get pa
addl $4, (%eax) ; add 4 (sizeof int)
jmp L5 ; loop around
L6:
*/
その例から、ポインタの例が長く、不必要にそうなっていることが実際にわかります。変更せずに複数回ロードpa
され、実際にとを交互に繰り返します。ここでのデフォルトの最適化は基本的にまったくありません。%eax
%eax
pa
&(a[10])
最適化レベル2に切り替えると、取得するコードは次のようになります。
xorl %eax, %eax
L5:
movl $100, %edx
movl %edx, -56(%ebp,%eax,4)
incl %eax
cmpl $9, %eax
jle L5
アレイバージョンの場合、および:
leal -56(%ebp), %eax
leal -16(%ebp), %edx
jmp L14
L16:
movl $100, (%eax)
addl $4, %eax
L14:
cmpl %eax, %edx
ja L16
ポインタバージョンの場合。
ここではクロックサイクルの分析は行いませんが(作業が多すぎて基本的に怠惰なので)、1つ指摘しておきます。アセンブラ命令に関しては、両方のバージョンのコードに大きな違いはありません。また、最新のCPUが実際に実行される速度を考えると、これらの操作を何十億回も実行しない限り、違いに気付くことはありません。私はいつも読みやすさのためにコードを書くことを好み、それが問題になった場合にのみパフォーマンスを心配する傾向があります。
余談ですが、あなたが参照するそのステートメントは次のとおりです。
5.3ポインターと配列:ポインターのバージョンは一般的に高速ですが、少なくとも初心者にとっては、すぐに把握するのがやや困難です。
K&Rの最も初期のバージョンにまでさかのぼります。これには、関数がまだ記述されている私の古代の1978年のものが含まれます。
getint(pn)
int *pn;
{
...
}
コンパイラはそれ以来、非常に長い道のりを歩んできました。