この主張の背後にある理由を理解する必要があります。なぜ速いのか疑問に思ったことはありますか?いくつかのコードを比較してみましょう:
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
します(intは通常4バイトです)。
そのアドレスから値を取得します。
したがって、から値をフェッチするたびa
に、のベースアドレスが4a
の乗算の結果に追加されi
ます。ポインタを逆参照するだけの場合は、ステップ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つ増やします。
(初心者にとって重要:ポインタで実行++
しても、メモリ内のポインタは1バイト増加しません!ポインタが指すデータのサイズと同じ数のメモリ内のポインタが増加します。これは、を指しint
、
int
は4バイトです。私のマシンなので、b ++はbを4つ増やします!)
わかりましたが、なぜもっと速いのでしょうか?なぜなら、ポインターに4を加算する方が、4を乗算i
してそれをポインターに加算するよりも高速だからです。どちらの場合も加算がありますが、2番目の場合は乗算がありません(1回の乗算に必要なCPU時間を回避できます)。最近のCPUの速度を考えると、配列が1 mio要素であったとしても、実際に違いをベンチマークできるかどうか疑問に思います。
最新のコンパイラがどちらかを同等に高速に最適化できることは、生成されるアセンブリ出力を確認することで確認できます。これを行うには、「-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は単一のアセンブリコマンドでますます多くのことを実行できるため(昔は、コンパイラは実際にアドレスを手動でフェッチし、i
4を掛けて、値をフェッチする前に両方を加算する必要がありました)、昔は絶対的な真実であったステートメントは次のとおりです。今日、ますます疑わしい。また、CPUが内部でどのように機能するかを誰が知っていますか?上記では、1つの組み立て手順を他の2つの手順と比較しています。
命令の数が異なり、そのような命令が必要な時間も異なる可能性があることがわかります。また、これらの命令がマシンのプレゼンテーションで必要とするメモリの量も異なります(結局、メモリからCPUキャッシュに転送する必要があります)。ただし、最近のCPUは、フィードした方法で命令を実行しません。大きな命令(CISCと呼ばれることが多い)を小さなサブ命令(RISCと呼ばれることが多い)に分割します。これにより、プログラムフローを内部で速度を上げるために最適化することもできます。実際、以下の最初の単一の命令と他の2つの命令は、同じサブ命令のセットになる可能性があります。その場合、測定可能な速度の違いはまったくありません。
Objective-Cに関しては、拡張機能を備えたCだけです。したがって、Cに当てはまるものはすべて、ポインターと配列に関してObjective-Cにも当てはまります。一方、オブジェクト(たとえば、NSArray
またはNSMutableArray
)を使用する場合、これは完全に異なる獣です。ただし、その場合は、とにかくメソッドを使用してこれらの配列にアクセスする必要があります。選択できるポインタ/配列アクセスはありません。