Performansın son derece önemli olduğu durumlarda, C derleyicisi, elle ayarlanmış montaj diliyle yapabileceklerinize kıyasla büyük olasılıkla en hızlı kodu üretmeyecektir. En az dirençli yolu seçme eğilimindeyim - bunun gibi küçük rutinler için, sadece asm kodu yazıyorum ve yürütmenin kaç döngü alacağına dair bir fikrim var. C kodu ile oynayabilir ve derleyicinin iyi çıktı üretmesini sağlayabilirsiniz, ancak çıktıyı bu şekilde ayarlamak için çok fazla zaman harcayabilirsiniz. Derleyiciler (özellikle Microsoft'tan) son birkaç yılda uzun bir yol kat ettiler, ancak yine de kulaklarınız arasındaki derleyici kadar akıllı değiller çünkü yalnızca genel bir durum değil, özel durumunuz üzerinde çalışıyorsunuz. Derleyici, bunu hızlandırabilecek belirli talimatları (örn. LDM) kullanmayabilir ve ' Döngüyü açmak için yeterince akıllı olma ihtimali düşük. İşte yorumumda bahsettiğim 3 fikri içeren bunu yapmanın bir yolu: Döngü açma, önbelleğe alma ve çoklu yükleme (ldm) talimatını kullanma. Komut döngüsü sayısı, dizi elemanı başına yaklaşık 3 saate çıkar, ancak bu, bellek gecikmelerini hesaba katmaz.
Çalışma teorisi: ARM'in CPU tasarımı, çoğu komutu bir saat döngüsünde yürütür, ancak komutlar bir boru hattında yürütülür. C derleyicileri, aradaki diğer talimatları ekleyerek boru hattı gecikmelerini ortadan kaldırmaya çalışacaktır. Orijinal C kodu gibi sıkı bir döngü ile sunulduğunda, derleyici gecikmeleri gizlemekte zorlanacaktır çünkü bellekten okunan değerin hemen karşılaştırılması gerekir. Aşağıdaki kodum, belleğin kendisindeki gecikmeleri ve verileri alan işlem hattını önemli ölçüde azaltmak için 2 set 4 kayıt arasında değişir. Genel olarak, büyük veri kümeleriyle çalışırken ve kodunuz mevcut yazmaçların çoğunu veya tamamını kullanmıyorsa, maksimum performans elde edemezsiniz.
; 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
Güncelleme:
Yorumlarda, deneyimlerimin anekdot / değersiz olduğunu ve kanıt gerektirdiğini düşünen birçok şüpheci var. Aşağıdaki çıktıyı -O2 optimizasyonuyla oluşturmak için GCC 4.8'i (Android NDK 9C'den) kullandım ( döngü açma dahil tüm optimizasyonlar açık ). Yukarıdaki soruda sunulan orijinal C kodunu derledim. İşte GCC'nin ürettiği:
.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'nin çıktısı yalnızca döngüyü açmakla kalmaz, aynı zamanda LDR'den sonra bir stallda saati boşa harcar. Dizi öğesi başına en az 8 saat gerektirir. Döngüden ne zaman çıkılacağını bilmek için adresi kullanmak iyi bir iş çıkarır, ancak derleyicilerin yapabileceği tüm sihirli şeyler bu kodda hiçbir yerde bulunamaz. Kodu hedef platformda çalıştırmadım (sahibi değilim), ancak ARM kod performansında deneyimli olan herkes kodumun daha hızlı olduğunu görebilir.
Güncelleme 2:
Microsoft'un Visual Studio 2013 SP2'sine kodla daha iyisini yapma şansı verdim. Dizi başlatmamı vektörleştirmek için NEON komutlarını kullanabildi, ancak OP tarafından yazılan doğrusal değer araması GCC'nin ürettiği ile benzer çıktı (daha okunaklı hale getirmek için etiketleri yeniden adlandırdım):
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
Dediğim gibi, OP'nin tam donanımına sahip değilim, ancak performansı 3 farklı sürümden bir nVidia Tegra 3 ve Tegra 4 üzerinde test edeceğim ve sonuçları yakında burada yayınlayacağım.
Güncelleme 3:
Kodumu ve Microsoft'un derlenmiş ARM kodunu bir Tegra 3 ve Tegra 4 (Surface RT, Surface RT 2) üzerinde çalıştırdım. Eşleşme bulamayan bir döngünün 1000000 yinelemesini çalıştırdım, böylece her şey önbellekte ve ölçülmesi kolay.
My Code MS Code
Surface RT 297ns 562ns
Surface RT 2 172ns 296ns
Her iki durumda da kodum neredeyse iki kat daha hızlı çalışıyor. Modern ARM CPU'ların çoğu muhtemelen benzer sonuçlar verecektir.