web-dev-qa-db-de.com

Finden Sie schnell heraus, ob ein Wert in einem C-Array vorhanden ist?

Ich habe eine eingebettete Anwendung mit einem zeitkritischen ISR, die ein Array der Größe 256 durchlaufen muss (vorzugsweise 1024, aber mindestens 256) und prüfe, ob ein Wert mit dem Inhalt des Arrays übereinstimmt. Ein bool wird auf true gesetzt, wenn dies der Fall ist.

Der Mikrocontroller ist ein NXP LPC4357, ARM= Cortex M4-Kern, und der Compiler ist GCC. Ich habe bereits Optimierungsstufe 2 (3 ist langsamer) kombiniert und die Funktion in RAM anstelle von Flash Ich verwende auch Zeigerarithmetik und eine for -Schleife, die abwärts zählt anstatt aufwärts (prüfe, ob i!=0 ist schneller als zu überprüfen, ob i<256). Alles in allem habe ich eine Dauer von 12,5 µs, die drastisch reduziert werden muss, um machbar zu sein. Dies ist der (Pseudo-) Code, den ich jetzt benutze:

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;
     }
}

Was wäre der absolut schnellste Weg dazu? Die Verwendung von Inline-Assembly ist zulässig. Andere "weniger elegante" Tricks sind ebenfalls erlaubt.

124
wlamers

In Situationen, in denen die Leistung von größter Bedeutung ist, wird der C-Compiler wahrscheinlich nicht den schnellsten Code produzieren, verglichen mit dem, was Sie mit handgestimmter Assemblersprache tun können. Ich tendiere dazu, den Weg des geringsten Widerstands zu beschreiten - für kleine Routinen wie diese schreibe ich einfach asm-Code und habe eine gute Vorstellung davon, wie viele Zyklen für die Ausführung erforderlich sind. Möglicherweise können Sie mit dem C-Code experimentieren und den Compiler dazu bringen, eine gute Ausgabe zu generieren, aber möglicherweise verschwenden Sie viel Zeit damit, die Ausgabe auf diese Weise zu optimieren. Compiler (insbesondere von Microsoft) haben in den letzten Jahren einen langen Weg zurückgelegt, sind aber immer noch nicht so intelligent wie der Compiler zwischen Ihren Ohren, da Sie an Ihrer spezifischen Situation arbeiten und nicht nur an einem allgemeinen Fall. Der Compiler verwendet möglicherweise bestimmte Anweisungen (z. B. LDM) nicht, die dies beschleunigen können, und es ist unwahrscheinlich, dass er intelligent genug ist, um die Schleife zu entrollen. Hier ist eine Möglichkeit, dies zu tun, die die drei Ideen enthält, die ich in meinem Kommentar erwähnt habe: Loop-Unrolling, Cache-Prefetch und Verwendung des Befehls Multiple Load (LDM). Die Anzahl der Befehlszyklen beträgt ungefähr 3 Takte pro Array-Element, dies berücksichtigt jedoch keine Speicherverzögerungen.

Theorie der Funktionsweise: ARMs CPU-Design führt die meisten Befehle in einem Taktzyklus aus, die Befehle werden jedoch in einer Pipeline ausgeführt. C-Compiler versuchen, die Verzögerungen in der Pipeline zu beseitigen, indem sie andere Anweisungen dazwischen verschachteln. Wenn eine enge Schleife wie beim ursprünglichen C-Code angezeigt wird, kann der Compiler die Verzögerungen nur schwer verbergen, da der aus dem Speicher gelesene Wert sofort verglichen werden muss. Mein Code unten wechselt zwischen 2 Sätzen von 4 Registern, um die Verzögerungen des Speichers selbst und der Pipeline, die die Daten abruft, erheblich zu verringern. Wenn Sie mit großen Datenmengen arbeiten und Ihr Code die meisten oder alle verfügbaren Register nicht nutzt, erhalten Sie im Allgemeinen nicht die maximale Leistung.

; 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

pdate: Es gibt viele Skeptiker in den Kommentaren, die meinen, dass meine Erfahrung anekdotisch/wertlos ist und Beweise verlangt. Ich habe GCC 4.8 (vom Android NDK 9C)) verwendet, um die folgende Ausgabe mit der Optimierung -O2 zu generieren (alle Optimierungen aktiviert einschließlich Loop-Unrolling). Ich habe das Original kompiliert C-Code, der in der obigen Frage dargestellt wurde:

.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

Der GCC-Ausgang rollt nicht nur die Schleife nicht ab, sondern verschwendet auch eine Uhr für einen Stand nach dem LDR. Es sind mindestens 8 Takte pro Array-Element erforderlich. Es ist eine gute Aufgabe, die Adresse zu verwenden, um zu wissen, wann die Schleife zu verlassen ist, aber all die magischen Dinge, die Compiler tun können, sind in diesem Code nirgends zu finden. Ich habe den Code nicht auf der Zielplattform ausgeführt (ich besitze keinen), aber jeder, der Erfahrung mit ARM Codeleistung hat, kann feststellen, dass mein Code schneller ist.

pdate 2: Ich habe Microsoft Visual Studio 2013 SP2 die Möglichkeit gegeben, mit dem Code besser umzugehen. Es war möglich, NEON-Anweisungen zu verwenden, um meine Array-Initialisierung zu vektorisieren, aber die vom OP geschriebene Suche nach linearen Werten lief ähnlich ab wie die von GCC generierte (ich habe die Bezeichnungen umbenannt, um sie lesbarer zu machen):

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

Wie gesagt, ich besitze nicht die genaue Hardware des OP, aber ich werde die Leistung auf einem nVidia Tegra 3 und Tegra 4 der 3 verschiedenen Versionen testen und die Ergebnisse demnächst hier veröffentlichen.

pdate 3: Ich habe meinen Code und den von Microsoft kompilierten ARM Code auf einem Tegra 3 und Tegra 4 (Surface RT, Surface RT) ausgeführt 2) Ich habe 1000000 Iterationen einer Schleife ausgeführt, bei der keine Übereinstimmung gefunden wurde, sodass sich alles im Cache befindet und die Messung einfach ist.

             My Code       MS Code
Surface RT    297ns         562ns
Surface RT 2  172ns         296ns  

In beiden Fällen läuft mein Code fast doppelt so schnell. Die meisten modernen ARM CPUs liefern wahrscheinlich ähnliche Ergebnisse.

104
BitBank

Es gibt einen Trick, um es zu optimieren (ich wurde einmal in einem Vorstellungsgespräch gefragt):

  • Wenn der letzte Eintrag im Array den gesuchten Wert enthält, geben Sie true zurück
  • Schreiben Sie den gesuchten Wert in den letzten Eintrag des Arrays
  • Durchlaufen Sie das Array, bis Sie auf den gesuchten Wert stoßen
  • Wenn Sie es vor dem letzten Eintrag im Array gefunden haben, geben Sie true zurück
  • Falsch zurückgeben

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;
}

Dies ergibt einen Zweig pro Iteration anstelle von zwei Zweigen pro Iteration.


PDATE:

Wenn Sie das Array SIZE+1 Zuweisen dürfen, können Sie den Teil "Last Entry Swap" entfernen:

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

Sie können auch die in theArray[i] Eingebettete zusätzliche Arithmetik entfernen, indem Sie stattdessen Folgendes verwenden:

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

Wenn der Compiler es nicht bereits anwendet, wird diese Funktion dies mit Sicherheit tun. Andererseits könnte es für den Optimierer schwieriger sein, die Schleife zu entrollen, sodass Sie dies im generierten Assembly-Code überprüfen müssen ...

87
barak manos

Sie bitten um Hilfe bei der Optimierung Ihres Algorithmus, wodurch Sie möglicherweise zum Assembler weitergeleitet werden. Ihr Algorithmus (eine lineare Suche) ist jedoch nicht so clever, daher sollten Sie überlegen, Ihren Algorithmus zu ändern. Z.B.:

Perfekte Hash-Funktion

Wenn Ihre 256 "gültigen" Werte statisch sind und zur Kompilierungszeit bekannt sind, können Sie eine perfekte Hash-Funktion verwenden. Sie müssen eine Hash-Funktion finden, die Ihren Eingabewert auf einen Wert im Bereich 0 abbildet. n, wobei für alle gültigen keine Kollisionen vorhanden sind Werte, die Sie interessieren. Das heißt, keine zwei "gültigen" Werte haben den gleichen Ausgabewert. Wenn Sie nach einer guten Hash-Funktion suchen, möchten Sie:

  • Halten Sie die Hash-Funktion einigermaßen schnell.
  • Minimieren n. Das kleinste, das Sie bekommen können, ist 256 (minimale perfekte Hash-Funktion), aber das ist wahrscheinlich schwer zu erreichen, abhängig von den Daten.

Beachten Sie für effiziente Hash-Funktionen, dass n häufig eine Zweierpotenz ist, was einer bitweisen Maske niedriger Bits entspricht (UND-Verknüpfung). Beispiel für Hash-Funktionen:

  • CRC der Eingangsbytes, Modulo n.
  • ((x << i) ^ (x >> j) ^ (x << k) ^ ...) % n (Auswählen von beliebig vielen i, j, k, ... mit Links- oder Rechtsverschiebung)

Dann erstellen Sie eine feste Tabelle mit n Einträgen, wobei der Hash die Eingabewerte einem Index i in der Tabelle zuordnet. Für gültige Werte enthält der Tabelleneintrag i den gültigen Wert. Stellen Sie für alle anderen Tabelleneinträge sicher, dass jeder Eintrag von index i einen anderen ungültigen Wert enthält, der nicht zu i hasht.

Dann in Ihrer Interruptroutine mit Eingabe x:

  1. Hash x bis Index i (liegt im Bereich 0..n)
  2. Schlagen Sie den Eintrag i in der Tabelle nach und prüfen Sie, ob er den Wert x enthält.

Dies ist viel schneller als eine lineare Suche mit 256 oder 1024 Werten.

Ich habe etwas Python Code geschrieben, um vernünftige Hash-Funktionen zu finden.

Binäre Suche

Wenn Sie Ihr Array mit 256 "gültigen" Werten sortieren, können Sie eine binäre Suche anstelle einer linearen Suche durchführen. Das heißt, Sie sollten in der Lage sein, eine 256-Eintrag-Tabelle in nur 8 Schritten (log2(256)) oder eine 1024-Eintrag-Tabelle in 10 Schritten zu durchsuchen. Dies ist wiederum viel schneller als eine lineare Suche mit 256 oder 1024 Werten.

62
Craig McQueen

Halten Sie die Tabelle in sortierter Reihenfolge, und verwenden Sie die entrollte Binärsuche von Bentley:

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]);

Der Punkt ist,

  • wenn Sie wissen, wie groß der Tisch ist, wissen Sie, wie viele Iterationen es geben wird, damit Sie ihn vollständig ausrollen können.
  • Dann macht es keinen Sinn, den == - Fall bei jeder Iteration zu testen, da die Wahrscheinlichkeit, dass dieser Fall auftritt, mit Ausnahme der letzten Iteration zu gering ist, um einen Zeitaufwand für das Testen zu rechtfertigen. **
  • Wenn Sie die Tabelle auf eine Zweierpotenz erweitern, addieren Sie höchstens einen Vergleich und höchstens den Faktor zwei für den Speicher.

** Wenn Sie es nicht gewohnt sind, in Wahrscheinlichkeiten zu denken, hat jeder Entscheidungspunkt ein Entropie, was die durchschnittliche Information ist, die Sie durch Ausführen lernen. Bei den >= - Tests beträgt die Wahrscheinlichkeit für jeden Zweig ungefähr 0,5, und -log2 (0,5) ist 1. Wenn Sie also einen Zweig nehmen, lernen Sie 1 Bit, und wenn Sie den anderen Zweig nehmen, lernen Sie Ein Bit, und der Durchschnitt ist nur die Summe dessen, was Sie auf jedem Zweig lernen, multipliziert mit der Wahrscheinlichkeit dieses Zweigs. Also 1*0.5 + 1*0.5 = 1, Also ist die Entropie des Tests >= 1. Da Sie 10 Bits lernen müssen, werden 10 Zweige benötigt. Deshalb ist es schnell!

Was ist dagegen, wenn Ihr erster Test if (key == a[i+512) ist? Die Wahrscheinlichkeit, wahr zu sein, beträgt 1/1024, während die Wahrscheinlichkeit, falsch zu sein, 1023/1024 beträgt. Wenn es stimmt, lernst du alle 10 Bits! Aber wenn es falsch ist, lernt man -log2 (1023/1024) = .00141 Bits, praktisch nichts! Die durchschnittliche Menge, die Sie aus diesem Test lernen, beträgt 10/1024 + .00141*1023/1024 = .0098 + .00141 = .0112 Bits. ngefähr ein Hundertstel eines Bits. Dieser Test ist trägt nicht sein Gewicht!

60
Mike Dunlavey

Wenn die Menge der Konstanten in Ihrer Tabelle im Voraus bekannt ist, können Sie perfektes Hashing verwenden, um sicherzustellen, dass nur ein Zugriff auf die Tabelle erfolgt. Perfect Hashing bestimmt eine Hash-Funktion, die jeden interessanten Schlüssel einem eindeutigen Slot zuordnet (diese Tabelle ist nicht immer dicht, aber Sie können entscheiden, wie dicht eine Tabelle sein soll, wobei weniger dichte Tabellen in der Regel zu einfacheren Hash-Funktionen führen).

Normalerweise ist die perfekte Hash-Funktion für den jeweiligen Schlüsselsatz relativ einfach zu berechnen. Sie möchten nicht, dass das lang und kompliziert ist, da es um die Zeit konkurriert, die Sie vielleicht lieber mit mehreren Sonden verbringen.

Perfect Hashing ist ein "1-Probe-Max" -Schema. Man kann die Idee mit dem Gedanken verallgemeinern, dass man die Einfachheit der Berechnung des Hash-Codes mit der Zeit tauschen sollte, die zur Herstellung von k Sonden benötigt wird. Schließlich ist das Ziel die "geringste Gesamtzeit zum Nachschlagen", nicht die geringste Anzahl von Tests oder die einfachste Hash-Funktion. Ich habe jedoch noch nie jemanden gesehen, der einen k-probes-max-Hashing-Algorithmus erstellt hat. Ich vermute, man kann es schaffen, aber das ist wahrscheinlich Forschung.

Ein anderer Gedanke: Wenn Ihr Prozessor extrem schnell ist, wird die Ausführungszeit wahrscheinlich von der einen Probe zum Speicher von einem perfekten Hash dominiert. Wenn der Prozessor nicht sehr schnell ist, können k> 1 Sonden sinnvoll sein.

16
Ira Baxter

Verwenden Sie ein Hash-Set. Es wird O(1) Nachschlagezeit geben.

Der folgende Code setzt voraus, dass Sie den Wert 0 Als "leeren" Wert reservieren können, d. H. In tatsächlichen Daten nicht vorkommen. Die Lösung kann für eine Situation erweitert werden, in der dies nicht der Fall ist.

#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);
}

In dieser beispielhaften Implementierung ist die Nachschlagezeit normalerweise sehr gering, kann aber im schlimmsten Fall bis zur Anzahl der gespeicherten Einträge betragen. Für eine Echtzeitanwendung können Sie auch eine Implementierung unter Verwendung von Binärbäumen in Betracht ziehen, die eine besser vorhersehbare Nachschlagezeit haben.

14
jpa

In diesem Fall kann es sich lohnen, Bloom-Filter zu untersuchen. Sie können schnell feststellen, dass ein Wert nicht vorhanden ist, was gut ist, da sich die meisten der 2 ^ 32 möglichen Werte nicht in diesem 1024-Element-Array befinden. Es gibt jedoch einige Fehlalarme, für die eine zusätzliche Prüfung erforderlich ist.

Da Ihre Tabelle anscheinend statisch ist, können Sie feststellen, welche False Positives für Ihren Bloom-Filter existieren, und diese in einen perfekten Hash einfügen.

9
MSalters

Angenommen, Ihr Prozessor läuft mit 204 MHz, was für den LPC4357 das Maximum zu sein scheint, und auch, wenn Ihr Timing-Ergebnis den Durchschnittsfall widerspiegelt (die Hälfte des durchquerten Arrays), erhalten wir:

  • CPU-Frequenz: 204 MHz
  • Zykluszeit: 4,9 ns
  • Dauer in Zyklen: 12,5 µs/4,9 ns = 2551 Zyklen
  • Zyklen pro Iteration: 2551/128 = 19,9

Ihre Suchschleife benötigt also ungefähr 20 Zyklen pro Iteration. Das hört sich nicht schrecklich an, aber ich denke, um es schneller zu machen, muss man sich die Versammlung ansehen.

Ich würde empfehlen, den Index zu löschen und stattdessen einen Zeigervergleich zu verwenden und alle Zeiger const zu erstellen.

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;
}

Das ist zumindest einen Test wert.

8
unwind

Andere Personen haben vorgeschlagen, Ihre Tabelle neu zu organisieren, am Ende einen Sentinel-Wert hinzuzufügen oder sie zu sortieren, um eine binäre Suche bereitzustellen.

Sie geben an: "Ich verwende auch Zeigerarithmetik und eine for-Schleife, die anstelle von up herunterzählt (prüft, ob i != 0 ist schneller als zu überprüfen, ob i < 256). "

Mein erster Rat ist: Zeigerarithmetik und Abwärtszählung loswerden. Zeug wie

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

neigt dazu, idiomatisch für den Compiler zu sein. Die Schleife ist idiomatisch und die Indizierung eines Arrays über eine Schleifenvariable ist idiomatisch. Das Jonglieren mit Zeigerarithmetik und Zeigern führt dazu, dass verschleiert die Redewendungen an den Compiler gesendet werden und Code generiert wird, der sich auf das bezieht, was Sie geschrieben hat, anstatt auf das, was der Compiler-Schreiber als das Beste bezeichnet hat Kurs für die allgemeine Aufgabe.

Zum Beispiel könnte der obige Code in eine Schleife kompiliert werden, die von -256 oder -255 auf Null, Indizierung aus &the_array[256]. Möglicherweise Dinge, die in gültigem C nicht einmal ausgedrückt werden können, aber der Architektur der Maschine entsprechen, für die Sie generieren.

Also nicht Mikrooptimieren. Sie werfen nur Schraubenschlüssel in die Werke Ihres Optimierers. Wenn Sie clever sein möchten, arbeiten Sie an den Datenstrukturen und Algorithmen, optimieren Sie jedoch nicht deren Ausdruck. Es wird nur zurückkommen, um Sie zu beißen, wenn nicht über den aktuellen Compiler/die aktuelle Architektur, dann über die nächste.

Insbesondere die Verwendung von Zeigerarithmetik anstelle von Arrays und Indizes ist ein Gift für den Compiler, der sich der Ausrichtungen, Speicherorte, Aliasing-Überlegungen und anderer Aspekte bewusst ist und Optimierungen wie die Festigkeitsreduzierung in der Weise vornimmt, die für die Maschinenarchitektur am besten geeignet ist.

6
user4015204

Die Vektorisierung kann hier verwendet werden, wie dies häufig bei Implementierungen von memchr der Fall ist. Sie verwenden den folgenden Algorithmus:

  1. Erstellen Sie eine Maske, deren Länge der Anzahl der vom Betriebssystem verwendeten Bits (64-Bit, 32-Bit usw.) entspricht. Auf einem 64-Bit-System würden Sie die 32-Bit-Abfrage zweimal wiederholen.

  2. Verarbeiten Sie die Liste als eine Liste mit mehreren Datenelementen gleichzeitig, indem Sie die Liste einfach in eine Liste mit einem größeren Datentyp umwandeln und Werte herausziehen. Für jeden Chunk XOR mit der Maske, dann XOR mit 0b0111 ... 1, dann addiere 1, dann & mit einer Maske von 0b1000 .. .0 Wiederholung. Wenn das Ergebnis 0 ist, gibt es definitiv keine Übereinstimmung. Andernfalls kann es (normalerweise mit sehr hoher Wahrscheinlichkeit) eine Übereinstimmung geben, also suchen Sie den Block normal.

Beispielimplementierung: 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
meisel

Wenn Sie die Domäne Ihrer Werte mit verfügbare Speicherkapazität in Ihrer Anwendung unterbringen können, besteht die schnellste Lösung darin, Ihr Array als Array von Bits darzustellen:

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

EDIT

Ich bin erstaunt über die Anzahl der Kritiker. Der Titel dieses Threads lautet "Wie finde ich schnell heraus, ob ein Wert in einem C-Array vorhanden ist?", für das ich zu meiner Antwort stehe, weil es genau diese Antwort gibt. Ich könnte argumentieren, dass dies die schnellste Hash-Funktion hat (seit Adresse === Wert). Ich habe die Kommentare gelesen und bin mir der offensichtlichen Vorbehalte bewusst. Zweifellos schränken diese Vorbehalte die Bandbreite der Probleme ein, die damit gelöst werden können, aber für die Probleme, die es löst, ist es sehr effizient.

Anstatt diese Antwort sofort abzulehnen, betrachten Sie sie als den optimalen Ausgangspunkt, für den Sie mithilfe von Hash-Funktionen ein besseres Gleichgewicht zwischen Geschwindigkeit und Leistung erreichen können.

3
Stephen Quan

Stellen Sie sicher, dass sich die Anweisungen ("der Pseudocode") und die Daten ("theArray") in separaten Speichern (RAM) befinden, damit die CM4-Harvard-Architektur ihr volles Potenzial entfalten kann. Aus dem Benutzerhandbuch:

enter image description here

Um die CPU-Leistung zu optimieren, verfügt der ARM Cortex-M4 über drei Busse für Instruction (Code) (I) -Zugriff, Data (D) -Zugriff und System (S) -Zugriff werden in getrennten Speichern gespeichert, dann können Code- und Datenzugriffe in einem Zyklus parallel ausgeführt werden.Wenn Code und Daten in demselben Speicher gespeichert werden, können Anweisungen zum Laden oder Speichern von Daten zwei Zyklen dauern.

1
francek

Es tut mir leid, wenn meine Antwort bereits beantwortet wurde - ich bin nur ein fauler Leser. Fühlen Sie sich frei, dann downvote))

1) Sie könnten den Zähler "i" überhaupt entfernen - vergleichen Sie einfach die Zeiger, dh

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.

all dies wird jedoch keine signifikante Verbesserung bringen, eine solche Optimierung könnte wahrscheinlich vom Compiler selbst erreicht werden.

2) Wie bereits in anderen Antworten erwähnt, basieren fast alle modernen CPUs auf RISC, z. B. ARM. Sogar moderne Intel X86-CPUs verwenden meines Wissens RISC-Kerne (Kompilieren von X86 on fly). Die Hauptoptimierung für RISC ist die Pipeline-Optimierung (und auch für Intel und andere CPUs), um Code-Sprünge zu minimieren. Ein Typ einer solchen Optimierung (wahrscheinlich ein Hauptoptimierungstyp) ist "Cycle Rollback". Es ist unglaublich dumm und effizient, selbst Intel Compiler können das AFAIK. Es sieht aus wie:

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:

Auf diese Weise wird die Pipeline im ungünstigsten Fall nicht unterbrochen (wenn compareVal im Array nicht vorhanden ist), und zwar so schnell wie möglich (Algorithmusoptimierungen wie Hash-Tabellen, sortierte Arrays usw. werden natürlich nicht berücksichtigt). Erwähnt in anderen Antworten, die je nach Arraygröße bessere Ergebnisse liefern können. Der Cycles Rollback-Ansatz kann übrigens auch dort angewendet werden. Ich schreibe hier darüber, dass ich es in anderen nicht gesehen habe.

Der zweite Teil dieser Optimierung besteht darin, dass das Array-Element von der direkten Adresse (berechnet in der Kompilierungsphase, stellen Sie sicher, dass Sie ein statisches Array verwenden) genommen wird und keine zusätzliche ADD-Operation benötigt wird, um den Zeiger von der Basisadresse des Arrays zu berechnen. Diese Optimierung hat möglicherweise keine nennenswerten Auswirkungen, da die AFAIK ARM= Architektur über spezielle Funktionen zur Beschleunigung der Adressierung von Arrays verfügt. Trotzdem ist es immer besser zu wissen, dass Sie das Beste direkt in C-Code getan haben ?

Cycle Rollback kann aufgrund der Verschwendung von ROM (ja, Sie haben es richtig platziert, um einen Teil des Arbeitsspeichers zu beschleunigen, wenn Ihr Board diese Funktion unterstützt) umständlich aussehen, aber tatsächlich ist es ein faires Entgelt für Geschwindigkeit. Dies ist nur ein allgemeiner Punkt der Berechnungsoptimierung - Sie opfern Platz aus Gründen der Geschwindigkeit und umgekehrt, je nach Ihren Anforderungen.

Wenn Sie der Meinung sind, dass ein Rollback für ein Array mit 1024 Elementen für Ihren Fall ein zu großes Opfer darstellt, können Sie einen partiellen Rollback in Betracht ziehen, indem Sie das Array beispielsweise in zwei Teile mit jeweils 512 Elementen oder 4x256 usw. teilen.

3) Moderne CPUs unterstützen oft SIMD-Operationen, zum Beispiel ARM NEON-Befehlssatz - es ermöglicht die parallele Ausführung derselben Operationen. Ehrlich gesagt erinnere ich mich nicht, ob es für Vergleichsoperationen geeignet ist, aber Ich denke, es kann sein, dass Sie das überprüfen sollten. Googeln zeigt, dass es auch einige Tricks geben kann, um die maximale Geschwindigkeit zu erreichen, siehe https://stackoverflow.com/a/5734019/1028256

Ich hoffe, es kann Ihnen einige neue Ideen geben.

0
Mixaz

Ich bin ein großer Fan von Hashing. Das Problem besteht natürlich darin, einen effizienten Algorithmus zu finden, der sowohl schnell als auch speicherintensiv ist (insbesondere auf einem eingebetteten Prozessor).

Wenn Sie die möglichen Werte im Voraus kennen, können Sie ein Programm erstellen, das eine Vielzahl von Algorithmen durchläuft, um den besten oder besser die besten Parameter für Ihre Daten zu finden.

Ich habe ein solches Programm erstellt, über das Sie in diesem Beitrag lesen können, und habe einige sehr schnelle Ergebnisse erzielt. 16000 Einträge entsprechen ungefähr 2 ^ 14 oder einem Durchschnitt von 14 Vergleichen, um den Wert mithilfe einer binären Suche zu ermitteln. Ich habe ausdrücklich sehr schnelle Suchvorgänge angestrebt - im Durchschnitt wurde der Wert in <= 1,5 Suchvorgängen ermittelt - was zu höheren RAM) Anforderungen führte. Es könnte viel Speicherplatz gespart werden. Im Vergleich dazu würde der Durchschnittsfall für eine binäre Suche nach 256 oder 1024 Einträgen zu einer durchschnittlichen Anzahl von Vergleichen von 8 bzw. 10 führen.

Meine durchschnittliche Suche erforderte ungefähr 60 Zyklen (auf einem Laptop mit Intel i5) mit einem generischen Algorithmus (unter Verwendung einer Division durch eine Variable) und 40-45 Zyklen mit einem spezialisierten Algorithmus (wahrscheinlich unter Verwendung einer Multiplikation). Dies sollte sich in Suchzeiten im Submikrosekundenbereich auf Ihrer MCU niederschlagen, abhängig von der Taktfrequenz, mit der sie ausgeführt wird.

Es kann im wirklichen Leben weiter optimiert werden, wenn das Eintragsarray verfolgt, wie oft auf einen Eintrag zugegriffen wurde. Wenn das Eingabearray vor der Berechnung der Indices von den meisten nach den wenigsten Zugriffen sortiert wird, werden die am häufigsten vorkommenden Werte mit einem einzigen Vergleich ermittelt.

0
Olof Forshell

Dies ist eher ein Nachtrag als eine Antwort.

Ich hatte in der Vergangenheit einen ähnlichen Fall, aber mein Array war über eine beträchtliche Anzahl von Suchen konstant.

In der Hälfte von ihnen war der gesuchte Wert NICHT im Array vorhanden. Dann wurde mir klar, dass ich vor jeder Suche einen "Filter" anwenden konnte.

Dieser "Filter" ist nur eine einfache Ganzzahl, die EINMAL berechnet und bei jeder Suche verwendet wird.

Es ist in Java, aber es ist ziemlich einfach:

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

Bevor ich also eine Binärsuche durchführe, überprüfe ich binärfilter:

// 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 ...

}

Sie können einen 'besseren' Hash-Algorithmus verwenden, dies kann jedoch sehr schnell sein, insbesondere bei großen Zahlen. Vielleicht können Sie dadurch noch mehr Zyklen sparen.

0
Christian