パフォーマンスが最も重要である状況では、Cコンパイラーは、手動で調整されたアセンブリ言語で実行できるものと比較して、最速のコードを生成しない可能性があります。私は最も抵抗の少ない経路をとる傾向があります-このような小さなルーチンの場合、私はasmコードを書くだけで、実行に何サイクルかかるかを理解しています。Cコードをいじってコンパイラに適切な出力を生成させることはできるかもしれませんが、そのようにして出力を調整することに多くの時間を費やすことになるかもしれません。コンパイラ(特にMicrosoft製)はここ数年で大きな進歩を遂げましたが、一般的なケースだけでなく、特定の状況に取り組んでいるため、耳の間のコンパイラほどスマートではありません。コンパイラは、これをスピードアップできる特定の命令(LDMなど)を使用しない場合があります。ループを展開するのに十分スマートである可能性は低いです。これは、コメントで述べた3つのアイデアを組み込んだ方法です。ループのアンロール、キャッシュのプリフェッチ、および複数のロード(ldm)命令の使用。命令サイクルカウントは、配列要素ごとに約3クロックになりますが、これはメモリ遅延を考慮していません。
動作理論: ARMのCPUデザインは1クロックサイクルでほとんどの命令を実行しますが、命令はパイプラインで実行されます。Cコンパイラーは、間に他の命令をインターリーブすることにより、パイプライン遅延を排除しようとします。元のCコードのようなタイトなループが提示されると、メモリから読み取られた値をすぐに比較する必要があるため、コンパイラは遅延を隠すのに苦労します。以下のコードでは、4つのレジスタの2つのセットを交互に使用して、メモリ自体とデータをフェッチするパイプラインの遅延を大幅に削減しています。一般に、大きなデータセットを使用していて、コードが使用可能なレジスタのほとんどまたはすべてを使用していない場合、最大のパフォーマンスは得られません。
; r0 = count, r1 = source ptr, r2 = comparison value
stmfd sp!,{r4-r11} ; save non-volatile registers
mov r3,r0,LSR #3 ; loop count = total count / 8
pld [r1,#128]
ldmia r1!,{r4-r7} ; pre load first set
loop_top:
pld [r1,#128]
ldmia r1!,{r8-r11} ; pre load second set
cmp r4,r2 ; search for match
cmpne r5,r2 ; use conditional execution to avoid extra branch instructions
cmpne r6,r2
cmpne r7,r2
beq found_it
ldmia r1!,{r4-r7} ; use 2 sets of registers to hide load delays
cmp r8,r2
cmpne r9,r2
cmpne r10,r2
cmpne r11,r2
beq found_it
subs r3,r3,#1 ; decrement loop count
bne loop_top
mov r0,#0 ; return value = false (not found)
ldmia sp!,{r4-r11} ; restore non-volatile registers
bx lr ; return
found_it:
mov r0,#1 ; return true
ldmia sp!,{r4-r11}
bx lr
更新:
私の経験は逸話的/価値がなく、証拠が必要であると考えるコメントには多くの懐疑論者がいます。GCC 4.8(Android NDK 9Cから)を使用して、最適化-O2(ループの展開を含むすべての最適化をオンにした)を使用して次の出力を生成しました。上記の質問で提示された元のCコードをコンパイルしました。GCCが生成したものは次のとおりです。
.L9: cmp r3, r0
beq .L8
.L3: ldr r2, [r3, #4]!
cmp r2, r1
bne .L9
mov r0, #1
.L2: add sp, sp, #1024
bx lr
.L8: mov r0, #0
b .L2
GCCの出力はループを展開しないだけでなく、LDR後のストールでクロックを浪費します。配列要素ごとに少なくとも8クロックが必要です。ループを終了するタイミングを知るためにアドレスを使用することはうまくいきますが、コンパイラーが実行できるすべての不思議なことは、このコードにはありません。ターゲットプラットフォームでコードを実行していません(私はコードを所有していません)が、ARMコードパフォーマンスの経験者なら誰でも私のコードの方が速いことがわかります。
更新2:
MicrosoftのVisual Studio 2013 SP2に、コードをより効果的に使用する機会を与えました。NEON命令を使用して配列の初期化をベクトル化できましたが、OPによって書き込まれた線形値の検索は、GCCが生成したものと同様になりました(読みやすくするためにラベルの名前を変更しました)。
loop_top:
ldr r3,[r1],#4
cmp r3,r2
beq true_exit
subs r0,r0,#1
bne loop_top
false_exit: xxx
bx lr
true_exit: xxx
bx lr
私が言ったように、私はOPの正確なハードウェアを所有していませんが、3つの異なるバージョンのnVidia Tegra 3およびTegra 4でパフォーマンスをテストし、すぐに結果をここに投稿します。
更新3:
コードとMicrosoftのコンパイル済みARMコードをTegra 3およびTegra 4(Surface RT、Surface RT 2)で実行しました。ループの1000000回の反復を実行しましたが、一致が見つからないため、すべてがキャッシュにあり、測定が簡単です。
My Code MS Code
Surface RT 297ns 562ns
Surface RT 2 172ns 296ns
どちらの場合でも、コードはほぼ2倍の速度で実行されます。最近のARM CPUのほとんどは、おそらく同様の結果をもたらすでしょう。