Was ist der schnellste Teilstring-Suchalgorithmus?


165

OK, also ich klinge nicht wie ein Idiot. Ich werde das Problem / die Anforderungen expliziter darlegen:

  • Nadel (Muster) und Heuhaufen (zu durchsuchender Text) sind nullterminierte Zeichenfolgen im C-Stil. Es werden keine Längeninformationen bereitgestellt. Bei Bedarf muss es berechnet werden.
  • Die Funktion sollte einen Zeiger auf die erste Übereinstimmung zurückgeben oder NULLwenn keine Übereinstimmung gefunden wird.
  • Fehlerfälle sind nicht zulässig. Dies bedeutet, dass jeder Algorithmus mit nicht konstanten (oder großen konstanten) Speicheranforderungen einen Fallback-Fall für einen Zuordnungsfehler haben muss (und die Leistung in der Fallback-Pflege dadurch zur Worst-Case-Leistung beiträgt).
  • Die Implementierung soll in C erfolgen, obwohl eine gute Beschreibung des Algorithmus (oder der Verknüpfung mit einem solchen) ohne Code ebenfalls in Ordnung ist.

... sowie was ich mit "am schnellsten" meine:

  • Deterministisch O(n)wo n= Heuhaufenlänge. (Es kann jedoch möglich sein, Ideen von Algorithmen zu verwenden, die normalerweise verwendet werden O(nm)(z. B. rollierender Hash), wenn sie mit einem robusteren Algorithmus kombiniert werden, um deterministische O(n)Ergebnisse zu erzielen .)
  • Niemals if (!needle[1])schlechter (messbar; ein paar Uhren usw. sind in Ordnung) schlechter als der naive Brute-Force-Algorithmus, insbesondere bei sehr kurzen Nadeln, die wahrscheinlich der häufigste Fall sind. (Der bedingungslose hohe Vorverarbeitungsaufwand ist schlecht, ebenso wie der Versuch, den linearen Koeffizienten für pathologische Nadeln auf Kosten wahrscheinlicher Nadeln zu verbessern.)
  • Bei einer beliebigen Nadel und einem beliebigen Heuhaufen ist die Leistung vergleichbar oder besser (nicht schlechter als 50% längere Suchzeit) als bei jedem anderen weit verbreiteten Algorithmus.
  • Abgesehen von diesen Bedingungen lasse ich die Definition von "schnellstem" unbefristet. Eine gute Antwort sollte erklären, warum Sie den von Ihnen vorgeschlagenen Ansatz als "am schnellsten" betrachten.

Meine aktuelle Implementierung läuft ungefähr 10% langsamer und 8-mal schneller (abhängig von der Eingabe) als die Implementierung von Two-Way von glibc.

Update: Mein aktueller optimaler Algorithmus lautet wie folgt:

  • Verwenden Sie für Nadeln der Länge 1 strchr.
  • Verwenden Sie für Nadeln der Länge 2-4 Maschinenwörter, um 2-4 Bytes gleichzeitig wie folgt zu vergleichen: Laden Sie die Nadel in einer 16- oder 32-Bit-Ganzzahl mit Bitverschiebungen vor und wechseln Sie bei jeder Iteration alte Bytes aus / neue Bytes aus dem Heuhaufen . Jedes Byte des Heuhaufens wird genau einmal gelesen und es wird eine Prüfung gegen 0 (Ende der Zeichenfolge) und ein 16- oder 32-Bit-Vergleich durchgeführt.
  • Verwenden Sie für Nadeln mit einer Länge> 4 den Zwei-Wege-Algorithmus mit einer schlechten Verschiebungstabelle (wie Boyer-Moore), die nur auf das letzte Byte des Fensters angewendet wird. Um den Aufwand für die Initialisierung einer 1-KB-Tabelle zu vermeiden, der für viele Nadeln mittlerer Länge einen Nettoverlust darstellen würde, behalte ich ein Bit-Array (32 Byte) bei, das markiert, welche Einträge in der Verschiebungstabelle initialisiert werden. Nicht gesetzte Bits entsprechen Bytewerten, die niemals in der Nadel erscheinen, für die eine Verschiebung um die gesamte Nadellänge möglich ist.

Die großen Fragen, die mir noch im Kopf bleiben, sind:

  • Gibt es eine Möglichkeit, den schlechten Schichttisch besser zu nutzen? Boyer-Moore nutzt es am besten, indem er rückwärts (von rechts nach links) scannt, für Two-Way ist jedoch ein Scan von links nach rechts erforderlich.
  • Die einzigen zwei brauchbaren Kandidatenalgorithmen, die ich für den allgemeinen Fall gefunden habe (keine Speichermangel- oder quadratischen Leistungsbedingungen), sind Zweiwege- und String-Matching für geordnete Alphabete . Aber gibt es leicht erkennbare Fälle, in denen unterschiedliche Algorithmen optimal wären? Sicherlich könnten viele der O(m)(wo mist die Nadellänge) im Weltraum-Algorithmen für m<100oder so verwendet werden. Es wäre auch möglich, Algorithmen zu verwenden, die im schlimmsten Fall quadratisch sind, wenn es einen einfachen Test für Nadeln gibt, die nachweislich nur eine lineare Zeit benötigen.

Bonuspunkte für:

  • Können Sie die Leistung verbessern, indem Sie davon ausgehen, dass Nadel und Heuhaufen beide gut geformte UTF-8 sind? (Bei Zeichen mit unterschiedlichen Bytelängen stellt die Formgebung einige Anforderungen an die Ausrichtung der Zeichenfolge zwischen Nadel und Heuhaufen und ermöglicht automatische Verschiebungen von 2 bis 4 Bytes, wenn ein nicht übereinstimmendes Kopfbyte auftritt Maximale Suffixberechnungen, gute Suffixverschiebungen usw. geben Ihnen bereits verschiedene Algorithmen?)

Hinweis: Mir sind die meisten Algorithmen bekannt, nur nicht, wie gut sie in der Praxis funktionieren. Hier ist eine gute Referenz, damit mir die Leute nicht immer Referenzen zu Algorithmen als Kommentare / Antworten geben: http://www-igm.univ-mlv.fr/~lecroq/string/index.html


Es gibt eine ganze Reihe von Algorithmen für die Zeichenfolgensuche, die unter Algorithmen für Zeichenfolgen aufgeführt sind . Möglicherweise möchten Sie beschreiben, welche Algorithmen Sie in dieser Liste berücksichtigt haben.
Greg Hewgill

61
Dieser Link am Ende ist Gold!
Carlos

4
Ich kann nicht glauben, dass Sie immer noch keine Antwort akzeptiert haben.
user541686

1
@Mehrdad: Ich wollte gerade sagen, dass es keine Antworten gibt, die die gestellte Frage wirklich beantworten, aber Ihre scheint es zu tun. Zu dem Zeitpunkt, als Sie geantwortet haben, war ich weitergegangen und habe eine weitere Verbesserung strstrals etwas für später hinterlassen , sodass ich nicht wirklich dazu gekommen bin, das von Ihnen verlinkte Papier richtig zu lesen, aber es klingt sehr vielversprechend. Vielen Dank und Entschuldigung, dass Sie sich nicht bei Ihnen gemeldet haben.
R .. GitHub STOP HELPING ICE

Antworten:


37

Bauen Sie eine Testbibliothek mit wahrscheinlichen Nadeln und Heuhaufen auf. Profilieren Sie die Tests anhand mehrerer Suchalgorithmen, einschließlich Brute Force. Wählen Sie diejenige aus, die mit Ihren Daten am besten funktioniert.

Boyer-Moore verwendet eine schlechte Zeichentabelle mit einer guten Suffix-Tabelle.

Boyer-Moore-Horspool verwendet eine Tabelle mit schlechten Charakteren.

Knuth-Morris-Pratt verwendet eine teilweise Übereinstimmungstabelle.

Rabin-Karp verwendet laufende Hashes.

Sie alle tauschen Overhead gegen reduzierte Vergleiche in unterschiedlichem Maße, sodass die tatsächliche Leistung von der durchschnittlichen Länge von Nadel und Heuhaufen abhängt. Je höher der anfängliche Overhead, desto besser bei längeren Eingaben. Mit sehr kurzen Nadeln kann Brute Force gewinnen.

Bearbeiten:

Ein anderer Algorithmus ist möglicherweise am besten geeignet, um Basenpaare, englische Phrasen oder einzelne Wörter zu finden. Wenn es einen besten Algorithmus für alle Eingaben gegeben hätte, wäre er veröffentlicht worden.

Denken Sie an die folgende kleine Tabelle. Jedes Fragezeichen hat möglicherweise einen anderen besten Suchalgorithmus.

                 short needle     long needle
short haystack         ?               ?
long haystack          ?               ?

Dies sollte eigentlich ein Diagramm sein, mit einem Bereich von kürzeren bis längeren Eingaben auf jeder Achse. Wenn Sie jeden Algorithmus in einem solchen Diagramm darstellen würden, hätte jeder eine andere Signatur. Einige Algorithmen leiden unter vielen Wiederholungen im Muster, was sich auf Anwendungen wie die Suche nach Genen auswirken kann. Einige andere Faktoren, die sich auf die Gesamtleistung auswirken, sind die mehrfache Suche nach demselben Muster und die gleichzeitige Suche nach verschiedenen Mustern.

Wenn ich ein Beispielset benötige, würde ich wahrscheinlich eine Website wie Google oder Wikipedia kratzen und dann das HTML von allen Ergebnisseiten entfernen. Geben Sie für eine Suchwebsite ein Wort ein und verwenden Sie einen der vorgeschlagenen Suchbegriffe. Wählen Sie gegebenenfalls einige verschiedene Sprachen aus. Bei Verwendung von Webseiten sind alle Texte kurz bis mittelgroß. Führen Sie daher genügend Seiten zusammen, um längere Texte zu erhalten. Sie können auch gemeinfreie Bücher, juristische Aufzeichnungen und andere große Textkörper finden. Oder generieren Sie einfach zufälligen Inhalt, indem Sie Wörter aus einem Wörterbuch auswählen. Bei der Profilerstellung geht es jedoch darum, anhand der Art des Inhalts zu testen, nach dem Sie suchen. Verwenden Sie daher nach Möglichkeit Beispiele aus der Praxis.

Ich ging kurz und lang vage. Für die Nadel denke ich an kurze als unter 8 Zeichen, mittlere als unter 64 Zeichen und lange als unter 1k. Für den Heuhaufen denke ich an kurz als unter 2 ^ 10, mittel wie unter 2 ^ 20 und lang bis zu 2 ^ 30 Zeichen.


1
Haben Sie gute Vorschläge für eine Testbibliothek? Die vorherige Frage, die ich zu SO gestellt habe, war damit verbunden und ich habe keine wirklichen Antworten bekommen. (außer meinem eigenen ...) Es sollte umfangreich sein. Selbst wenn meine Idee einer Anwendung für strstr darin besteht, englischen Text zu durchsuchen, sucht jemand anderes möglicherweise nach Genen in Basenpaarsequenzen ...
R .. GitHub STOP HELPING ICE

3
Es ist etwas komplizierter als kurz / lang. Für die Nadel sind die großen Fragen, die für die Leistung der meisten Algorithmen relevant sind: Länge? Gibt es eine Periodizität? Enthält die Nadel alle eindeutigen Zeichen (keine Wiederholungen)? Oder alle der gleiche Charakter? Gibt es eine große Anzahl von Zeichen im Heuhaufen, die niemals in der Nadel erscheinen? Gibt es eine Chance, mit Nadeln eines Angreifers umgehen zu müssen, der die Worst-Case-Leistung nutzen möchte, um Ihr System zu lähmen? Usw.
R .. GitHub STOP HELPING ICE

31

Ich glaube, dass es sich um den 2011 veröffentlichten Algorithmus "Simple Real-Time Constant-Space String Matching" von Dany Breslauer, Roberto Grossi und Filippo Mignosi handelt.

Aktualisieren:

2014 veröffentlichten die Autoren diese Verbesserung: Auf dem Weg zu einem optimalen Matching gepackter Strings .


1
Wow, danke. Ich lese die Zeitung. Wenn es sich als besser herausstellt als das, was ich habe, werde ich Ihre Antwort definitiv akzeptieren.
R .. GitHub STOP HELPING ICE

1
@R ..: Sicher! :) Apropos, wenn Sie es schaffen, den Algorithmus zu implementieren, sollten Sie ihn auf StackOverflow veröffentlichen, damit jeder davon profitieren kann! Ich habe nirgendwo Implementierungen davon gefunden und bin nicht gut darin, Algorithmen zu implementieren, die ich in Forschungsarbeiten finde, haha.
user541686

2
Es ist eine Variante des "Zwei-Wege" -Algorithmus, den ich bereits verwende, daher ist es möglicherweise einfach, meinen Code an diese anzupassen. Ich muss das Papier jedoch genauer lesen, um sicherzugehen, und ich muss bewerten, ob die vorgenommenen Änderungen mit meiner Verwendung einer "Tabelle mit schlechten Zeichen" kompatibel sind, was den allgemeinen Fall erheblich beschleunigt.
R .. GitHub STOP HELPING ICE

11
Und Sie haben die Antwort von @ Mehrdad immer noch nicht akzeptiert! :-)
Lebensbalance

3
@ DavidWallace: Was? Es hat die Papiertitel und die Autoren. Auch wenn der Link nicht mehr funktioniert, finden Sie die Papiere. Was erwartest du von mir, Pseudocode für den Algorithmus zu schreiben? Warum glaubst du, ich verstehe den Algorithmus?
user541686

23

Der Link http://www-igm.univ-mlv.fr/~lecroq/string/index.html , auf den Sie verweisen, ist eine hervorragende Quelle und Zusammenfassung einiger der bekanntesten und erforschten Algorithmen für den String-Abgleich.

Lösungen für die meisten Suchprobleme beinhalten Kompromisse hinsichtlich des Vorverarbeitungsaufwands, des Zeit- und Platzbedarfs. Kein einzelner Algorithmus ist in allen Fällen optimal oder praktisch.

Wenn Sie einen bestimmten Algorithmus für die Zeichenfolgensuche entwerfen möchten, ignorieren Sie den Rest meiner Aussagen. Wenn Sie eine allgemeine Routine für die Zeichenfolgensuche entwickeln möchten, versuchen Sie Folgendes:

Nehmen Sie sich etwas Zeit, um die spezifischen Stärken und Schwächen der Algorithmen zu überprüfen, auf die Sie bereits verwiesen haben. Führen Sie die Überprüfung mit dem Ziel durch, eine Reihe von Algorithmen zu finden, die den Bereich und den Umfang der Zeichenfolgensuche abdecken, an denen Sie interessiert sind. Erstellen Sie anschließend einen Front-End-Suchselektor basierend auf einer Klassifizierungsfunktion, um den besten Algorithmus für die angegebenen Eingaben zu ermitteln. Auf diese Weise können Sie den effizientesten Algorithmus verwenden, um die Aufgabe zu erledigen. Dies ist besonders effektiv, wenn ein Algorithmus für bestimmte Suchvorgänge sehr gut ist, sich jedoch nur schlecht verschlechtert. Zum Beispiel ist Brute Force wahrscheinlich die beste für Nadeln der Länge 1, nimmt jedoch mit zunehmender Nadellänge schnell ab, woraufhin das Sustik-Moore-Algoritimkann effizienter werden (gegenüber kleinen Alphabeten), dann sind bei längeren Nadeln und größeren Alphabeten die KMP- oder Boyer-Moore-Algorithmen möglicherweise besser. Dies sind nur Beispiele zur Veranschaulichung einer möglichen Strategie.

Der Ansatz mit mehreren Algorithmen ist keine neue Idee. Ich glaube, es wurde von einigen kommerziellen Sortier- / Suchpaketen verwendet (z. B. implementiert SYNCSORT, das üblicherweise auf Großrechnern verwendet wird, mehrere Sortieralgorithmen und verwendet Heuristiken, um den "besten" für die gegebenen Eingaben auszuwählen).

Jeder Suchalgorithmus ist in verschiedenen Varianten erhältlich, die die Leistung erheblich verbessern können, wie beispielsweise in diesem Artikel dargestellt.

Benchmarking Ihres Dienstes, um die Bereiche zu kategorisieren, in denen zusätzliche Suchstrategien erforderlich sind, oder um Ihre Auswahlfunktion effektiver zu optimieren. Dieser Ansatz ist nicht schnell oder einfach, kann aber bei guter Ausführung zu sehr guten Ergebnissen führen.


1
Vielen Dank für die Antwort, insbesondere für den Link zu Sustik-Moore, den ich vorher noch nicht gesehen hatte. Der Ansatz mit mehreren Algorithmen ist sicherlich weit verbreitet. Glibc führt grundsätzlich strchr, Two-Way ohne schlechte Zeichenverschiebungstabelle oder Two-Way mit schlechter Zeichenverschiebungstabelle aus, je nachdem, ob Needle_len 1, <32 oder> 32 ist. Mein aktueller Ansatz ist der gleiche, außer dass ich immer die Verschiebungstabelle verwende. Ich habe das dafür erforderliche 1-KB-Memset durch ein 32-Byte-Memset in einem Bitset ersetzt, mit dem markiert wird, welche Elemente der Tabelle initialisiert wurden, und ich bekomme den Vorteil (aber nicht den Overhead) auch für winzige Nadeln.
R .. GitHub STOP HELPING ICE

1
Nachdem ich darüber nachgedacht habe, bin ich wirklich neugierig, was die beabsichtigte Anwendung für Sustik-Moore ist. Mit kleinen Alphabeten können Sie keine signifikanten Verschiebungen vornehmen (alle Zeichen des Alphabets erscheinen fast sicher am Ende der Nadel), und endliche Automatenansätze sind sehr effizient (kleine Zustandsübergangstabelle). Ich kann mir also kein Szenario vorstellen, in dem Sustik-Moore optimal sein könnte ...
R .. GitHub STOP HELPING ICE

Tolle Antwort - wenn ich diese spezielle Antwort markieren könnte, würde ich es tun.
Jason S

1
@R .. Die Theorie hinter dem Sustik-Moore-Algorithmus ist, dass er Ihnen größere durchschnittliche Verschiebungsbeträge geben sollte, wenn die Nadel relativ groß und das Alphabet relativ klein ist (z. B. Suche nach DNA-Sequenzen). Größer bedeutet in diesem Fall nur größer als der grundlegende Boyer-Moore-Algorithmus bei gleichen Eingaben. Wie viel effizienter dies im Vergleich zu einem endlichen Automatenansatz oder zu einer anderen Boyer-Moore-Variante (von der es viele gibt) ist, ist schwer zu sagen. Aus diesem Grund habe ich betont, dass ich einige Zeit damit verbracht habe, die spezifischen Stärken / Schwächen Ihrer Kandidatenalgorithmen zu untersuchen.
NealB

1
Hm, ich glaube, ich war festgefahren, an Verschiebungen zu denken, nur im Sinne von schlechten Charakterverschiebungen von Boyer-Moore. Mit einer Verbesserung der BM-Suffixverschiebungen könnte Sustik-Moore möglicherweise die DFA-Ansätze für die DNA-Suche übertreffen. Ordentliches Zeug.
R .. GitHub STOP HELPING ICE

21

Ich war überrascht zu sehen, dass unser technischer Bericht in dieser Diskussion zitiert wurde. Ich bin einer der Autoren des Algorithmus, der oben Sustik-Moore genannt wurde. (Wir haben diesen Begriff in unserer Arbeit nicht verwendet.)

Ich wollte hier betonen, dass für mich das interessanteste Merkmal des Algorithmus ist, dass es ziemlich einfach ist zu beweisen, dass jeder Buchstabe höchstens einmal untersucht wird. Für frühere Boyer-Moore-Versionen haben sie bewiesen, dass jeder Brief höchstens dreimal und später höchstens zweimal geprüft wird, und diese Beweise waren stärker involviert (siehe Zitate in Papierform). Daher sehe ich auch einen didaktischen Wert darin, diese Variante zu präsentieren / zu studieren.

In der Arbeit beschreiben wir auch weitere Variationen, die auf Effizienz ausgerichtet sind und gleichzeitig die theoretischen Garantien lockern. Es ist eine kurze Arbeit und das Material sollte meiner Meinung nach für einen durchschnittlichen Abiturienten verständlich sein.

Unser Hauptziel war es, andere auf diese Version aufmerksam zu machen, die sie weiter verbessern können. Die Suche nach Zeichenfolgen hat so viele Variationen, und wir allein können unmöglich an alle denken, bei denen diese Idee Vorteile bringen könnte. (Fester Text und sich änderndes Muster, fester Muster, anderer Text, Vorverarbeitung möglich / nicht möglich, parallele Ausführung, Finden übereinstimmender Teilmengen in großen Texten, Zulassen von Fehlern, Beinahe-Übereinstimmungen usw. usw.)


1
Kennen Sie zufällig eine verfügbare C- oder C ++ - Implementierung? Ich denke darüber nach, dies für eine DNA-Motivsuche zu verwenden (genaue Motivübereinstimmungen). Wenn nicht, werde ich vielleicht versuchen, eine Implementierung selbst zu entwickeln und den Algorithmus zu
verbessern

4
Da keine verfügbare Implementierung bekannt ist, ist es unwahrscheinlich, dass der Sustik-Moore / 2BLOCK-Algorithmus in der Praxis verwendet wird, und er wird weiterhin nicht in den Ergebnissen von Zusammenfassungspapieren wie "Das Problem der
genauen Zeichenfolgenübereinstimmung

18

Der schnellste Suchalgorithmus für Teilzeichenfolgen hängt vom Kontext ab:

  1. die Alphabetgröße (zB DNA vs Englisch)
  2. die Nadellänge

Das 2010 erschienene Papier "The Exact String Matching Problem: Eine umfassende experimentelle Bewertung" enthält Tabellen mit Laufzeiten für 51 Algorithmen (mit unterschiedlichen Alphabetgrößen und Nadellängen), sodass Sie den besten Algorithmus für Ihren Kontext auswählen können.

Alle diese Algorithmen verfügen hier über C-Implementierungen sowie eine Testsuite:

http://www.dmi.unict.it/~faro/smart/algorithms.php


4

Eine wirklich gute Frage. Fügen Sie einfach ein paar winzige Teile hinzu ...

  1. Jemand sprach über DNA-Sequenz-Matching. Für die DNA-Sequenz erstellen wir normalerweise eine Datenstruktur (z. B. Suffix-Array, Suffix-Baum oder FM-Index) für den Heuhaufen und passen viele Nadeln daran an. Dies ist eine andere Frage.

  2. Es wäre wirklich großartig, wenn jemand verschiedene Algorithmen vergleichen möchte. Es gibt sehr gute Benchmarks für die Komprimierung und die Konstruktion von Suffix-Arrays, aber ich habe keinen Benchmark für die Zeichenfolgenübereinstimmung gesehen. Potenzielle Heuhaufenkandidaten könnten aus dem SACA-Benchmark stammen .

  3. Vor ein paar Tagen habe ich die Boyer-Moore-Implementierung von der von Ihnen empfohlenen Seite aus getestet (BEARBEITEN: Ich benötige einen Funktionsaufruf wie memmem (), aber es ist keine Standardfunktion, daher habe ich beschlossen, sie zu implementieren). Mein Benchmarking-Programm verwendet zufälligen Heuhaufen. Es scheint, dass die Boyer-Moore-Implementierung auf dieser Seite zeitweise schneller ist als glibcs ​​memmem () und Macs strnstr (). Falls Sie interessiert sind, ist die Umsetzung hier und der Benchmarking - Code ist hier . Dies ist definitiv kein realistischer Maßstab, aber es ist ein Anfang.


Wenn Sie einige gute Nadeln zusammen mit den Heuhaufen-Kandidaten aus dem SACA-Benchmark testen müssen, posten Sie sie als Antwort auf meine andere Frage, und bevor ich eine bessere Antwort bekomme , werde ich sie als akzeptiert markieren.
R .. GitHub STOP HELPING ICE

3
In Bezug auf Ihr Memmem und Boyer-Moore ist es sehr wahrscheinlich, dass Boyer-Moore (oder besser gesagt eine der Verbesserungen von Boyer-Moore) bei zufälligen Daten die beste Leistung erbringt. Zufällige Daten weisen eine extrem geringe Wahrscheinlichkeit für Periodizität und lange Teilübereinstimmungen auf, die zu einem quadratischen Worst-Case führen. Ich suche nach einer Möglichkeit, Boyer-Moore und Two-Way zu kombinieren oder effizient zu erkennen, wann Boyer-Moore "sicher zu bedienen" ist, aber bisher hatte ich keinen Erfolg. Übrigens würde ich glibcs ​​Memmem nicht als Vergleich verwenden. Meine Implementierung des im Grunde gleichen Algorithmus wie bei glibc ist um ein Vielfaches schneller.
R .. GitHub STOP HELPING ICE

Wie gesagt, es ist nicht meine Implementierung. Dank an Christian Charras und Thierry Lecroq. Ich kann mir vorstellen, warum zufällige Eingaben für das Benchmarking schlecht sind, und ich bin sicher, dass glibc aus Gründen Algorithmen auswählt. Ich denke auch, dass memmem () nicht effizient implementiert ist. Ich werde versuchen. Vielen Dank.
user172818

4

Ich weiß, dass es eine alte Frage ist, aber die meisten schlechten Schichttabellen bestehen aus einzelnen Zeichen. Wenn es für Ihren Datensatz sinnvoll ist (z. B. wenn es sich um geschriebene Wörter handelt) und wenn Sie über genügend Speicherplatz verfügen, können Sie eine dramatische Beschleunigung erzielen, indem Sie eine schlechte Verschiebungstabelle aus n-Gramm anstelle einzelner Zeichen verwenden.


3

Verwenden Sie stdlib strstr:

char *foundit = strstr(haystack, needle);

Es war sehr schnell, ich brauchte nur 5 Sekunden, um zu tippen.


26
Und wenn Sie meine Frage lesen, werden Sie sehen, dass ich es ziemlich leicht hatte, sie zu übertreffen. Ich mag deinen Sarkasmus genug, aber ich werde die -1 überspringen.
R .. GitHub STOP HELPING ICE

3

Hier ist die Suchimplementierung von Python , die im gesamten Kern verwendet wird. Die Kommentare zeigen an, dass eine komprimierte Boyer-Moore-Delta-1-Tabelle verwendet wird .

Ich habe selbst ziemlich ausführlich mit der Suche nach Zeichenfolgen experimentiert, aber es war für mehrere Suchzeichenfolgen. Assembly-Implementierungen von Horspool und Bitap können sich häufig gegen Algorithmen wie Aho-Corasick für niedrige Musterzahlen behaupten .


3

Ein schnellerer strchrAlgorithmus "Suche nach einem einzelnen übereinstimmenden Zeichen" (ala ).

Wichtige Notizen:

  • Diese Funktionen verwenden einen "Anzahl / Anzahl von (führenden | nachfolgenden) Nullen" gcc-Compiler __builtin_ctz. Diese Funktionen sind wahrscheinlich nur auf Computern schnell, die über Anweisungen verfügen, die diese Operation ausführen (z. B. x86, ppc, arm).

  • Diese Funktionen setzen voraus, dass die Zielarchitektur nicht ausgerichtete 32- und 64-Bit-Ladevorgänge ausführen kann. Wenn Ihre Zielarchitektur dies nicht unterstützt, müssen Sie eine Startlogik hinzufügen, um die Lesevorgänge ordnungsgemäß auszurichten.

  • Diese Funktionen sind prozessorneutral. Wenn die Ziel-CPU über Vektoranweisungen verfügt, können Sie dies möglicherweise (viel) besser machen. Die folgende strlenFunktion verwendet beispielsweise SSE3 und kann trivial so geändert werden, dass die gescannten Bytes XOR-verknüpft werden, um nach einem anderen Byte als zu suchen 0. Benchmarks auf einem 2,66-GHz-Core-2-Laptop unter Mac OS X 10.6 (x86_64):

    • 843,433 MB / s für strchr
    • 2656.742 MB / s für findFirstByte64
    • 13094,479 MB / s für strlen

... eine 32-Bit-Version:

#ifdef __BIG_ENDIAN__
#define findFirstZeroByte32(x) ({ uint32_t _x = (x); _x = ~(((_x & 0x7F7F7F7Fu) + 0x7F7F7F7Fu) | _x | 0x7F7F7F7Fu); (_x == 0u)   ? 0 : (__builtin_clz(_x) >> 3) + 1; })
#else
#define findFirstZeroByte32(x) ({ uint32_t _x = (x); _x = ~(((_x & 0x7F7F7F7Fu) + 0x7F7F7F7Fu) | _x | 0x7F7F7F7Fu);                    (__builtin_ctz(_x) + 1) >> 3; })
#endif

unsigned char *findFirstByte32(unsigned char *ptr, unsigned char byte) {
  uint32_t *ptr32 = (uint32_t *)ptr, firstByte32 = 0u, byteMask32 = (byte) | (byte << 8);
  byteMask32 |= byteMask32 << 16;
  while((firstByte32 = findFirstZeroByte32((*ptr32) ^ byteMask32)) == 0) { ptr32++; }
  return(ptr + ((((unsigned char *)ptr32) - ptr) + firstByte32 - 1));
}

... und eine 64-Bit-Version:

#ifdef __BIG_ENDIAN__
#define findFirstZeroByte64(x) ({ uint64_t _x = (x); _x = ~(((_x & 0x7F7F7F7F7f7f7f7full) + 0x7F7F7F7F7f7f7f7full) | _x | 0x7F7F7F7F7f7f7f7full); (_x == 0ull) ? 0 : (__builtin_clzll(_x) >> 3) + 1; })
#else
#define findFirstZeroByte64(x) ({ uint64_t _x = (x); _x = ~(((_x & 0x7F7F7F7F7f7f7f7full) + 0x7F7F7F7F7f7f7f7full) | _x | 0x7F7F7F7F7f7f7f7full);                    (__builtin_ctzll(_x) + 1) >> 3; })
#endif

unsigned char *findFirstByte64(unsigned char *ptr, unsigned char byte) {
  uint64_t *ptr64 = (uint64_t *)ptr, firstByte64 = 0u, byteMask64 = (byte) | (byte << 8);
  byteMask64 |= byteMask64 << 16;
  byteMask64 |= byteMask64 << 32;
  while((firstByte64 = findFirstZeroByte64((*ptr64) ^ byteMask64)) == 0) { ptr64++; }
  return(ptr + ((((unsigned char *)ptr64) - ptr) + firstByte64 - 1));
}

Edit 2011/06/04 Das OP weist in den Kommentaren darauf hin, dass diese Lösung einen "unüberwindbaren Fehler" aufweist:

Es kann über das gesuchte Byte oder den Null-Terminator hinaus lesen, der ohne Leseberechtigung auf eine nicht zugeordnete Seite oder Seite zugreifen kann. Sie können große Lesevorgänge in Zeichenfolgenfunktionen nur verwenden, wenn sie ausgerichtet sind.

Dies ist technisch richtig, gilt jedoch für praktisch jeden Algorithmus, der mit Blöcken arbeitet, die größer als ein einzelnes Byte sind, einschließlich der vom OP in den Kommentaren vorgeschlagenen Methode :

Eine typische strchrImplementierung ist nicht naiv, aber wesentlich effizienter als das, was Sie angegeben haben. Am Ende finden Sie den am häufigsten verwendeten Algorithmus: http://graphics.stanford.edu/~seander/bithacks.html#ZeroInWord

Es hat auch wirklich nichts mit der Ausrichtung an sich zu tun . Dies könnte zwar das Verhalten verursachen, das bei den meisten gängigen Architekturen diskutiert wird. Dies hat jedoch mehr mit Details der Implementierung der Mikroarchitektur zu tun. Wenn der nicht ausgerichtete Lesevorgang eine 4K-Grenze überschreitet (wiederum typisch), verursacht dieser Lesevorgang ein Programm Beenden des Fehlers, wenn die nächste 4K-Seitengrenze nicht zugeordnet ist.

Dies ist jedoch kein "Fehler" in dem in der Antwort angegebenen Algorithmus. Dieses Verhalten liegt daran, dass Funktionen ein Argument mögen strchrund strlennicht akzeptieren length, um die Größe der Suche zu begrenzen. Die Suche char bytes[1] = {0x55};, die für die Zwecke unserer Diskussion zufällig ganz am Ende einer 4K-VM-Seitengrenze platziert wird und deren nächste Seite nicht zugeordnet ist, mit strchr(bytes, 0xAA)(wo strchrsich jeweils eine Byte-Implementierung befindet) stürzt genau ab gleicher Weg. Das Gleiche gilt für strchrverwandte Cousins strlen.

Ohne ein lengthArgument gibt es keine Möglichkeit zu sagen, wann Sie vom Hochgeschwindigkeitsalgorithmus zu einem Byte-für-Byte-Algorithmus zurückkehren sollten. Ein viel wahrscheinlicherer "Fehler" wäre, "über die Größe der Zuordnung hinaus" zu lesen, was technisch undefined behaviorzu den verschiedenen C-Sprachstandards führt und von so etwas als Fehler gekennzeichnet würde valgrind.

Zusammenfassend lässt sich sagen, dass alles, was mit Blöcken größer als Byte arbeitet, um schneller zu werden, wie dies der Antwortcode tut und der Code vom OP angegeben wird, aber eine bytegenaue Lesesemantik aufweisen muss, wahrscheinlich "fehlerhaft" ist, wenn es kein lengthArgument dafür gibt Kontrollieren Sie die Eckfälle des "letzten Lesevorgangs".

Der Code in dieser Antwort ist ein Kernel, mit dem das erste Byte in einem natürlichen CPU-Wortgrößenblock schnell gefunden werden kann, wenn die Ziel-CPU einen schnellen ctzBefehl hat. Es ist trivial, Dinge hinzuzufügen, wie sicherzustellen, dass nur korrekt ausgerichtete natürliche Grenzen oder irgendeine Form von lengthBindung funktionieren , die es Ihnen ermöglichen würden, aus dem Hochgeschwindigkeitskern heraus und zu einer langsameren Byte-für-Byte-Prüfung zu wechseln.

Das OP sagt auch in den Kommentaren:

Die ctz-Optimierung macht nur für die O (1) -Schwanzoperation einen Unterschied. Es könnte die Leistung mit winzigen Saiten verbessern (zB strchr("abc", 'a');aber sicherlich nicht mit Saiten größerer Größe).

Ob diese Aussage wahr ist oder nicht, hängt stark von der jeweiligen Mikroarchitektur ab. Bei Verwendung des kanonischen 4-stufigen RISC-Pipeline-Modells ist dies mit ziemlicher Sicherheit der Fall. Es ist jedoch äußerst schwer zu sagen, ob dies für eine moderne, nicht in Ordnung befindliche superskalare CPU zutrifft, bei der die Kerngeschwindigkeit die Speicher-Streaming-Geschwindigkeit völlig in den Schatten stellen kann. In diesem Fall ist es nicht nur plausibel, sondern durchaus üblich, dass es eine große Lücke in der "Anzahl der Befehle, die zurückgezogen werden können" im Verhältnis zu "der Anzahl der Bytes, die gestreamt werden können" gibt, so dass Sie "die" haben Anzahl der Anweisungen, die für jedes gestreamte Byte zurückgezogen werden können ". Wenn dies groß genug ist, kann der ctz+ Shift-Befehl "kostenlos" ausgeführt werden.


"Verwenden strchrSie für Nadeln der Länge 1. " - Sie haben nach den schnellsten Suchalgorithmen für Teilzeichenfolgen gefragt. Das Finden eines Teilstrings der Länge 1 ist nur ein Sonderfall, der auch optimiert werden kann. Wenn Sie Ihren aktuellen Sonderfallcode gegen Teilzeichenfolgen der Länge 1 ( strchr) mit den oben genannten austauschen , werden die Dinge (möglicherweise abhängig von der strchrImplementierung) schneller. Der obige Algorithmus ist fast dreimal schneller als eine typische naive strchrImplementierung.
Johne

2
OP sagte, dass die Zeichenfolge ordnungsgemäß mit Null abgeschlossen wurde, sodass Ihre Diskussion darüber char bytes[1] = {0x55};irrelevant ist. Sehr relevant ist Ihr Kommentar dazu, dass dies für jeden Algorithmus zum Lesen von Wörtern gilt, der die Länge vorher nicht kennt.
Seth Robertson

1
Das Problem betrifft nicht die von mir zitierte Version, da Sie es nur für ausgerichtete Zeiger verwenden - zumindest tun dies korrekte Implementierungen.
R .. GitHub STOP HELPING ICE

2
@R, es hat nichts mit "ausgerichteten Zeigern" zu tun. Hypothetisch, wenn Sie eine Architektur hatten, die VM-Schutz mit Granularität auf Byte-Ebene unterstützte und jede mallocZuordnung auf beiden Seiten "ausreichend aufgefüllt" war und das VM-System den granularen Byte-Schutz für diese Zuordnung erzwang ... unabhängig davon, ob der Zeiger ausgerichtet ist oder nicht ( Unter der Annahme einer trivialen intnatürlichen 32-Bit- Ausrichtung ist dies nicht möglich. Es ist weiterhin möglich, dass dieser ausgerichtete Lesevorgang über die Größe der Zuordnung hinaus liest. JEDER , der über die Größe der Zuordnung hinaus gelesen wird, ist undefined behavior.
Johne

5
@ Johne: +1 zu kommentieren. Konzeptionell haben Sie Recht, aber die Realität ist, dass der Schutz und die Durchsetzung von Byte-Granularitätsschutzmaßnahmen so teuer sind, dass sie nicht existieren und niemals existieren werden. Wenn Sie wissen, dass der zugrunde liegende Speicher Seitengranularitätszuordnungen sind, die aus dem Äquivalent von erhalten werden mmap, ist die Ausrichtung ausreichend.
R .. GitHub STOP HELPING ICE

3

Suchen Sie einfach nach "schnellster strstr" und wenn Sie etwas Interessantes sehen, fragen Sie mich einfach.

Meiner Ansicht nach legen Sie sich selbst zu viele Einschränkungen auf (ja, wir alle wollen bei maximalem Sucher sublinear linear), aber es braucht einen echten Programmierer, um einzugreifen. Bis dahin denke ich, dass der Hash-Ansatz einfach eine raffinierte Lösung ist ( gut verstärkt durch BNDM für kürzere 2..16 Muster).

Nur ein kurzes Beispiel:

Doing Suche nach Muster (32bytes) in String (206908949bytes) as-one-line ... Überspringen-Leistung (größer-the-besser): 3041%, 6.801.754 Überspringen / Iterationen Railgun_Quadruplet_7Hasherezade_hits / Railgun_Quadruplet_7Hasherezade_clocks: 0/58 Railgun_Quadruplet_7Hasherezade Leistung: 3483KB / Uhr

Doing Suche nach Muster (32bytes) in String (206908949bytes) as-one-line ... Überspringen-Leistung (größer-the-besser): 1.554%, 13.307.181 Überspringen / Iterationen Boyer_Moore_Flensburg_hits / Boyer_Moore_Flensburg_clocks: 0/83 Boyer_Moore_Flensburg Leistung: 2434KB / Uhr

Suche nach Muster (32 Byte) in String (206908949 Byte) als einzeilige ... Sprungleistung (größer, desto besser): 129%, 160239051 überspringt / iteriert Zwei-Wege-Hits / Zwei-Wege-Uhren: 0/816 Zwei -Wegleistung : 247KB / Uhr

Sanmayce,
Grüße


3

Der Zwei-Wege-Algorithmus, den Sie in Ihrer Frage erwähnen (was übrigens unglaublich ist!), Wurde kürzlich verbessert, um effizient mit Multibyte-Wörtern gleichzeitig zu arbeiten: Optimal Packed String Matching .

Ich habe nicht das ganze Papier gelesen, aber es scheint, dass sie sich darauf verlassen, dass ein paar neue, spezielle CPU-Anweisungen (z. B. in SSE 4.2 enthalten) O (1) für ihren Anspruch auf Zeitkomplexität sind. Wenn sie jedoch nicht verfügbar sind, können sie dies simulieren Sie sie in O-Zeit (log log w) für w-Bit-Wörter, die nicht schlecht klingen.


3

Sie könnten beispielsweise 4 verschiedene Algorithmen implementieren. Führen Sie alle M Minuten (empirisch zu bestimmen) alle 4 auf aktuellen realen Daten aus. Sammeln Sie Statistiken über N Läufe (auch TBD). Verwenden Sie dann nur den Gewinner für die nächsten M Minuten.

Protokollieren Sie Statistiken zu Wins, damit Sie Algorithmen, die niemals gewinnen, durch neue ersetzen können. Konzentrieren Sie die Optimierungsbemühungen auf die erfolgreichste Routine. Achten Sie nach Änderungen an der Hardware, Datenbank oder Datenquelle besonders auf die Statistiken. Fügen Sie diese Informationen nach Möglichkeit in das Statistikprotokoll ein, damit Sie sie nicht anhand des Datums- / Zeitstempels des Protokolls ermitteln müssen.


3

Ich habe kürzlich ein nützliches Tool entdeckt, um die Leistung der verschiedenen verfügbaren Algen zu messen: http://www.dmi.unict.it/~faro/smart/index.php

Vielleicht finden Sie es nützlich. Wenn ich mich kurz mit dem Suchalgorithmus für Teilzeichenfolgen befassen müsste, würde ich mich für Knuth-Morris-Pratt entscheiden.


Danke für den Link. Die Tests sehen für das Timing im typischen Fall interessant aus, aber nicht für das Erfassen von Worst-Case-Zeiten.
R .. GitHub STOP HELPING ICE

2

Möglicherweise möchten Sie auch verschiedene Benchmarks mit verschiedenen Arten von Zeichenfolgen haben, da dies einen großen Einfluss auf die Leistung haben kann. Die Algen werden unterschiedliche Leistungen erbringen, basierend auf der Suche nach natürlicher Sprache (und selbst hier kann es aufgrund der unterschiedlichen Morphologien immer noch feinkörnige Unterscheidungen geben), DNA-Strings oder zufälligen Strings usw.

Die Alphabetgröße spielt in vielen Algen eine Rolle, ebenso wie die Nadelgröße. Zum Beispiel kann Horspool aufgrund der unterschiedlichen Alphabetgröße gut mit englischem Text umgehen, aber schlecht mit DNA, was der Regel für schlechte Charaktere das Leben schwer macht. Die Einführung des Good-Suffix erleichtert dies erheblich.


0

Ich weiß nicht, ob es das absolut Beste ist, aber ich habe gute Erfahrungen mit Boyer-Moore gemacht .


Kennen Sie eine Möglichkeit, Boyer-Moores schlechten Schichttisch mit Two-Way zu kombinieren? Glibc macht eine Variante davon für lange Nadeln (> 32 Byte), prüft aber nur das letzte Byte. Das Problem ist, dass Two-Way den rechten Teil der Nadel von links nach rechts durchsuchen muss, während Boyer-Moores schlechte Verschiebung bei der Suche von rechts nach links am effizientesten ist. Ich habe versucht, es in Zwei-Wege-Richtung von links nach rechts zu verwenden (Vorschub durch Verschiebungstabelle oder normale Zwei-Wege-Fehlanpassung der rechten Hälfte, je nachdem, welcher Wert länger ist), aber ich habe in den meisten Fällen eine Verlangsamung von 5-10% gegenüber normaler Zwei-Wege-Abweichung erhalten Es konnten keine Fälle gefunden werden, in denen die Leistung verbessert wurde.
R .. GitHub STOP HELPING ICE

0

Dies beantwortet die Frage nicht direkt, aber wenn der Text sehr groß ist, wie wäre es, wenn Sie ihn in überlappende Abschnitte (Überlappung um eine Musterlänge) unterteilen und dann gleichzeitig die Abschnitte mithilfe von Threads durchsuchen. In Bezug auf den schnellsten Algorithmus ist Boyer-Moore-Horspool meiner Meinung nach einer der schnellsten, wenn nicht der schnellste unter den Varianten von Boyer-Moore. Ich habe in diesem Thema einige Boyer-Moore-Varianten (deren Namen ich nicht kenne) veröffentlicht. Algorithmus schneller als BMH-Suche (Boyer-Moore-Horspool) .


0

Das schnellste ist derzeit EPSM von S. Faro und OM Kulekci. Siehe http://www.dmi.unict.it/~faro/smart/algorithms.php?algorithm=EPSM&code=epsm

"Exact Packed String Matching" optimiert für SIMD SSE4.2 (x86_64 und aarch64). Es arbeitet stabil und am besten auf allen Größen.

Die Site, auf die ich verlinkt habe, vergleicht 199 schnelle String-Suchalgorithmen, wobei die üblichen (BM, KMP, BMH) ziemlich langsam sind. EPSM übertrifft alle anderen, die hier auf diesen Plattformen erwähnt werden. Es ist auch das Neueste.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.