Wie kann ich code in Java zu ermöglichen, SSE verwenden und bounds-check-elimination (oder andere erweiterte Optimierungen)?
Die Situation:
Ich bin die Optimierung einer pure-java-Implementierung des LZF-Kompressions-Algorithmus, die beinhaltet eine Menge von byte[] zugreifen und grundlegende int Mathematik für das hashing und Vergleich. Leistung wirklich zählt, denn das Ziel der Kompression ist die Verringerung der I/O-Anforderungen. Ich bin nicht posting-code, weil es nicht aufgeräumt ist noch nicht, und kann umstrukturiert werden, stark.
Die Fragen:
- Wie kann Schreibe ich meinen code, um es zu ermöglichen, JIT-kompilieren, um eine form mit schnelleren SSE-Operationen?
- Wie kann ich die Struktur es so, dass der compiler kann Sie leicht beseitigen, die array-Grenzen überprüft?
- Gibt es umfassende Referenzen über die relative Geschwindigkeit der bestimmte mathematische Operationen (wie viele inkrementiert/dekrementiert dauert es, gleich eine normal addieren/subtrahieren, wie schnell ist der shift-oder im Vergleich zu einem array-Zugriff)?
- Wie kann ich arbeiten auf die Optimierung der Verzweigung -- ist es besser mehrere bedingte Anweisungen mit kurzen Körper, oder ein paar lange oder kurze mit verschachtelten Bedingungen?
- Mit aktuellen 1.6 JVM, wie viele Elemente kopiert werden müssen, bevor das System.arraycopy beats eine Kopier-Schleife?
Was ich bereits getan habe:
Bevor ich angegriffen für die vorzeitige Optimierung: der grundlegende Algorithmus ist schon Prima, aber die Java-Implementierung ist weniger als 2/3 der Geschwindigkeit entspricht C. habe ich bereits ausgetauscht und kopieren von loops und System.arraycopy, arbeitete an der Optimierung von Schleifen und beseitigt un-Operationen benötigt.
Ich machen starken Gebrauch von bit twiddling und Verpackung bytes in int-Werte für die Leistung, sowie die Verlagerung & maskieren.
Aus rechtlichen Gründen, kann ich nicht anschauen-Implementierungen in ähnlichen Bibliotheken, und Bibliotheken haben eine zu restriktive Lizenzbedingungen zu verwenden.
Anforderungen für eine GUTE (angenommen) Antwort:
- Inakzeptable Antworten: "das ist schneller" ohne eine Erklärung, wie viel UND warum, ODER ist nicht getestet worden mit einem JIT-compiler.
- Borderline Antworten: nicht getestet haben, mit etwas vor-Hotspot 1.4
- Grundlegenden Antworten: wird eine Allgemeine Regel und die Erklärung, warum es schneller in die compiler-Ebene, und ungefähr wie viel schneller
- Gute Antworten: ein paar Beispiele von code, um zu demonstrieren,
- Ausgezeichnete Antworten: haben benchmarks mit den beiden JRE 1.5 und 1.6
- PERFEKTE Antwort: Ist von jemandem, der arbeitete auf der HotSpot-compiler, und kann vollständig erklären oder einen Verweis auf die Bedingungen für eine Optimierung verwendet werden, und wie viel schneller es in der Regel. Vielleicht gehören java-code und Beispiel-Assembler-code generiert HotSpot.
Außerdem: wenn jemand die links mit den Details der Eingeweide der Hotspot-Optimierung und-Verzweigung Leistung, sind willkommen. Ich weiß genug über bytecode, die eine Website Analyse der Leistung bei bytecode eher als sourcecode-Ebene wäre hilfreich.
(Edit) Teilweise Antwort: Bounds-Check-Ellimination:
Dies ist genommen aus gelieferten link zur HotSpot internals wiki unter: https://wikis.oracle.com/display/HotSpotInternals/RangeCheckElimination
HotSpot beseitigen bounds-checks in allen for-Schleifen mit den folgenden Bedingungen:
- Array loop-invariant (nicht umgeschichtet innerhalb der Schleife)
- Index-variable eine Konstante Schrittlänge (erhöht/verringert durch die Konstante Menge an nur einem Ort, wenn möglich)
- Array wird indiziert durch eine lineare Funktion der variable.
Beispiel: int val = array[index*2 + 5]
ODER: int val = array[index+9]
NICHT: int val = array[Math.min(var,index)+7]
Frühe version von code:
Dies ist eine Probe-version. Nicht stehlen, denn es ist eine unveröffentlichte version des Codes für die H2-Datenbank-Projekt. Die Finale version wird open source sein. Dies ist eine Optimierung auf dem code hier: H2 CompressLZF code
Logisch, diese ist identisch mit der development-version, aber das nutzt man eine for (...) - Schleife zum Schritt durch den Eingang, und eine if/else-Schleife für unterschiedliche Logik zwischen wörtlichen und Rückverweis Modi. Es reduziert array-Zugriff und Kontrollen zwischen den Modi.
public int compressNewer(final byte[] in, final int inLen, final byte[] out, int outPos){
int inPos = 0;
//initialize the hash table
if (cachedHashTable == null) {
cachedHashTable = new int[HASH_SIZE];
} else {
System.arraycopy(EMPTY, 0, cachedHashTable, 0, HASH_SIZE);
}
int[] hashTab = cachedHashTable;
//number of literals in current run
int literals = 0;
int future = first(in, inPos);
final int endPos = inLen-4;
//Loop through data until all of it has been compressed
while (inPos < endPos) {
future = (future << 8) | in[inPos+2] & 255;
// hash = next(hash,in,inPos);
int off = hash(future);
//ref = possible index of matching group in data
int ref = hashTab[off];
hashTab[off] = inPos;
off = inPos - ref - 1; //dropped for speed
//has match if bytes at ref match bytes in future, etc
//note: using ref++ rather than ref+1, ref+2, etc is about 15% faster
boolean hasMatch = (ref > 0 && off <= MAX_OFF && (in[ref++] == (byte) (future >> 16) && in[ref++] == (byte)(future >> 8) && in[ref] == (byte)future));
ref -=2; //...EVEN when I have to recover it
//write out literals, if max literals reached, OR has a match
if ((hasMatch && literals != 0) || (literals == MAX_LITERAL)) {
out[outPos++] = (byte) (literals - 1);
System.arraycopy(in, inPos - literals, out, outPos, literals);
outPos += literals;
literals = 0;
}
//literal copying split because this improved performance by 5%
if (hasMatch) { //grow match as much as possible
int maxLen = inLen - inPos - 2;
maxLen = maxLen > MAX_REF ? MAX_REF : maxLen;
int len = 3;
//grow match length as possible...
while (len < maxLen && in[ref + len] == in[inPos + len]) {
len++;
}
len -= 2;
//short matches write length to first byte, longer write to 2nd too
if (len < 7) {
out[outPos++] = (byte) ((off >> 8) + (len << 5));
} else {
out[outPos++] = (byte) ((off >> 8) + (7 << 5));
out[outPos++] = (byte) (len - 7);
}
out[outPos++] = (byte) off;
inPos += len;
//OPTIMIZATION: don't store hashtable entry for last byte of match and next byte
//rebuild neighborhood for hashing, but don't store location for this 3-byte group
//improves compress performance by ~10% or more, sacrificing ~2% compression...
future = ((in[inPos+1] & 255) << 16) | ((in[inPos + 2] & 255) << 8) | (in[inPos + 3] & 255);
inPos += 2;
} else { //grow literals
literals++;
inPos++;
}
}
//write out remaining literals
literals += inLen-inPos;
inPos = inLen-literals;
if(literals >= MAX_LITERAL){
out[outPos++] = (byte)(MAX_LITERAL-1);
System.arraycopy(in, inPos, out, outPos, MAX_LITERAL);
outPos += MAX_LITERAL;
inPos += MAX_LITERAL;
literals -= MAX_LITERAL;
}
if (literals != 0) {
out[outPos++] = (byte) (literals - 1);
System.arraycopy(in, inPos, out, outPos, literals);
outPos += literals;
}
return outPos;
}
Letzte änderung:
Ich ' ve markiert die beste Antwort, die so weit akzeptiert, da die Frist fast bis. Da ich so lange dauerte, bevor Sie sich entscheiden zu post code, werde ich weiter upvote und reagieren auf Kommentare, wo möglich. Entschuldigt, wenn der code unordentlich ist: diese vertreten-code in Entwicklung, nicht Poliert zu Begehen.
InformationsquelleAutor der Frage BobMcGee | 2009-08-29
Du musst angemeldet sein, um einen Kommentar abzugeben.
Nicht eine vollständige Antwort, ich habe einfach nicht die Zeit, um die detaillierte benchmarks Ihre Frage muss aber hoffentlich nützlich.
Lernen Sie Ihren Feind kennen
Sie Zielen auf eine Kombination der JVM (im wesentlichen die JIT) und die zugrunde liegende CPU - /Speicher-subsystem. Also "Dies ist schneller auf der JVM-X" ist nicht wahrscheinlich zu sein, gilt in allen Fällen, wie Sie bewegen in aggressiver Optimierungen.
Wenn Sie Ihre Zielgruppe/Anwendung laufen größtenteils auf eine bestimmte Architektur sollten Sie erwägen, die Investition in Werkzeuge, die spezifisch für ihn.
* Wenn Sie Ihre Leistung auf x86 ist der kritische Faktor, so intel VTune ist hervorragend für den Drill-down in der Art jit-output-Analyse Sie beschreiben.
* Die Unterschiede zwischen 64 bit und 32-bit-JITs können erheblich sein, vor allem auf x86-Plattformen, wo die Aufruf-Konventionen können sich ändern und enregistering Möglichkeiten sind sehr unterschiedlich.
Holen Sie sich die richtigen Werkzeuge
Würden Sie wahrscheinlich wollen, um einen sampling-profiler. Der overhead der Instrumentierung (und dem damit verbundenen klopfen auf Dinge wie inlining, cache-Verschmutzung und code-Größe der inflation), die für Ihre spezifischen Bedürfnisse wäre viel zu groß. Der intel VTune Analyzer kann tatsächlich verwendet werden, für Java-obwohl die integration nicht so eng wie andere.
Wenn Sie die sun JVM und sind erst zufrieden, zu wissen, was die neueste/beste version ist, dann werden die verfügbaren Optionen untersuchen Sie die Ausgabe des JIT sind beträchtlich, wenn Sie wissen ein bisschen Montage.
Diese Artikel details manche interessante Analyse mit dieser Funktionalität
Lernen Sie von anderen Implementierungen
Die änderungshistorie ändern die Geschichte zeigt, dass die bisherige inline-Montage war in der Tat kontraproduktiv und ermöglicht, dass der compiler nehmen Sie die totale Kontrolle über die Ausgabe (mit tweaks in code eher als Richtlinien in der Montage) lieferte bessere Ergebnisse.
Einige Besonderheiten
Seit LZF ist eine effiziente unmanaged Implementierung auf modernen desktop-CPUS, weitgehend Speicher-Bandbreite begrenzt (damit es compered, um die Geschwindigkeit eines unoptimised memcpy) wird, müssen Sie den code zu verbleiben vollständig im level-1-cache.
Als solche statische Felder kann man nicht machen, in Konstanten gesetzt werden sollte, innerhalb der gleichen Klasse wie diese Werte werden oft in den gleichen Bereich des Speichers widmet sich die vtables und meta-Daten, die im Zusammenhang mit Klassen.
Objekt-Zuordnungen, die nicht gefangen werden, indem man Escape-Analyse (nur in 1.6 ab) müssen vermieden werden.
Den c-code macht aggressive Einsatz von loop unrolling. Jedoch ist die performance auf älterer (1.4-ära) VM ist stark abhängig von der mode ist die JVM in. Offenbar letzteres sun jvm-Versionen sind aggressiver bei inlining und abrollen, vor allem im server-Modus.
Den prefetch-instrctions generiert der JIT-können machen den Unterschied auf code wie dieser ist in der Nähe von Speicher gebunden.
"Es kommt direkt auf uns"
Deinem Ziel bewegt, und weiterhin. Wieder Marc Lehmann bisherigen Erfahrungen:
Sogar kleinere updates für die jvm eingebunden werden können signifikante änderungen compiler
Kapitän Offensichtlich
Messen, Messen, Messen. WENN Sie Ihre Bibliothek enthalten (in einer separaten dll), die eine einfache und leicht zu führen benchmark-logs die relevanten Informationen (vm-version, cpu, OS, command line switches etc) und lässt diese einfach wieder zurück schicken, Sie werden Sie erhöhen Ihre Reichweite, am besten du wirst die Leute mit es dass Pflege.
InformationsquelleAutor der Antwort ShuggyCoUk
Soweit bounds-check Beseitigung betrifft, glaube ich das neue JDK, beinhaltet bereits ein verbesserter Algorithmus, vermeidet es, Wann immer es möglich ist. Dies sind die beiden wichtigsten Veröffentlichungen zu diesem Thema:
Effektive Verbesserung der Loop-Versionsverwaltung in Java -. Link. Dieses Papier ist von den Jungs von Excelsior, wer implementiert die Technik in Ihren Jet JVM.
Gibt es auch diese blog-Eintrag, der beschreibt eines der Papiere oberflächlich, und stellt auch einige benchmarking-Ergebnisse, nicht nur für arrays sondern auch für die Arithmetik in der neuen JDK. Die Kommentare der blog-Eintrag sind ebenfalls sehr interessant, da die Autoren der oben genannten Papiere vorhanden, einige sehr interessante Kommentare und diskutieren Sie die Argumente. Auch gibt es einige Hinweise zu anderen ähnlichen blog-Beiträge zu diesem Thema.
Hoffe, es hilft.
InformationsquelleAutor der Antwort João Silva
Es ist eher unwahrscheinlich, dass Sie benötigen, um den JIT-compiler zu viel optimieren eine einfache Zahl Knirschen Algorithmus wie LZW. ShuggyCoUk erwähnt, aber ich denke, es verdient Besondere Aufmerksamkeit:
Die cache-Freundlichkeit Ihrer code wird ein großer Faktor sein.
Müssen Sie reduzieren die Größe Ihrer woking gesetzt und verbessert den Datenzugriff Ort so viel wie möglich. Sie erwähnen "Verpackung bytes in int-Werte für Leistung". Das klingt wie mit int-Werten zu halten, byte-Werte, um Sie am "word" ausgerichtet. Tun Sie das nicht! Der erhöhte Speicherbedarf wird, überwiegen Gewinne (ich habe einmal Umgerechnet einige ECC Anzahl Knirschen code von int[] nach byte[] und bekam ein 2x-speed-up).
Auf dem off-chance, dass Sie diese nicht kennen: wenn Sie behandeln müssen einige Daten, wie beide Byte-und int-Werte, die Sie nicht haben, zu verschieben und |-Maske - Verwendung
ByteBuffer.asIntBuffer()
und Verwandte Methoden.Besser tun, den benchmark selbst. Wenn ich es Weg zurück, wenn in Java-1,3 mal, es war irgendwo um 2000 Elemente.
InformationsquelleAutor der Antwort Michael Borgwardt
Vielen Antworten bisher, aber ein paar zusätzliche Dinge:
H2-Umsetzung optimiert wurde auch einiges (zum Beispiel: es ist nicht klar, die hash-array mehr, dies macht oft Sinn); und ich habe tatsächlich geholfen, ändern Sie für die Verwendung in einem anderen Java-Projekt. Mein Beitrag war meist nur die änderung, die es tun, werden mehr optimal für nicht-streaming-Fall, aber nicht berühren die enge encode/decode Schleifen.
InformationsquelleAutor der Antwort StaxMan