Dalam situasi di mana kinerja sangat penting, kompiler C kemungkinan besar tidak akan menghasilkan kode tercepat dibandingkan dengan apa yang dapat Anda lakukan dengan bahasa assembly yang disetel dengan tangan. Saya cenderung mengambil jalan yang paling tidak resistan - untuk rutin kecil seperti ini, saya hanya menulis kode asm dan punya ide bagus berapa banyak siklus yang diperlukan untuk mengeksekusi. Anda mungkin bisa mengutak-atik kode C dan mendapatkan kompiler untuk menghasilkan output yang baik, tetapi Anda mungkin akhirnya membuang banyak waktu untuk menyetel output dengan cara itu. Kompiler (terutama dari Microsoft) telah berkembang jauh dalam beberapa tahun terakhir, tetapi mereka masih tidak sepintar kompiler di antara kedua telinga Anda karena Anda sedang mengerjakan situasi spesifik Anda dan bukan hanya kasus umum. Kompiler mungkin tidak menggunakan instruksi tertentu (misalnya LDM) yang dapat mempercepat ini, dan itu ' Tidak mungkin cukup pintar untuk membuka gulungannya. Berikut adalah cara untuk melakukannya yang menggabungkan 3 ide yang saya sebutkan di komentar saya: Loop unrolling, cache prefetch dan memanfaatkan instruksi multiple load (ldm). Jumlah siklus instruksi mencapai sekitar 3 jam per elemen array, tetapi ini tidak memperhitungkan penundaan memori akun.
Teori operasi: Desain CPU ARM mengeksekusi sebagian besar instruksi dalam satu siklus clock, tetapi instruksi dieksekusi dalam pipa. Kompiler C akan mencoba untuk menghilangkan penundaan pipa dengan interleaving instruksi lain di antaranya. Ketika disajikan dengan loop ketat seperti kode C asli, kompiler akan kesulitan menyembunyikan penundaan karena nilai yang dibaca dari memori harus segera dibandingkan. Kode saya di bawah ini berganti-ganti antara 2 set 4 register untuk secara signifikan mengurangi keterlambatan memori itu sendiri dan pipa mengambil data. Secara umum, ketika bekerja dengan kumpulan data besar dan kode Anda tidak menggunakan sebagian besar atau semua register yang tersedia, maka Anda tidak mendapatkan kinerja maksimal.
; 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
Pembaruan:
Ada banyak skeptis dalam komentar yang berpikir bahwa pengalaman saya adalah anekdotal / tidak berharga dan memerlukan bukti. Saya menggunakan GCC 4.8 (dari Android NDK 9C) untuk menghasilkan output berikut dengan optimasi -O2 (semua optimisasi diaktifkan termasuk loop membuka gulungan ). Saya mengkompilasi kode C asli yang disajikan dalam pertanyaan di atas. Inilah yang dihasilkan 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
Output GCC tidak hanya tidak membuka loop, tetapi juga membuang-buang jam di kios setelah LDR. Ini membutuhkan setidaknya 8 jam per elemen array. Melakukan pekerjaan dengan baik menggunakan alamat untuk mengetahui kapan harus keluar dari loop, tetapi semua hal yang dapat dilakukan oleh kompiler tidak dapat ditemukan di kode ini. Saya belum menjalankan kode pada platform target (saya tidak memilikinya), tetapi siapa pun yang berpengalaman dalam kinerja kode ARM dapat melihat bahwa kode saya lebih cepat.
Pembaruan 2:
Saya memberi Microsoft Visual Studio 2013 SP2 kesempatan untuk berbuat lebih baik dengan kode. Itu bisa menggunakan instruksi NEON untuk membuat vektor inisialisasi array saya, tetapi pencarian nilai linier seperti yang ditulis oleh OP keluar mirip dengan apa yang dihasilkan GCC (saya mengganti label untuk membuatnya lebih mudah dibaca):
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
Seperti yang saya katakan, saya tidak memiliki perangkat keras OP yang tepat, tetapi saya akan menguji kinerjanya pada nVidia Tegra 3 dan Tegra 4 dari 3 versi yang berbeda dan memposting hasilnya di sini segera.
Pembaruan 3:
Saya menjalankan kode saya dan Microsoft menyusun kode ARM pada Tegra 3 dan Tegra 4 (Surface RT, Surface RT 2). Saya menjalankan iterasi 10.000.000 loop yang gagal menemukan kecocokan sehingga semuanya ada dalam cache dan mudah untuk diukur.
My Code MS Code
Surface RT 297ns 562ns
Surface RT 2 172ns 296ns
Dalam kedua kasus, kode saya berjalan hampir dua kali lebih cepat. Sebagian besar CPU ARM modern mungkin akan memberikan hasil yang serupa.