Bir C dizisinde bir değer olup olmadığını hızlıca bulun.


124

256 boyutlu (tercihen 1024, ancak minimum 256) bir dizi boyunca yineleme yapması ve bir değerin dizi içeriğiyle eşleşip eşleşmediğini kontrol etmesi gereken zaman açısından kritik bir ISR'ye sahip gömülü bir uygulamam var. A booldoğru olarak ayarlanacak ise durum budur.

Mikrodenetleyici bir NXP LPC4357, ARM Cortex M4 çekirdeğidir ve derleyici GCC'dir. Optimizasyon seviyesi 2'yi (3 daha yavaş) ve işlevi flash yerine RAM'e yerleştirmeyi zaten birleştirdim. Ayrıca işaretçi aritmetiği ve foryukarı yerine aşağı sayma yapan bir döngü kullanıyorum (kontrol i!=0etmekten daha hızlı olup olmadığını kontrol etmek i<256). Sonuç olarak, uygulanabilir olması için büyük ölçüde azaltılması gereken 12,5 µs'lik bir süre ile sonuçlandım. Şu anda kullandığım (sözde) kod bu:

uint32_t i;
uint32_t *array_ptr = &theArray[0];
uint32_t compareVal = 0x1234ABCD;
bool validFlag = false;

for (i=256; i!=0; i--)
{
    if (compareVal == *array_ptr++)
    {
         validFlag = true;
         break;
     }
}

Bunu yapmanın mutlak en hızlı yolu nedir? Satır içi montaj kullanımına izin verilir. Diğer 'daha az zarif' numaralara da izin verilir.


28
Dizideki değeri farklı şekilde saklamanın bir yolu var mı? Bunları sıralayabilirseniz, ikili arama kesinlikle daha hızlı olacaktır. Depolanacak ve aranacak veriler belirli bir aralık dahilindeyse, bunlar bir bit haritası vb.
İle gösterilebilir

20
@BitBank: Son otuz yılda ne kadar derleyicinin geliştiğine şaşıracaksınız. ARM özellikle derleyici dostudur. Ve bir gerçeği biliyorum ki GCC'de ARM yükleme çoklu talimatlar verebilir (en azından 2009'dan beri)
MSalters

8
harika bir soru, insanlar performansın önemli olduğu gerçek dünya durumlarının olduğunu unutur. birçok kez bunun gibi sorular "sadece stl kullan" ile yanıtlanıyor
Kik

14
"... bir dizi boyunca yineleme" başlığı yanıltıcıdır, çünkü aslında yalnızca belirli bir değeri arıyorsunuz. Bir dizi üzerinde yineleme yapmak, her girişte bir şeyler yapılması gerektiği anlamına gelir. Sıralama, eğer maliyet birçok aramada amorti edilebiliyorsa, gerçekten de dil uygulama sorunlarından bağımsız etkili bir yaklaşımdır.
hardmath

8
İkili arama veya karma tablo kullanamayacağınızdan emin misiniz? 256 öğe için ikili arama == 8 karşılaştırma. Bir hash tablosu == ortalama 1 atlama (veya mükemmel bir hash'e sahipseniz 1 maksimum atlama ). Montaj optimizasyonuna yalnızca 1) iyi bir arama algoritmasına sahip olduktan ( O(1)veya O(logN)buna kıyasla O(N)) ve 2) darboğaz olarak profil oluşturduktan sonra başvurmalısınız.
Groo

Yanıtlar:


105

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.


13
@ LưuVĩnhPhúc - bu genellikle doğrudur, ancak sıkı ISR'ler en büyük istisnalardan biridir ve genellikle derleyicinin bildiğinden çok daha fazlasını bilirsiniz.
sapi

47
Şeytanın avukatı: Bu kodun daha hızlı olduğuna dair herhangi bir nicel kanıt var mı?
Oliver Charlesworth

11
@BitBank: Bu yeterince iyi değil. İddialarınızı kanıtlarla desteklemelisiniz .
Orbit'te Hafiflik Yarışları

13
Dersimi yıllar önce aldım. U ve V borularını en iyi şekilde kullanarak bir Pentium'da bir grafik rutini için harika bir optimize edilmiş iç döngü oluşturdum. Döngü başına 6 saat döngüsüne düştüm (hesaplandı ve ölçüldü) ve kendimle çok gurur duydum. C ile yazılanla aynı şeyi test ettiğimde, C daha hızlıydı. Bir daha asla Intel assembler satırını yazmadım.
Rocketmagnet

14
"Deneyimlerimin anekdot niteliğinde / değersiz olduğunu ve kanıt gerektirdiğini düşünen yorumlarda şüpheciler." Yorumlarını aşırı derecede olumsuz almayın. Kanıtı göstermek, harika cevabınızı çok daha iyi hale getirir.
Cody Grey

87

Optimize etmenin bir yolu var (bir keresinde bir iş görüşmesinde bana sorulmuştu):

  • Dizideki son giriş aradığınız değeri tutuyorsa, doğru döndürün
  • Dizideki son girişe aradığınız değeri yazın
  • Aradığınız değerle karşılaşana kadar diziyi yineleyin
  • Dizideki son girişten önce onunla karşılaştıysanız, doğru döndürün
  • Yanlış döndür

bool check(uint32_t theArray[], uint32_t compareVal)
{
    uint32_t i;
    uint32_t x = theArray[SIZE-1];
    if (x == compareVal)
        return true;
    theArray[SIZE-1] = compareVal;
    for (i = 0; theArray[i] != compareVal; i++);
    theArray[SIZE-1] = x;
    return i != SIZE-1;
}

Bu, yineleme başına iki dal yerine yineleme başına bir dal verir.


GÜNCELLEME:

Diziyi tahsis etme SIZE+1izniniz varsa, "son giriş değiş tokuşu" kısmından kurtulabilirsiniz:

bool check(uint32_t theArray[], uint32_t compareVal)
{
    uint32_t i;
    theArray[SIZE] = compareVal;
    for (i = 0; theArray[i] != compareVal; i++);
    return i != SIZE;
}

Ayrıca theArray[i], bunun yerine aşağıdakileri kullanarak gömülü ek aritmetikten kurtulabilirsiniz :

bool check(uint32_t theArray[], uint32_t compareVal)
{
    uint32_t *arrayPtr;
    theArray[SIZE] = compareVal;
    for (arrayPtr = theArray; *arrayPtr != compareVal; arrayPtr++);
    return arrayPtr != theArray+SIZE;
}

Derleyici bunu zaten uygulamıyorsa, bu işlev kesinlikle uygulayacaktır. Öte yandan, optimize edicinin döngüyü açmasını zorlaştırabilir, bu nedenle üretilen derleme kodunda bunu doğrulamanız gerekir ...


2
@ratchetfreak: OP bu dizinin nasıl, nerede ve ne zaman tahsis edilip başlatıldığına dair herhangi bir detay vermiyor, bu yüzden buna bağlı olmayan bir cevap verdim.
barak manos

3
Dizi RAM'dedir ancak yazmaya izin verilmez.
wlamers

1
güzel, ancak dizi artık yok const, bu da bunu iş parçacığı için güvenli değil. Ödenmesi yüksek bir bedel gibi görünüyor.
EOF

2
@EOF: Sorunun neresinden constbahsedildi?
barak manos

4
@barakmanos: Size bir dizi ve bir değer iletirsem ve değerin dizide olup olmadığını sorarsam, genellikle diziyi değiştireceğinizi düşünmüyorum. Asıl soru ne konulardan ne constde bahsetmektedir , ancak bu uyarıdan bahsetmenin doğru olduğunu düşünüyorum.
EOF

62

Algoritmanızı optimize etmek için yardım istiyorsunuz, bu sizi montajcıya itebilir. Ancak algoritmanız (doğrusal bir arama) o kadar akıllı değil, bu nedenle algoritmanızı değiştirmeyi düşünmelisiniz. Örneğin:

Mükemmel hash işlevi

256 "geçerli" değeriniz statikse ve derleme zamanında biliniyorsa, mükemmel bir hash işlevi kullanabilirsiniz . Önem verdiğiniz tüm geçerli değerler için hiçbir çarpışmanın olmadığı, girdi değerinizi 0 .. n aralığındaki bir değere eşleyen bir karma işlevi bulmanız gerekir . Yani, iki "geçerli" değer aynı çıktı değerine hash değildir. İyi bir hash işlevi ararken şunları hedeflersiniz:

  • Hash fonksiyonunu makul derecede hızlı tutun.
  • Minimize n . Elde edebileceğiniz en küçük 256'dır (minimum mükemmel hash işlevi), ancak verilere bağlı olarak bunu başarması muhtemelen zordur.

Etkili bir karma fonksiyonları için Not n genellikle düşük bitlerin (VE işlemi) bir bit maskesi eşdeğerdir 2'nin bir güçtür. Örnek hash fonksiyonları:

  • Giriş baytlarının CRC'si, modulo n .
  • ((x << i) ^ (x >> j) ^ (x << k) ^ ...) % n(birçok olarak toplama i, j, k, ... gerektiği gibi, sol veya sağ vardiya ile)

Ardından , karmanın giriş değerlerini tabloya i indeksine eşlediği n girişli sabit bir tablo oluşturursunuz . Geçerli değerler için, tablo girişi i geçerli değeri içerir. Diğer tüm tablo girdileri için dizinin her bir giriş sağlamak i için karma değil bazı diğer geçersiz değer içeriyor i .

Ardından, x girişiyle kesme rutininizde :

  1. İ dizini için karma x (0..n aralığındadır)
  2. Tablodaki i girişine bakın ve x değerini içerip içermediğine bakın .

Bu, 256 veya 1024 değerin doğrusal aramasından çok daha hızlı olacaktır.

Ben ettik bazı Python kodu yazılı makul özet fonksiyonlarını bulmak için.

Ikili arama

256 "geçerli" değer dizinizi sıralarsanız, doğrusal arama yerine ikili arama yapabilirsiniz . Bu, 256 girişli tabloyu yalnızca 8 adımda ( log2(256)) veya 1024 girişli bir tabloda 10 adımda arayabileceğiniz anlamına gelir . Yine, bu 256 veya 1024 değerin doğrusal aramasından çok daha hızlı olacaktır.


Bunun için teşekkürler. İkili arama seçeneği, seçtiğim seçenek. İlk gönderideki daha önceki bir yoruma da bakın. Bu, montaj kullanmadan hile çok iyi yapar.
wlamers

11
Aslında, kodunuzu optimize etmeye çalışmadan önce (montaj veya diğer hileler gibi) algoritmik karmaşıklığı azaltıp azaltamayacağınızı muhtemelen görmelisiniz. Genellikle algoritmik karmaşıklığı azaltmak, birkaç döngüyü ayırmaya çalışmaktan, ancak aynı algoritmik karmaşıklığı korumaktan daha verimli olacaktır.
ysdx

3
İkili arama için +1. Algoritmik yeniden tasarım, optimize etmenin en iyi yoludur.
Rocketmagnet

Popüler bir fikir, verimli bir hash rutini bulmanın çok fazla çaba gerektirmesidir, bu nedenle "en iyi uygulama" ikili bir aramadır. Bazen olsa da, "en iyi uygulama" yeterince iyi değildir. Bir paketin başlığının ulaştığı anda (ancak yükünü değil) ağ trafiğini anında yönlendirdiğinizi varsayalım: ikili arama kullanmak, ürününüzü umutsuzca yavaşlatır. Gömülü ürünler genellikle öylesine kısıtlamalara ve gereksinimlere sahiptir ki, örneğin bir x86 yürütme ortamında "en iyi uygulama", gömülü olarak "kolay yoldan çıkmaktır".
Olof Forshell

60

Tabloyu sıralı olarak tutun ve Bentley'in kaydırılmamış ikili aramasını kullanın:

i = 0;
if (key >= a[i+512]) i += 512;
if (key >= a[i+256]) i += 256;
if (key >= a[i+128]) i += 128;
if (key >= a[i+ 64]) i +=  64;
if (key >= a[i+ 32]) i +=  32;
if (key >= a[i+ 16]) i +=  16;
if (key >= a[i+  8]) i +=   8;
if (key >= a[i+  4]) i +=   4;
if (key >= a[i+  2]) i +=   2;
if (key >= a[i+  1]) i +=   1;
return (key == a[i]);

Önemli olan,

  • Masanın ne kadar büyük olduğunu biliyorsanız, o zaman kaç tane yineleme olacağını bilirsiniz, böylece masayı tamamen açabilirsiniz.
  • Daha sonra, ==her yinelemede vaka için bir nokta testi yoktur , çünkü son yineleme dışında, bu vakanın olasılığı, test için zaman harcamak için çok düşüktür. **
  • Son olarak, tabloyu 2 kuvvetine genişleterek, en fazla bir karşılaştırma ve en fazla iki depolama faktörü eklersiniz.

** Olasılıklar açısından düşünmeye alışkın değilseniz, her karar noktasının bir entropisi vardır , bu da onu çalıştırarak öğrendiğiniz ortalama bilgidir. İçin >=testler, her şube olasılığı böylece bir dalını alırsak araçlar size 1 bit öğrenmek ve diğer şube almamın bir bit ve ortalama öğrendikleri, 0.5 ve -log2 (0,5) 1'dir hakkındadır her dalda öğrendiklerinizle o dalın olasılığının toplamıdır. Öyleyse 1*0.5 + 1*0.5 = 1, >=testin entropisi 1'dir. Öğrenmek için 10 bitiniz olduğundan, 10 dal gerekir. Bu yüzden hızlı!

Öte yandan, ya ilk testinizse if (key == a[i+512)? Doğru olma olasılığı 1/1024, yanlış olma olasılığı ise 1023/1024'tür. Yani doğruysa 10 bitin hepsini öğrenirsiniz! Ama yanlışsa -log2 (1023/1024) = .00141 bit öğrenirsiniz, pratikte hiçbir şey yapmazsınız! Yani bu testten öğrendiğiniz ortalama miktar 10/1024 + .00141*1023/1024 = .0098 + .00141 = .0112bittir. Yaklaşık yüzde biri. Bu test onun ağırlığını taşımıyor!


4
Bu çözümü gerçekten beğendim. Değerin konumu hassas bilgilerse, zamanlamaya dayalı adli tıptan kaçınmak için sabit sayıda döngüde çalışacak şekilde değiştirilebilir.
OregonTrail

1
@OregonTrail: Zamanlamaya dayalı adli tıp? Eğlenceli bir sorun ama üzücü bir yorum.
Mike Dunlavey

16
Zamanlama Saldırılarını önlemek için kripto kitaplıklarında buna benzer kaydedilmemiş döngüler görürsünüz en.wikipedia.org/wiki/Timing_attack . İşte güzel bir örnek github.com/jedisct1/libsodium/blob/… Bu durumda, bir saldırganın bir dizenin uzunluğunu tahmin etmesini engelliyoruz. Genellikle saldırgan, bir zamanlama saldırısı gerçekleştirmek için bir işlev çağrısının birkaç milyon örneğini alır.
OregonTrail

3
+1 Harika! Güzel, kayıtsız arama. Bunu daha önce görmemiştim. Kullanabilirim.
Rocketmagnet

1
@OregonTrail: Zamanlama temelli yorumunuzu beğeniyorum. Zamanlama tabanlı saldırılara bilgi sızmasını önlemek için birden fazla kez sabit sayıda döngüde çalışan kriptografik kod yazmak zorunda kaldım.
TonyK

16

Tablonuzdaki sabitler önceden biliniyorsa, tabloya yalnızca bir erişimin yapıldığından emin olmak için mükemmel hashing kullanabilirsiniz . Mükemmel hashing, her ilginç anahtarı benzersiz bir yuvaya eşleyen bir hash işlevi belirler (bu tablo her zaman yoğun değildir, ancak bir tablonun ne kadar yoğun olmadığına karar verebilirsiniz, daha az yoğun tablolar genellikle daha basit hashing işlevlerine yol açar).

Genellikle, belirli anahtarlar için mükemmel hash işlevinin hesaplanması nispeten kolaydır; bunun uzun ve karmaşık olmasını istemezsiniz çünkü bu, birden fazla araştırma yapmak için daha iyi harcanan zaman için rekabet eder.

Mükemmel hashing, "1-sonda maksimum" şemasıdır. K probu yapmak için gereken süre ile hash kodunu hesaplamanın basitliği ile ticaret yapılması gerektiği düşüncesi ile fikir genelleştirilebilir. Sonuçta amaç, en az araştırma veya en basit hash işlevi değil, "aranacak en az toplam süre" dir. Ancak, hiç kimsenin bir k-probes-max karma algoritması oluşturduğunu görmedim. Birinin yapabileceğinden şüpheleniyorum, ama bu muhtemelen araştırma.

Başka bir düşünce: İşlemciniz son derece hızlıysa, mükemmel bir hash'den belleğe bir sonda muhtemelen yürütme süresine hükmeder. İşlemci çok hızlı değilse, k> 1'den fazla prob pratik olabilir.


1
Bir Cortex-M, son derece hızlı değildir .
MSalters

2
Aslında bu durumda herhangi bir hash tablosuna hiç ihtiyacı yoktur. Yalnızca belirli bir anahtarın sette olup olmadığını bilmek istiyor, onu bir değerle eşleştirmek istemiyor. Bu nedenle, mükemmel hash fonksiyonunun her 32 bit değerini 0 veya 1'e eşlemesi yeterlidir; burada "1", "kümede" olarak tanımlanabilir.
David Ongaro

1
İyi bir nokta, böyle bir eşleştirme yapmak için mükemmel bir hash üreteci bulabilirse. Ama bu "son derece yoğun bir küme" olacaktır; Belki de bunu yapan mükemmel bir hash üreteci bulabilir. Kümede ise sabit bir K üreten ve kümede değilse K dışında herhangi bir değer üreten mükemmel bir hash elde etmeye çalışması daha iyi olabilir. İkincisi için bile mükemmel bir hash elde etmenin zor olduğundan şüpheleniyorum.
Ira Baxter

@DavidOngaro table[PerfectHash(value)] == value, değer kümedeyse 1, değilse 0 verir ve PerfectHash işlevini üretmenin iyi bilinen yolları vardır (bkz., Ör . Burtleburtle.net/bob/hash/perfect.html ). Kümedeki tüm değerleri doğrudan 1'e ve 0'a ayarlanmamış tüm değerleri eşleyen bir karma işlevi bulmaya çalışmak çılgınca bir iştir.
Jim Balter

@DavidOngaro: mükemmel bir karma işlevin birçok "yanlış pozitif" değeri vardır , yani kümede olmayan değerler kümedeki değerlerle aynı karmaya sahip olur. Bu nedenle, "sette" giriş değerini içeren, hash değeriyle indekslenmiş bir tablonuz olması gerekir. Dolayısıyla, verilen herhangi bir girdi değerini doğrulamak için (a) hashing uygulayabilirsiniz; (b) tablo aramasını yapmak için karma değerini kullanın; (c) tablodaki girişin giriş değeriyle eşleşip eşleşmediğini kontrol edin.
Craig McQueen

14

Bir karma set kullanın. O (1) arama süresi verecektir.

Aşağıdaki kod, değeri 0'boş' bir değer olarak rezerve edebileceğinizi , yani gerçek verilerde meydana gelmediğini varsayar . Çözüm, durumun böyle olmadığı bir durumda genişletilebilir.

#define HASH(x) (((x >> 16) ^ x) & 1023)
#define HASH_LEN 1024
uint32_t my_hash[HASH_LEN];

int lookup(uint32_t value)
{
    int i = HASH(value);
    while (my_hash[i] != 0 && my_hash[i] != value) i = (i + 1) % HASH_LEN;
    return i;
}

void store(uint32_t value)
{
    int i = lookup(value);
    if (my_hash[i] == 0)
       my_hash[i] = value;
}

bool contains(uint32_t value)
{
    return (my_hash[lookup(value)] == value);
}

Bu örnek uygulamada, arama süresi tipik olarak çok düşük olacaktır, ancak en kötü durumda depolanan girişlerin sayısı kadar olabilir. Gerçek zamanlı bir uygulama için, daha öngörülebilir bir arama süresine sahip olacak ikili ağaçların kullanıldığı bir uygulamayı da düşünebilirsiniz.


3
Bunun etkili olması için bu aramanın kaç kez yapılması gerektiğine bağlıdır.
maxywb

1
Arama dizinin sonundan başlayabilir. Ve bu tür doğrusal hashing, yüksek çarpışma oranlarına sahiptir - O (1) elde etme imkanınız yoktur. İyi hash setleri bu şekilde uygulanmaz.
Jim Balter

@JimBalter Doğru, mükemmel kod değil. Daha çok genel fikir gibi; sadece mevcut hash set koduna işaret edebilirdi. Ancak bunun bir kesme hizmeti rutini olduğu düşünüldüğünde, aramanın çok karmaşık bir kod olmadığını göstermek faydalı olabilir.
jpa

Bunu düzeltmelisin ki etrafı saracak.
Jim Balter

Mükemmel bir hash fonksiyonunun amacı, tek bir araştırma yapmasıdır. Dönemi.
Ira Baxter

10

Bu durumda, Bloom filtrelerini araştırmak faydalı olabilir . Bir değerin olmadığını hızlı bir şekilde belirleyebilirler, bu iyi bir şeydir çünkü 2 ^ 32 olası değerlerin çoğu o 1024 elemanlı dizide değildir. Bununla birlikte, fazladan bir kontrole ihtiyaç duyacak bazı yanlış pozitifler vardır.

Tablonuz görünüşte statik olduğundan, Bloom filtreniz için hangi yanlış pozitiflerin var olduğunu belirleyebilir ve bunları mükemmel bir hash içine yerleştirebilirsiniz.


1
İlginç, daha önce Bloom filtrelerini görmemiştim.
Rocketmagnet

8

İşlemcinizin 204 MHz'de çalıştığını varsayarsak, bu LPC4357 için maksimum gibi görünüyor ve ayrıca zamanlama sonucunuzun ortalama durumu (geçen dizinin yarısı) yansıttığını varsayarsak:

  • CPU frekansı: 204 MHz
  • Döngü süresi: 4,9 ns
  • Döngü olarak süre: 12,5 µs / 4,9 ns = 2551 döngü
  • Yineleme başına döngü: 2551/128 = 19,9

Dolayısıyla, arama döngünüz yineleme başına yaklaşık 20 döngü harcar. Kulağa kötü gelmiyor, ama sanırım daha hızlı hale getirmek için montaja bakmanız gerekiyor.

Dizini bırakmanızı ve bunun yerine bir işaretçi karşılaştırması kullanmanızı ve tüm işaretçileri yapmanızı öneririm const.

bool arrayContains(const uint32_t *array, size_t length)
{
  const uint32_t * const end = array + length;
  while(array != end)
  {
    if(*array++ == 0x1234ABCD)
      return true;
  }
  return false;
}

Bu en azından test etmeye değer.


1
-1, ARM indekslenmiş bir adres moduna sahip, bu yüzden bu anlamsız. İşaretçiyi yapmaya gelince const, GCC zaten değişmediğini fark ediyor. constDoesnt't eklenti şey ya.
MSalters

11
Tamam @MSalters, ben oluşturulan kodla doğrulamadım, C noktası seviyesinde kadar kolay olacağı bir şey ifade etmek, ve ben sadece işaretçileri yerine bir işaretçi yönetmek düşünüyorum ve bir endeks olan daha basit. " constHiçbir şey eklemediğine " katılmıyorum : okuyucuya değerin değişmeyeceğini çok açık bir şekilde söylüyor. Bu harika bir bilgi.
gevşeyin

9
Bu, derinlemesine gömülü koddur; Şimdiye kadarki optimizasyonlar, kodun flash'tan RAM'e taşınmasını içeriyor. Yine de daha hızlı olması gerekiyor. Bu noktada amaç okunabilirlik değildir .
MSalters

1
@MSalters "ARM'de dizinlenmiş bir adres modu var, bu yüzden bu anlamsız" - eğer noktayı tamamen kaçırırsanız ... OP "Ayrıca işaretçi aritmetiği ve bir for döngüsü kullanıyorum" yazdı. çözme, indekslemeyi işaretçilerle değiştirmedi, sadece indeks değişkenini ve böylece her döngü yinelemesinde fazladan bir çıkarımı ortadan kaldırdı. Ancak OP bilgeydi (cevap veren ve yorum yapan pek çok insanın aksine) ve sonunda ikili bir arama yaptı.
Jim Balter

6

Başkaları, tablonuzu yeniden düzenlemenizi, sona bir gözcü değer eklemenizi veya ikili arama sağlamak için sıralamanızı önerdiler.

"Ayrıca işaretçi aritmetiği ve yukarı yerine aşağı sayma yapan (kontrol i != 0etmekten daha hızlı olup olmadığını kontrol eden i < 256) bir for döngüsü kullanıyorum ."

İlk tavsiyem: işaretçi aritmetiği ve aşağı sayma işlemlerinden kurtulun. Gibi şeyler

for (i=0; i<256; i++)
{
    if (compareVal == the_array[i])
    {
       [...]
    }
}

derleyici için deyimsel olma eğilimindedir . Döngü deyimseldir ve bir dizinin bir döngü değişkeni üzerinden indekslenmesi deyimseldir. Gösterici aritmetiği ve işaretçileri ile Jonglörlük eğiliminde olacaktır karartmak derleyici deyim ve ne ilişkin kodu oluşturmak yapmak sen derleyici yazar genel için en isabetli karar olandan ziyade yazdığı görev .

Örneğin, yukarıdaki kod , indeksleme kapalı olarak sıfırdan -256veya -255sıfıra doğru çalışan bir döngü halinde derlenebilir &the_array[256]. Muhtemelen geçerli C'de ifade edilemeyen ancak ürettiğiniz makinenin mimarisine uyan şeyler.

Yani mikrooptimize etmeyin . Optimize edicinizin çalışmalarına sadece anahtarlar atıyorsunuz. Akıllı olmak istiyorsanız, veri yapıları ve algoritmalar üzerinde çalışın, ancak ifadelerini mikro optimize etmeyin. Sadece mevcut derleyicide / mimaride değilse, bir sonrakinde sizi ısırmak için geri gelecektir.

Özellikle diziler ve dizinler yerine işaretçi aritmetiğinin kullanılması, derleyicinin hizalamalardan, depolama konumlarından, diğer adlarla ilgili hususlardan ve diğer şeylerden tam olarak haberdar olması ve makine mimarisine en uygun şekilde güç azaltma gibi optimizasyonlar yapması için zehirdir.


İşaretçiler üzerinden döngüler, C'de deyimseldir ve iyi optimize eden derleyiciler, dizin oluşturmanın yanı sıra bunları işleyebilir. Ancak tüm bunlar tartışmalı çünkü OP ikili bir arama yaptı.
Jim Balter

3

Vektorizasyon, memchr uygulamalarında sıklıkla olduğu gibi burada da kullanılabilir. Aşağıdaki algoritmayı kullanırsınız:

  1. İşletim sisteminizin bit sayısının uzunluğuna eşit (64-bit, 32-bit, vb.) Sorgunuzun tekrar ettiği bir maske oluşturun. 64 bitlik bir sistemde 32 bit sorguyu iki kez tekrar edersiniz.

  2. Listeyi, yalnızca listeyi daha büyük bir veri türü listesine aktararak ve değerleri dışarı çekerek birden çok veri parçasının bir listesi olarak işleyin. Her yığın için, maskeyle birlikte XOR, ardından 0b0111 ... 1 ile XOR, sonra 1 ekle, sonra & 0b1000 ... 0 maskesiyle tekrarla. Sonuç 0 ise kesinlikle eşleşme yoktur. Aksi takdirde, (genellikle çok yüksek olasılıkla) bir eşleşme olabilir, bu nedenle parçayı normal şekilde arayın.

Örnek uygulama: https://sourceware.org/cgi-bin/cvsweb.cgi/src/newlib/libc/string/memchr.c?rev=1.3&content-type=text/x-cvsweb-markup&cvsroot=src


3

Değerlerinizin etki alanını uygulamanızın kullanabileceği bellek miktarıyla uyumlu hale getirebilirseniz, en hızlı çözüm dizinizi bir bit dizisi olarak temsil etmek olacaktır:

bool theArray[MAX_VALUE]; // of which 1024 values are true, the rest false
uint32_t compareVal = 0x1234ABCD;
bool validFlag = theArray[compareVal];

DÜZENLE

Eleştirmenlerin sayısı beni hayrete düşürüyor. Bu iş parçacığının başlığı "C dizisinde bir değer olup olmadığını nasıl hızlı bir şekilde bulabilirim?" bunun için cevabımın yanında duracağım çünkü tam olarak bunu cevaplıyor. Bunun en hızlı hızlı hash fonksiyonuna sahip olduğunu iddia edebilirim (adres === değerinden beri). Yorumları okudum ve bariz uyarıların farkındayım. Kuşkusuz bu uyarılar, çözmek için kullanılabilecek sorunların aralığını sınırlar, ancak çözdüğü sorunlar için çok verimli bir şekilde çözer.

Bu yanıtı tamamen reddetmek yerine, hız ve performans arasında daha iyi bir denge elde etmek için karma işlevler kullanarak geliştirebileceğiniz en uygun başlangıç ​​noktası olarak düşünün.


8
Bu nasıl 4 olumlu oy alır? Soru, bunun bir Cortex M4 olduğunu belirtir. Şeyde 262.144 KB değil 136 KB RAM var.
MSalters

1
Açıkça yanlış cevaplara kaç tane olumlu oy verildiği şaşırtıcı çünkü cevap verenin ormanı ağaçlar için kaçırması. OP'nin en büyük durumu için O (log n) << O (n).
msw

3
Çok daha iyi çözümler varken, gülünç miktarda bellek yakan programcılara karşı çok huysuz oluyorum. Her 5 yılda bir, bilgisayarımın hafızası bitiyor ve 5 yıl önce bu miktar çok fazlaydı.
Craig McQueen

1
@CraigMcQueen Kids bugünlerde. Hafıza kaybı. Çirkin! Benim günlerimde 1 MiB belleğimiz ve 16 bitlik bir kelime boyutumuz vardı. / s
Cole Johnson

2
Sert eleştirmenlerin nesi var? OP, kodun bu bölümü için hızın kesinlikle kritik olduğunu açıkça belirtiyor ve StephenQuan zaten "saçma bir bellek miktarı" ndan bahsetti.
Bogdan Alexandru

1

CM4 Harvard mimarisinin tam potansiyeliyle kullanılması için talimatların ("sözde kod") ve verilerin ("Dizi") ayrı (RAM) belleklerde olduğundan emin olun. Kullanım kılavuzundan:

görüntü açıklamasını buraya girin

CPU performansını optimize etmek için ARM Cortex-M4, Yönerge (kod) (I) erişimi, Veri (D) erişimi ve Sistem (S) erişimi için üç veriyoluna sahiptir. Komutlar ve veriler ayrı belleklerde tutulduğunda, kod ve veri erişimi tek döngüde paralel olarak yapılabilir. Kod ve veriler aynı bellekte tutulduğunda, verileri yükleyen veya depolayan talimatlar iki döngü alabilir.


İlginç, Cortex-M7'nin isteğe bağlı talimat / veri önbellekleri var, ancak ondan önce kesinlikle yok. en.wikipedia.org/wiki/ARM_Cortex-M#Silicon_customization .
Peter Cordes

0

Cevabım zaten cevaplandıysa özür dilerim - sadece tembel bir okuyucuyum. Olumsuz oy vermekte özgürsünüz o zaman))

1) 'i' sayacını kaldırabilirsiniz - sadece işaretçileri karşılaştırın, yani

for (ptr = &the_array[0]; ptr < the_array+1024; ptr++)
{
    if (compareVal == *ptr)
    {
       break;
    }
}
... compare ptr and the_array+1024 here - you do not need validFlag at all.

tüm bunlar önemli bir gelişme sağlamayacak olsa da, bu tür bir optimizasyon muhtemelen derleyicinin kendisi tarafından gerçekleştirilebilir.

2) Diğer yanıtlarda da belirtildiği gibi, neredeyse tüm modern CPU'lar RISC tabanlıdır, örneğin ARM. Modern Intel X86 CPU'ları bile bildiğim kadarıyla (X86'dan anında derlemek) RISC çekirdeklerini kullanıyor. RISC için ana optimizasyon, kod atlamalarını en aza indiren ardışık düzen optimizasyonudur (ve Intel ve diğer CPU'lar için de). Bu tür bir optimizasyon türü (muhtemelen en önemli olanı), "döngü geri dönüşü" dür. İnanılmaz derecede aptalca ve verimli, Intel derleyicisi bile bu AFAIK'i yapabilir. Şöyle görünüyor:

if (compareVal == the_array[0]) { validFlag = true; goto end_of_compare; }
if (compareVal == the_array[1]) { validFlag = true; goto end_of_compare; }
...and so on...
end_of_compare:

Bu şekilde optimizasyon, ardışık düzenin en kötü durum için bozulmamasıdır (dizide CompareVal yoksa), bu nedenle olabildiğince hızlıdır (elbette karma tablolar, sıralanmış diziler vb. Gibi algoritma optimizasyonlarını saymaz, dizinin boyutuna bağlı olarak daha iyi sonuçlar verebilecek diğer yanıtlarda bahsedildi. Döngüler Geri Alma yaklaşımı orada da uygulanabilir. Burada başkalarında görmediğimi düşündüğümü yazıyorum)

Bu optimizasyonun ikinci kısmı, dizi öğesinin doğrudan adres tarafından alınmasıdır (derleme aşamasında hesaplanır, statik bir dizi kullandığınızdan emin olun) ve dizinin temel adresinden işaretçi hesaplamak için ek ADD işlemine gerek yoktur. AFAIK ARM mimarisi, dizileri adreslemeyi hızlandırmak için özel özelliklere sahip olduğundan, bu optimizasyonun önemli bir etkisi olmayabilir. Ama her neyse, en iyisini sadece doğrudan C kodunda yaptığınızı bilmek her zaman daha iyidir, değil mi?

Döngü Geri Alma, ROM israfı nedeniyle garip görünebilir (evet, kartınız bu özelliği destekliyorsa, RAM'in hızlı bir kısmına yerleştirmeyi doğru yaptınız), ancak aslında RISC konseptine dayanan hız için adil bir ödeme. Bu sadece genel bir hesaplama optimizasyonu noktasıdır - ihtiyaçlarınıza bağlı olarak hız uğruna yerden ödün verirsiniz ve bunun tersi de geçerlidir.

1024 öğe dizisi için geri dönüşün sizin durumunuz için çok büyük bir fedakarlık olduğunu düşünüyorsanız, diziyi her biri 512 öğeden oluşan 2 parçaya veya 4x256'ya bölerek 'kısmi geri dönüşü' düşünebilirsiniz.

3) modern CPU genellikle SIMD işlemlerini destekler, örneğin ARM NEON komut seti - aynı işlemlerin paralel olarak yürütülmesine izin verir. Açıkçası karşılaştırma operasyonları için uygun olup olmadığını hatırlamıyorum ama öyle olduğunu düşünüyorum, kontrol etmelisiniz. Googling, maksimum hız elde etmek için bazı hileler olabileceğini gösteriyor, bkz. Https://stackoverflow.com/a/5734019/1028256

Umarım size yeni fikirler verebilir.


OP, doğrusal döngüleri optimize etmeye odaklanan tüm aptalca cevapları atladı ve bunun yerine diziyi önceden sıraladı ve ikili arama yaptı.
Jim Balter

@ Jim, bu tür bir optimizasyonun önce yapılması gerektiği aşikar. Örneğin diziyi sıralamak için zamanınız olmadığında bazı kullanım durumlarında 'aptalca' cevaplar o kadar aptalca görünmeyebilir. Ya da aldığınız hız zaten yeterli değilse
Mixaz

"Bu tür bir optimizasyonun önce yapılması gerektiği açıktır" - tabii ki doğrusal çözümler geliştirmek için büyük çaba harcayan insanlar için değil. "diziyi sıralamak için zamanınız yok" - Bunun ne anlama geldiği hakkında hiçbir fikrim yok. "Ya da aldığınız hız zaten yeterli değilse" - Ah, eğer bir ikili aramadan gelen hız "yeterli değilse", optimize edilmiş bir doğrusal arama yapmak onu iyileştirmez. Şimdi bu konuyla işim bitti.
Jim Balter

@JimBalter, OP gibi bir problemim olsaydı, kesinlikle ikili arama gibi algoritmalar kullanmayı düşünürdüm. OP'nin bunu zaten dikkate almadığını düşünemedim. "diziyi sıralamak için zamanınız yok" dizinin sıralanmasının zaman aldığı anlamına gelir. Her giriş veri seti için yapmanız gerekiyorsa, doğrusal bir döngüden daha uzun sürebilir. "Ya da aldığınız hız zaten yeterli değilse" şu anlama gelir - yukarıdaki optimizasyon ipuçları ikili arama kodunu veya herhangi bir şekilde hızlandırmak için kullanılabilir
Mixaz

0

Ben büyük bir esrar hayranıyım. Elbette sorun, hem hızlı hem de minimum miktarda bellek kullanan (özellikle gömülü bir işlemcide) verimli bir algoritma bulmaktır.

Oluşabilecek değerleri önceden biliyorsanız, en iyisini veya daha doğrusu verileriniz için en iyi parametreleri bulmak için çok sayıda algoritmadan geçen bir program oluşturabilirsiniz.

Öyle bir program oluşturdum ki bu yazıda okuyabilirsiniz ve çok hızlı sonuçlar elde ettim . 16000 giriş, ikili arama kullanarak değeri bulmak için kabaca 2 ^ 14 veya ortalama 14 karşılaştırmaya çevirir. Açıkça çok hızlı aramaları hedefledim - ortalama olarak <= 1.5 aramalarda değeri bulmak - bu da daha fazla RAM gereksinimi ile sonuçlandı. Daha muhafazakar bir ortalama değerle (<= 3 diyelim) çok fazla bellek kaydedilebileceğine inanıyorum. Karşılaştırıldığında, 256 veya 1024 girişinizdeki bir ikili arama için ortalama durum, sırasıyla 8 ve 10'luk ortalama bir karşılaştırma sayısıyla sonuçlanır.

Ortalama aramam, genel bir algoritma ile (bir değişkene göre bir bölüm kullanan) yaklaşık 60 döngü (intel i5'e sahip bir dizüstü bilgisayarda) ve özel bir (muhtemelen bir çarpma kullanarak) 40-45 döngü gerektirdi. Bu, elbette çalıştırdığı saat frekansına bağlı olarak MCU'nuzda mikrosaniyenin altındaki arama sürelerine dönüşmelidir.

Giriş dizisi bir girişe kaç kez erişildiğini takip ederse, gerçek hayatta daha da ince ayar yapılabilir. Giriş dizisi, indeces hesaplanmadan önce en çok erişilenden en az erişilene doğru sıralanırsa, tek bir karşılaştırmayla en sık görülen değerleri bulur.


0

Bu bir cevaptan çok bir zeyilname gibidir.

Geçmişte benzer bir durum yaşadım , ancak dizim hatırı sayılır sayıda aramada sabit kaldı.

Bunların yarısında, aranan değer dizide MEVCUT DEĞİLDİR. Sonra herhangi bir arama yapmadan önce bir "filtre" uygulayabileceğimi fark ettim.

Bu "filtre", sadece BİR KEZ hesaplanan ve her aramada kullanılan basit bir tam sayıdır .

Java'da, ancak oldukça basit:

binaryfilter = 0;
for (int i = 0; i < array.length; i++)
{
    // just apply "Binary OR Operator" over values.
    binaryfilter = binaryfilter | array[i];
}

Bu yüzden, ikili arama yapmadan önce binaryfilter'ı kontrol ediyorum:

// Check binaryfilter vs value with a "Binary AND Operator"
if ((binaryfilter & valuetosearch) != valuetosearch)
{
    // valuetosearch is not in the array!
    return false;
}
else
{
    // valuetosearch MAYBE in the array, so let's check it out
    // ... do binary search stuff ...

}

'Daha iyi' bir karma algoritma kullanabilirsiniz, ancak bu çok hızlı olabilir, özellikle büyük sayılar için. Belki bu size daha fazla döngü kazandırabilir.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.