Ich habe eine eingebettete Anwendung mit einem zeitkritischen ISR, die ein Array der Größe 256 (vorzugsweise 1024, aber mindestens 256) durchlaufen und prüfen muss, ob ein Wert mit dem Inhalt des Arrays übereinstimmt. A bool
wird auf true gesetzt, wenn dies der Fall ist.
Der Mikrocontroller ist ein NXP LPC4357, ein ARM Cortex M4-Kern und der Compiler ist GCC. Ich habe bereits Optimierungsstufe 2 (3 ist langsamer) kombiniert und die Funktion im RAM anstelle von Flash platziert. Ich verwende auch Zeigerarithmetik und eine for
Schleife, die statt nach oben herunterzählt (prüfen, ob i!=0
schneller ist als prü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 verwende:
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, dies zu tun? Die Verwendung der Inline-Baugruppe ist zulässig. Andere "weniger elegante" Tricks sind ebenfalls erlaubt.
O(1)
oderO(logN)
im Vergleich zuO(N)
) und 2) ihn als Engpass profiliert haben.Antworten:
In Situationen, in denen die Leistung von größter Bedeutung ist, erzeugt der C-Compiler höchstwahrscheinlich nicht den schnellsten Code im Vergleich zu dem, was Sie mit handgestimmter Assemblersprache tun können. Ich neige 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 herumspielen und den Compiler dazu bringen, eine gute Ausgabe zu generieren, aber Sie verschwenden möglicherweise 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 jedoch 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. Es ist unwahrscheinlich, dass es klug genug ist, um die Schleife abzuwickeln. Hier ist eine Möglichkeit, die die drei Ideen enthält, die ich in meinem Kommentar erwähnt habe: Schleifen-Abrollen, Cache-Prefetch und Verwenden der ldm-Anweisung (Multiple Load). Die Anzahl der Befehlszyklen beträgt ungefähr 3 Takte pro Array-Element, berücksichtigt jedoch keine Speicherverzögerungen.
Betriebstheorie: Das CPU-Design von ARM führt die meisten Befehle in einem Taktzyklus aus, die Befehle werden jedoch in einer Pipeline ausgeführt. C-Compiler versuchen, die Pipeline-Verzögerungen zu beseitigen, indem sie andere Anweisungen dazwischen verschachteln. Bei einer engen Schleife wie dem ursprünglichen C-Code fällt es dem Compiler schwer, die Verzögerungen zu 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 reduzieren. Wenn Sie mit großen Datenmengen arbeiten und Ihr Code nicht die meisten oder alle verfügbaren Register verwendet, erhalten Sie im Allgemeinen keine maximale Leistung.
Update: Es gibt viele Skeptiker in den Kommentaren, die meine Erfahrung für anekdotisch / wertlos halten und Beweise benötigen. Ich habe GCC 4.8 (vom Android NDK 9C) verwendet, um die folgende Ausgabe mit der Optimierung -O2 zu generieren (alle Optimierungen sind aktiviert, einschließlich des Abrollens der Schleife ). Ich habe den ursprünglichen C-Code zusammengestellt, der in der obigen Frage dargestellt ist. Folgendes hat GCC produziert:
Die Ausgabe von GCC rollt nicht nur die Schleife nicht ab, sondern verschwendet auch einen Takt bei einem Stillstand nach dem LDR. Es sind mindestens 8 Takte pro Array-Element erforderlich. Es ist gut, die Adresse zu verwenden, um zu wissen, wann die Schleife verlassen werden muss, aber all die magischen Dinge, zu denen Compiler in der Lage sind, sind in diesem Code nirgends zu finden. Ich habe den Code nicht auf der Zielplattform ausgeführt (ich besitze keine), aber jeder, der Erfahrung mit der Leistung von ARM-Code hat, kann feststellen, dass mein Code schneller ist.
Update 2: Ich habe Microsoft Visual Studio 2013 SP2 die Möglichkeit gegeben, den Code besser zu nutzen. Es war in der Lage, NEON-Anweisungen zu verwenden, um meine Array-Initialisierung zu vektorisieren, aber die vom OP geschriebene Suche nach linearen Werten verlief ähnlich wie die von GCC generierte (ich habe die Beschriftungen umbenannt, um sie besser lesbar zu machen):
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 bald hier veröffentlichen.
Update 3: Ich habe meinen Code und den kompilierten ARM-Code von Microsoft auf einem Tegra 3 und Tegra 4 (Surface RT, Surface RT 2) ausgeführt. Ich habe 1000000 Iterationen einer Schleife ausgeführt, die keine Übereinstimmung findet, sodass sich alles im Cache befindet und leicht zu messen ist.
In beiden Fällen läuft mein Code fast doppelt so schnell. Die meisten modernen ARM-CPUs werden wahrscheinlich ähnliche Ergebnisse liefern.
quelle
Es gibt einen Trick, um es zu optimieren (ich wurde dies einmal in einem Vorstellungsgespräch gefragt):
Dies ergibt einen Zweig pro Iteration anstelle von zwei Zweigen pro Iteration.
AKTUALISIEREN:
Wenn Sie das Array zuordnen dürfen,
SIZE+1
können Sie den Teil "Last Entry Swapping" entfernen:Sie können auch die zusätzliche eingebettete Arithmetik entfernen
theArray[i]
, indem Sie stattdessen Folgendes verwenden:Wenn der Compiler es noch nicht anwendet, wird diese Funktion dies mit Sicherheit tun. Auf der anderen Seite kann es für den Optimierer schwieriger sein, die Schleife abzuwickeln, sodass Sie dies im generierten Assemblycode überprüfen müssen ...
quelle
const
, was dies nicht threadsicher macht. Scheint ein hoher Preis zu sein.const
jemals in der Frage erwähnt?const
noch Fäden, aber ich denke, es ist fair, diese Einschränkung zu erwähnen.Sie bitten um Hilfe bei der Optimierung Ihres Algorithmus, wodurch Sie möglicherweise zum Assembler werden. Ihr Algorithmus (eine lineare Suche) ist jedoch nicht so clever. Sie sollten daher in Betracht ziehen, 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 einem Wert im Bereich 0 .. n zuordnet , bei dem für alle gültigen Werte, die Sie interessieren, keine Kollisionen auftreten . Das heißt, keine zwei "gültigen" Werte haben den gleichen Ausgabewert. Bei der Suche nach einer guten Hash-Funktion möchten Sie:
Beachten Sie für effiziente Hash-Funktionen, dass n häufig eine Potenz von 2 ist, was einer bitweisen Maske niedriger Bits (UND-Operation) entspricht. Beispiel-Hash-Funktionen:
((x << i) ^ (x >> j) ^ (x << k) ^ ...) % n
(so viel Kommissionierungi
,j
,k
, ... je nach Bedarf, mit links oder rechts verschiebt)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 des Index i einen anderen ungültigen Wert enthält, der nicht mit i hasht .
Dann in Ihrer Interrupt-Routine mit Eingabe x :
Dies ist viel schneller als eine lineare Suche mit 256 oder 1024 Werten.
Ich habe 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 Tabelle mit 256 Einträgen in nur 8 Schritten (
log2(256)
) oder eine Tabelle mit 1024 Einträgen in 10 Schritten zu durchsuchen . Dies ist wiederum viel schneller als eine lineare Suche mit 256 oder 1024 Werten.quelle
Halten Sie die Tabelle in sortierter Reihenfolge und verwenden Sie Bentleys ungerollte binäre Suche:
Der Punkt ist,
==
Fall bei jeder Iteration zu testen , da die Wahrscheinlichkeit für diesen Fall mit Ausnahme der letzten Iteration zu gering ist, um Zeit damit zu verbringen, ihn zu testen. **** Wenn Sie es nicht gewohnt sind, in Wahrscheinlichkeiten zu denken, hat jeder Entscheidungspunkt eine Entropie . Dies ist die durchschnittliche Information, die Sie durch Ausführen lernen. Für die
>=
Tests beträgt die Wahrscheinlichkeit für jeden Zweig etwa 0,5 und -log2 (0,5) 1. Wenn Sie also einen Zweig nehmen, lernen Sie 1 Bit, und wenn Sie den anderen Zweig nehmen, lernen Sie ein Bit und den Durchschnitt ist nur die Summe dessen, was Sie in jedem Zweig lernen, multipliziert mit der Wahrscheinlichkeit dieses Zweigs. So1*0.5 + 1*0.5 = 1
, so die Entropie der>=
Tests ist 1. Da Sie 10 Bits müssen zu lernen, dauert es 10 Niederlassungen. Deshalb ist es schnell!Was ist andererseits, wenn Ihr erster Test ist
if (key == a[i+512)
? 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, lernst du -log2 (1023/1024) = .00141 Bits, praktisch nichts! Die durchschnittliche Menge, die Sie aus diesem Test lernen, sind10/1024 + .00141*1023/1024 = .0098 + .00141 = .0112
Bits. Etwa ein Hundertstel. Dieser Test trägt nicht sein Gewicht!quelle
Wenn die Konstanten in Ihrer Tabelle im Voraus bekannt sind, können Sie perfektes Hashing verwenden sicherstellen, dass nur ein Zugriff auf die Tabelle erfolgt. Perfektes 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 ist, die Sie sich leisten können, wobei weniger dichte Tabellen normalerweise zu einfacheren Hashing-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 dies um die Zeit konkurriert, die Sie möglicherweise besser für mehrere Sonden benötigen.
Perfektes Hashing ist ein "1-Probe-Max" -Schema. Man kann die Idee verallgemeinern, mit dem Gedanken, dass man die Einfachheit der Berechnung des Hash-Codes mit der Zeit tauschen sollte, die benötigt wird, um k Sonden herzustellen. Schließlich ist das Ziel "geringste Gesamtzeit zum Nachschlagen", nicht die wenigsten Sonden 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, dominiert wahrscheinlich die einzige Prüfung des Speichers von einem perfekten Hash die Ausführungszeit. Wenn der Prozessor nicht sehr schnell ist, können k> 1 Sonden praktisch sein.
quelle
table[PerfectHash(value)] == value
ergibt 1, wenn der Wert in der Menge enthalten ist, und 0, wenn dies nicht der Fall ist, und es gibt bekannte Möglichkeiten, die PerfectHash-Funktion zu erzeugen (siehe z . B. burtleburtle.net/bob/hash/perfect.html ). Der Versuch, eine Hash-Funktion zu finden, die alle Werte in der Menge direkt auf 1 und alle Werte in der Menge auf 0 abbildet, ist eine tollkühne Aufgabe.Verwenden Sie ein Hash-Set. Es gibt O (1) Suchzeit.
Der folgende Code setzt voraus, dass Sie den Wert
0
als "leeren" Wert reservieren können , dh nicht in tatsächlichen Daten vorkommen. Die Lösung kann für eine Situation erweitert werden, in der dies nicht der Fall ist.In dieser Beispielimplementierung ist die Suchzeit normalerweise sehr gering, kann jedoch im schlimmsten Fall bis zur Anzahl der gespeicherten Einträge betragen. Für eine Echtzeitanwendung können Sie auch eine Implementierung mit Binärbäumen in Betracht ziehen, die eine besser vorhersehbare Suchzeit hat.
quelle
In diesem Fall kann es sich lohnen, Bloom-Filter zu untersuchen . Sie können schnell feststellen, dass kein Wert vorhanden ist, was gut ist, da die meisten der 2 ^ 32 möglichen Werte nicht in diesem 1024-Element-Array enthalten sind. Es gibt jedoch einige Fehlalarme, für die eine zusätzliche Überprüfung erforderlich ist.
Da Ihre Tabelle anscheinend statisch ist, können Sie feststellen, welche Fehlalarme für Ihren Bloom-Filter vorhanden sind, und diese in einen perfekten Hash setzen.
quelle
Angenommen, Ihr Prozessor läuft mit 204 MHz, was das Maximum für den LPC4357 zu sein scheint, und wenn Ihr Timing-Ergebnis den Durchschnittsfall widerspiegelt (die Hälfte des durchquerten Arrays), erhalten wir:
Ihre Suchschleife verbringt also ungefähr 20 Zyklen pro Iteration. Das hört sich nicht schrecklich an, aber ich denke, um es schneller zu machen, müssen Sie sich die Baugruppe ansehen.
Ich würde empfehlen, den Index zu löschen und stattdessen einen Zeigervergleich zu verwenden und alle Zeiger zu erstellen
const
.Das ist zumindest einen Test wert.
quelle
const
erkennt GCC bereits, dass er sich nicht ändert. Dasconst
fügt auch nichts hinzu.const
nichts hinzufügt": Es sagt dem Leser sehr deutlich, dass sich der Wert nicht ändern wird. Das sind fantastische Informationen.Andere Leute haben vorgeschlagen, Ihre Tabelle neu zu organisieren, am Ende einen Sentinel-Wert hinzuzufügen oder ihn zu sortieren, um eine binäre Suche bereitzustellen.
Sie geben an: "Ich verwende auch Zeigerarithmetik und eine for-Schleife, die statt nach oben herunterzählt (prüfen, ob dies
i != 0
schneller ist als prüfen, obi < 256
)."Mein erster Rat ist: Befreien Sie sich von der Zeigerarithmetik und dem Downcounting. Zeug wie
neigt dazu, für den Compiler idiomatisch zu sein. Die Schleife ist idiomatisch und die Indizierung eines Arrays über eine Schleifenvariable ist idiomatisch. Durch das Jonglieren mit Zeigerarithmetik und Zeigern werden die Redewendungen für den Compiler verschleiert und Code generiert, der sich auf das bezieht, was Sie geschrieben haben, und nicht auf das, was der Compiler-Autor als besten Kurs für die allgemeine Aufgabe festgelegt hat .
Zum Beispiel könnte der obige Code in eine Schleife kompiliert werden, die von
-256
oder-255
nach Null läuft und abschaltet&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 klug sein möchten, arbeiten Sie an den Datenstrukturen und Algorithmen, aber optimieren Sie deren Ausdruck nicht. Es wird nur zurückkommen, um Sie zu beißen, wenn nicht auf dem aktuellen Compiler / der aktuellen Architektur, dann auf dem nächsten.
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 Dinge voll bewusst ist und Optimierungen wie die Reduzierung der Festigkeit auf die für die Maschinenarchitektur am besten geeignete Weise vornimmt.
quelle
Die Vektorisierung kann hier verwendet werden, wie dies häufig bei Implementierungen von memchr der Fall ist. Sie verwenden den folgenden Algorithmus:
Erstellen Sie eine Maske, in der sich Ihre Abfrage wiederholt und deren Länge der Bitanzahl Ihres Betriebssystems entspricht (64-Bit, 32-Bit usw.). Auf einem 64-Bit-System würden Sie die 32-Bit-Abfrage zweimal wiederholen.
Verarbeiten Sie die Liste als Liste mehrerer Daten gleichzeitig, indem Sie die Liste einfach in eine Liste eines größeren Datentyps umwandeln und Werte herausziehen. Für jeden Block XOR mit der Maske, dann XOR mit 0b0111 ... 1, dann 1 hinzufügen, dann & mit einer Maske von 0b1000 ... 0 wiederholen. Wenn das Ergebnis 0 ist, gibt es definitiv keine Übereinstimmung. Andernfalls kann es (normalerweise mit sehr hoher Wahrscheinlichkeit) zu einer Übereinstimmung kommen. Durchsuchen Sie den Block also 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
quelle
Wenn Sie die Domäne Ihrer Werte mit der für Ihre Anwendung verfügbaren Speichermenge aufnehmen können, besteht die schnellste Lösung darin, Ihr Array als Array von Bits darzustellen:
BEARBEITEN
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?" wofür ich zu meiner Antwort stehen werde, weil sie genau das beantwortet. Ich könnte argumentieren, dass dies die schnellste und effizienteste Hash-Funktion hat (da Adresse === Wert). Ich habe die Kommentare gelesen und bin mir der offensichtlichen Vorbehalte bewusst. Zweifellos begrenzen diese Vorbehalte den Bereich der Probleme, mit denen dies gelöst werden kann, aber für die Probleme, die es löst, löst es sehr effizient.
Betrachten Sie diese Antwort nicht als vollständigen Ausgangspunkt, sondern als optimalen Ausgangspunkt, für den Sie sich mithilfe von Hash-Funktionen weiterentwickeln können, um ein besseres Gleichgewicht zwischen Geschwindigkeit und Leistung zu erreichen.
quelle
Stellen Sie sicher, dass sich die Anweisungen ("der Pseudocode") und die Daten ("theArray") in separaten (RAM) Speichern befinden, damit die CM4 Harvard-Architektur optimal genutzt wird. Aus dem Benutzerhandbuch:
quelle
Es tut mir leid, wenn meine Antwort bereits beantwortet wurde - ich bin nur ein fauler Leser. Fühlen Sie sich frei, dann abzustimmen))
1) Sie könnten den Zähler 'i' überhaupt entfernen - vergleichen Sie einfach die Zeiger, dh
All dies führt jedoch zu keiner signifikanten Verbesserung. Eine solche Optimierung könnte wahrscheinlich vom Compiler selbst erreicht werden.
2) Wie bereits in anderen Antworten erwähnt, sind fast alle modernen CPUs RISC-basiert, beispielsweise ARM. Selbst moderne Intel X86-CPUs verwenden meines Wissens RISC-Kerne im Inneren (Kompilieren von X86 im laufenden Betrieb). Die Hauptoptimierung für RISC ist die Pipeline-Optimierung (und auch für Intel und andere CPUs), um Codesprünge zu minimieren. Eine Art einer solchen Optimierung (wahrscheinlich eine Hauptoptimierung) ist die "Zyklus-Rollback" -Optimierung. Es ist unglaublich dumm und effizient, selbst Intel-Compiler können das AFAIK. Es sieht aus wie:
Auf diese Weise besteht die Optimierung darin, dass die Pipeline im schlimmsten Fall nicht unterbrochen wird (wenn compareVal im Array fehlt), also so schnell wie möglich (natürlich ohne Algorithmusoptimierungen wie Hash-Tabellen, sortierte Arrays usw.). in anderen Antworten erwähnt, die je nach Arraygröße zu besseren Ergebnissen führen können. Der Rollback-Ansatz für Zyklen kann übrigens auch dort angewendet werden. Ich schreibe hier darüber, was ich in anderen nicht gesehen habe.)
Der zweite Teil dieser Optimierung besteht darin, dass dieses Array-Element von der direkten Adresse übernommen wird (berechnet beim Kompilieren, stellen Sie sicher, dass Sie ein statisches Array verwenden) und keine zusätzliche ADD-Operation benötigt, um den Zeiger aus der Basisadresse des Arrays zu berechnen. Diese Optimierung hat möglicherweise keine signifikanten Auswirkungen, da die AFAIK ARM-Architektur über spezielle Funktionen verfügt, um die Adressierung von Arrays zu beschleunigen. Aber trotzdem ist es immer besser zu wissen, dass Sie direkt im C-Code alles Gute getan haben, oder?
Cycle Rollback mag aufgrund der Verschwendung von ROM unangenehm aussehen (ja, Sie haben es richtig in einen schnellen Teil des RAM gelegt, wenn Ihr Board diese Funktion unterstützt), aber tatsächlich ist es eine faire Bezahlung für Geschwindigkeit, basierend auf dem RISC-Konzept. Dies ist nur ein allgemeiner Punkt der Berechnungsoptimierung - Sie opfern Platz aus Gründen der Geschwindigkeit und umgekehrt, abhängig von 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 „teilweisen Rollback“ in Betracht ziehen, z. B. das Array in 2 Teile mit jeweils 512 Elementen oder 4x256 usw. aufteilen.
3) Moderne CPUs unterstützen häufig SIMD-Operationen, z. B. den ARM NEON-Befehlssatz. Sie ermöglichen 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.
quelle
Ich bin ein großer Fan von Hashing. Das Problem besteht natürlich darin, einen effizienten Algorithmus zu finden, der sowohl schnell ist als auch eine minimale Speichermenge benötigt (insbesondere auf einem eingebetteten Prozessor).
Wenn Sie die möglicherweise auftretenden Werte im Voraus kennen, können Sie ein Programm erstellen, das eine Vielzahl von Algorithmen durchläuft, um den besten - oder vielmehr 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 einige sehr schnelle Ergebnisse erzielt. 16000 Einträge bedeuten ungefähr 2 ^ 14 oder durchschnittlich 14 Vergleiche, um den Wert mithilfe einer binären Suche zu ermitteln. Ich habe explizit sehr schnelle Suchvorgänge angestrebt - im Durchschnitt den Wert in <= 1,5 Suchvorgängen zu finden -, was zu höheren RAM-Anforderungen führte. Ich glaube, dass mit einem konservativeren Durchschnittswert (sagen wir <= 3) viel Speicherplatz gespart werden könnte. Im Vergleich dazu würde der durchschnittliche Fall für eine binäre Suche in Ihren 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 einem Intel i5) mit einem generischen Algorithmus (unter Verwendung einer Division durch eine Variable) und 40-45 Zyklen mit einem Spezialalgorithmus (wahrscheinlich unter Verwendung einer Multiplikation). Dies sollte sich in Suchzeiten von weniger als einer Mikrosekunde auf Ihrer MCU niederschlagen, abhängig natürlich von der Taktfrequenz, mit der sie ausgeführt wird.
Es kann im realen Leben weiter optimiert werden, wenn das Eintragsarray verfolgt, wie oft auf einen Eintrag zugegriffen wurde. Wenn das Eintragsarray vor der Berechnung der Indeces von den meisten bis zu den am wenigsten aufgerufenen sortiert wird, werden die am häufigsten vorkommenden Werte mit einem einzigen Vergleich ermittelt.
quelle
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 Suchvorgängen 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:
Bevor ich eine binäre Suche durchführe, überprüfe ich den Binärfilter:
Sie können einen "besseren" Hash-Algorithmus verwenden, dies kann jedoch sehr schnell sein, insbesondere für große Zahlen. Möglicherweise können Sie dadurch noch mehr Zyklen sparen.
quelle