Einführung
Ich habe diesen Lieblingsalgorithmus, den ich vor einiger Zeit gemacht habe und den ich immer in neuen Programmiersprachen, Plattformen usw. schreibe und neu schreibe, als eine Art Benchmark. Obwohl meine Hauptprogrammiersprache C # ist, habe ich den Code buchstäblich kopiert und die Syntax leicht geändert, ihn in Java erstellt und festgestellt, dass er 1000x schneller ausgeführt wird.
Der Code
Es gibt ziemlich viel Code, aber ich werde nur diesen Ausschnitt präsentieren, der das Hauptproblem zu sein scheint:
for (int i = 0; i <= s1.Length; i++)
{
for (int j = i + 1; j <= s1.Length - i; j++)
{
string _s1 = s1.Substring(i, j);
if (tree.hasLeaf(_s1))
...
Die Daten
Es ist wichtig darauf hinzuweisen, dass die Zeichenfolge s1 in diesem speziellen Test eine Länge von 1 Million Zeichen (1 MB) hat.
Messungen
Ich habe meine Codeausführung in Visual Studio profiliert, weil ich dachte, dass die Art und Weise, wie ich meinen Baum erstelle oder wie ich ihn durchquere, nicht optimal ist. Nach Prüfung der Ergebnisse scheint die Leitung string _s1 = s1.Substring(i, j);
mehr als 90% der Ausführungszeit aufzunehmen!
Zusätzliche Beobachtungen
Ein weiterer Unterschied, den ich bemerkt habe, ist, dass Java, obwohl mein Code Single-Threaded ist, es schafft, ihn mit allen 8 Kernen (100% CPU-Auslastung) auszuführen, während mein C # -Code selbst mit Parallel.For () - und Multi-Threading-Techniken 35- schafft. Höchstens 40%. Da der Algorithmus linear mit der Anzahl der Kerne (und der Frequenz) skaliert, habe ich dies kompensiert und dennoch führt das Snippet in Java eine Größenordnung von 100-1000x schneller aus.
Argumentation
Ich gehe davon aus, dass der Grund dafür in der Tatsache liegt, dass Strings in C # unveränderlich sind, sodass String.Substring () eine Kopie erstellen muss. Da es sich um eine verschachtelte for-Schleife mit vielen Iterationen handelt, gehe ich davon aus, dass viel kopiert wird Die Speicherbereinigung wird fortgesetzt, ich weiß jedoch nicht, wie Substring in Java implementiert ist.
Frage
Welche Möglichkeiten habe ich derzeit? An der Anzahl und Länge der Teilzeichenfolgen führt kein Weg vorbei (dies ist bereits maximal optimiert). Gibt es eine Methode, die ich nicht kenne (oder vielleicht die Datenstruktur), die dieses Problem für mich lösen könnte?
Angeforderte minimale Implementierung (aus Kommentaren)
Ich habe die Implementierung des Suffixbaums ausgelassen, der im Aufbau O (n) und im Durchlauf O (log (n)) ist
public static double compute(string s1, string s2)
{
double score = 0.00;
suffixTree stree = new suffixTree(s2);
for (int i = 0; i <= s1.Length; i++)
{
int longest = 0;
for (int j = i + 1; j <= s1.Length - i; j++)
{
string _s1 = s1.Substring(i, j);
if (stree.has(_s1))
{
score += j - i;
longest = j - i;
}
else break;
};
i += longest;
};
return score;
}
Screenshot-Ausschnitt des Profilers
Beachten Sie, dass dies mit der Zeichenfolge s1 mit einer Größe von 300.000 Zeichen getestet wurde. Aus irgendeinem Grund werden 1 Million Zeichen in C # nie beendet, während es in Java nur 0,75 Sekunden dauert. Der verbrauchte Speicher und die Anzahl der Speicherbereinigungen scheinen kein Speicherproblem anzuzeigen. Der Peak betrug ungefähr 400 MB, aber angesichts des riesigen Suffixbaums scheint dies normal zu sein. Auch keine seltsamen Müllsammelmuster wurden entdeckt.
quelle
String
in Java ist auch unveränderlich. Hast du esStringBuilder
stattdessen versucht ?Span<char>
, wie andere Kommentatoren betonten, einfach(string, startIndex, endIndex)
in Methoden wie verwendenstree.has
. Verwenden Sie innerhalb der Methoden den String indexer (s[i]
), der ohnechar
Zuordnung zurückgibt .Antworten:
Problemursprung
Nach einem glorreichen Kampf, der zwei Tage und drei Nächte dauerte (und erstaunlichen Ideen und Gedanken aus den Kommentaren), habe ich es endlich geschafft, dieses Problem zu beheben!
Ich möchte eine Antwort für alle veröffentlichen, die auf ähnliche Probleme stoßen, bei denen die
string.Substring(i, j)
Funktion keine akzeptable Lösung ist, um den Teilstring eines Strings zu erhalten, da der String entweder zu groß ist und Sie sich das Kopieren nicht leisten könnenstring.Substring(i, j)
(es muss) Erstellen Sie eine Kopie, da C # -Strings unveränderlich sindstring.Substring(i, j)
(oder in meinem Fall für verschachtelte for-Schleifen), und dies wird dem Garbage Collector schwer fällt, oder wie in meinem Fall beiden !Versuche
Ich habe viele vorgeschlagene Dinge ausprobiert, wie den StringBuilder , Streams , die nicht verwaltete Speicherzuweisung mit Intptr und Marshal innerhalb des
unsafe{}
Blocks und sogar das Erstellen einer IEnumerable und die Rückgabe der Zeichen als Referenz innerhalb der angegebenen Positionen. Alle diese Versuche scheiterten letztendlich, weil irgendeine Form des Zusammenfügens der Daten durchgeführt werden musste, da es für mich keine einfache Möglichkeit gab, meinen Baum Zeichen für Zeichen zu durchlaufen, ohne die Leistung zu gefährden. Wenn es nur eine Möglichkeit gäbe, mehrere Speicheradressen innerhalb eines Arrays gleichzeitig zu überspannen, wie Sie es in C ++ mit einer Zeigerarithmetik tun könnten .. außer es gibt .. (Dank an @Ivan Stoevs Kommentar)Die Lösung
Die Lösung war die Verwendung
System.ReadOnlySpan<T>
(möglicherweise nichtSystem.Span<T>
aufgrund unveränderlicher Zeichenfolgen), die es uns unter anderem ermöglicht, Unterarrays von Speicheradressen innerhalb eines vorhandenen Arrays zu lesen, ohne Kopien zu erstellen.Dieser Teil des Codes gepostet:
string _s1 = s1.Substring(i, j); if (stree.has(_s1)) { score += j - i; longest = j - i; }
Wurde wie folgt geändert:
if (stree.has(i, j)) { score += j - i; longest = j - i; }
Wobei
stree.has()
nun zwei ganze Zahlen (Position und Länge des Teilstrings) benötigt werden und:ReadOnlySpan<char> substr = s1.AsSpan(i, j);
Beachten Sie, dass die
substr
Variable buchstäblich eine Referenz auf eine Teilmenge von Zeichen des ursprünglichens1
Arrays und keine Kopie ist! (Dies1
Variable wurde über diese Funktion zugänglich gemacht.)Beachten Sie, dass ich zum Zeitpunkt des Schreibens C # 7.2 und .NET Framework 4.6.1 verwende. Um die Span-Funktion zu erhalten, musste ich zu Projekt> NuGet-Pakete verwalten gehen, das Kontrollkästchen "Vorabversion einschließen" aktivieren und nach System suchen .Speichern und installieren.
Beim erneuten Ausführen des ersten Tests (bei Zeichenfolgen mit einer Länge von 1 Million Zeichen, dh 1 MB) wurde die Geschwindigkeit von 2+ Minuten (ich habe das Warten nach 2 Minuten aufgegeben) auf ~ 86 Millisekunden erhöht !!
quelle
s1.AsSpan(i, j)
etwas schneller sein?