Escape Analysis in Java 6?
Letzten Monat haben wir unseren Kurs Speeding up Java Applications in den holländischen Wäldern abgehalten. Bei der Vorbereitung habe ich einige der neuen Themen mit meinem Kollegen und Kursleiter Kirk Pepperdine besprochen. Wir erläutern die neuen Funktionen von Java 6 und wie sie zur Verbesserung Ihrer Leistung beitragen können. Eine der ausgefeilteren Funktionen auf VM-Ebene nennt sich Escape-Analyse. Die Frage ist: Funktioniert sie wirklich?
Wir erzählen von der Fluchtanalyse nicht nur im Kurs, sondern auch in unserem Performance Top-10 Blog und Podcast sowie in meiner J-Fall Präsentation. Brian Goetz
public String concatBuffer(String s1, String s2, String s3) {
StringPuffer sb = new StringPuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
Hier wird das Ergebnis sb nur lokal in der Methode verwendet und kein Verweis kann aus diesem Bereich entweichen. Es ist ein Kandidat für Stack-Allokation und Lock Elision. Mit Lock Elision haben Sie bei der Verwendung von Objekten, die thread-sicher sind, aber nicht im verwendeten Kontext sein müssen, nicht den Overhead, thread-sicher zu sein. Dies ist also wie "no cure - no pay": keine Threads zum Anhalten - kein Overhead. Der VM-Schalter zur Aktivierung dieser Funktion lautet: -XX:+DoEscapeAnalysis. Biased locking erklärt Biased locking ist eine weitere Threading-Optimierung in Java 6. Da die meisten Objekte während ihrer Lebensdauer von höchstens einem Thread gesperrt werden, ist es sinnvoll, für diesen Fall zu optimieren. Das ist es, was Biased Locking bewirkt. Es erlaubt diesem Thread, ein Objekt für sich selbst zu sperren. Sobald er das Objekt vorgespannt hat, kann er es anschließend sperren und entsperren, ohne auf teure atomare Anweisungen zurückgreifen zu müssen. Der VM-Schalter lautet: -XX:+UseBiasedLocking und ist standardmäßig aktiviert. Vergröberung von Sperren erklärt Eine weitere Threading-Optimierung ist die Vergröberung oder Zusammenführung von Sperren. Benachbarte synchronisierte Blöcke werden zu einem synchronisierten Block zusammengeführt oder mehrere synchronisierte Methoden werden zu einer zusammengefasst. Dies gilt nur, wenn das gleiche Sperrobjekt verwendet wird. Dadurch wird der Sperraufwand reduziert. Beispiel:
public static String concatToBuffer(StringBuffer sb, String s1, String s2, String s3) {
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
In diesem Beispiel ist die StringBuffer-Sperre kein Kandidat für die Lock Elision, da sie außerhalb der Methode verwendet wird. Die dreimalige Erfassung und Freigabe der Sperre kann jedoch auf eine einzige reduziert werden, nachdem die Append-Methoden eingefügt wurden. Der VM-Schalter lautet: -XX:+EliminateLocks und ist standardmäßig aktiviert.
Benchmarking - Messen, ob es funktioniert
Um das, was wir predigen, in die Praxis umzusetzen, habe ich einen sperrintensiven LockTest-Benchmark erstellt, um diese drei VM-Optionen zu testen. Der Code ist unten abgebildet. Ich möchte ihn zunächst mit deaktivierten Optionen auf meinem Vista-Laptop ausführen:
java -XX:-DoEscapeAnalysis -XX:-EliminateLocks -XX:-UseBiasedLocking LockTest
Ich verstehe:
Unrecognized VM option '-DoEscapeAnalysis'
Could not create the Java virtual machine.
Hmmm, seltsam. Ich bin sicher, dass dies die richtige Option ist. Nach einigem Nachforschen haben wir herausgefunden, dass dies nur für die Server-VM funktioniert, nicht aber für die Client-VM, die standardmäßig auf 32-Bit-Windows läuft. Diese VMs sind derzeit zwei separate Binärdateien. Nachdem ich mich mit meiner wertvollen Quelle des Performance-Teams bei Sun in Verbindung gesetzt hatte, erfuhr ich, dass diese Option im Gegensatz zu anderen Locking-Optimierungen nicht standardmäßig aktiviert ist. Eine weitere überraschende Information war, dass die Allokationsoptimierung (unter Verwendung der Escape-Analyse) noch nicht im JDK enthalten ist. Sie arbeiten noch daran und erwarten, dass sie mit dem JDK-Update im Frühjahr 2008 verfügbar sein wird. Das ist also enttäuschend, es gab eine 'kleine' Verzögerung seit der Aussage von Brian Goetz im Jahr 2005. Er sagte mir jedoch auch, dass die Elision von Escape-Analysen-Locks ab der letzten JDK-Version (6_03) tatsächlich verfügbar ist. Lassen Sie uns den Test machen. Hier sind meine Ergebnisse:
>java -server -XX:-DoEscapeAnalysis -XX:-EliminateLocks -XX:-UseBiasedLocking LockTest
StringBuffer: 6553 ms.
StringBuilder: 1836 ms.
Thread-Sicherheits-Overhead von StringBuffer: 256%
.
java -server -XX:+DoEscapeAnalysis -XX:-EliminateLocks -XX:-UseBiasedLocking LockTest StringBuffer: 6546 ms. StringBuilder: 1872 ms. Thread-Sicherheits-Overhead von StringBuffer: 249% . java -server -XX:-DoEscapeAnalysis -XX:+EliminateLocks -XX:-UseBiasedLocking LockTest StringBuffer: 3101 ms. StringBuilder: 1836 ms. Thread-Sicherheits-Overhead von StringBuffer: 68% . java -server -XX:-DoEscapeAnalysis -XX:-EliminateLocks -XX:+UseBiasedLocking LockTest StringBuffer: 2852 ms. StringBuilder: 1855 ms. Thread-Sicherheits-Overhead von StringBuffer: 53% . java -server -XX:-DoEscapeAnalysis -XX:+EliminateLocks -XX:+UseBiasedLocking LockTest StringBuffer: 2645 ms. StringBuilder: 1823 ms. Thread-Sicherheits-Overhead von StringBuffer: 45% Schlussfolgerungen Wir sehen also deutlich, dass für diesen offensichtlichen Fall die Optimierungen der Escape-Analyse aus Java 6 entkommen sind. Sie ist für die Client-VM nicht verfügbar, sie ist standardmäßig deaktiviert und wenn Sie sie für die Server-VM aktivieren, hilft sie nicht wesentlich: der Thread-Sicherheits-Overhead bleibt hier bei etwa 250%. Idealerweise sollte der Threadsicherheits-Overhead mit der Escape-Analyse Lock Elision auf 0% sinken. Glücklicherweise sehen wir jedoch, dass Lock Coarsening und Biased Locking sehr hilfreich sind und den Overhead auf etwa 50% senken. Wir sehen bei dieser Übung erneut, dass wir Annahmen in Frage stellen, die richtigen Fragen stellen und tatsächlich messen sollten, um Beweise zu erhalten. Hoffentlich bringt das Frühjahrs-Update 2008 von Java 6 die richtigen Optimierungen für die Escape-Analyse, die schon so lange versprochen wurden! Wir werden Sie auf dem Laufenden halten. LockTest Code:
public class LockTest { private static final int MAX = 20000000; // 20 Millionen . public static void main(String[] args) throws InterruptedException { // Wärmen Sie den Methoden-Cache auf concatBuffer("Josh", "James", "Duke"); concatBuilder("Josh", "James", "Duke"); . System.gc(); Thread.sleep(1000); . long start = System.currentTimeMillis(); for (int i = 0; i < MAX; i++) { concatBuffer("Josh", "James", "Duke"); } long bufferCost = System.currentTimeMillis() - start; System.out.println("StringPuffer: " + bufferCost + " ms."); . System.gc(); Thread.sleep(1000); . start = System.currentTimeMillis(); for (int i = 0; i < MAX; i++) { concatBuilder("Josh", "James", "Duke"); } long builderCost = System.currentTimeMillis() - start; System.out.println("StringBuilder: " + builderCost + " ms."); System.out.println("Thread-Sicherheits-Overhead von StringBuffer: " + ((bufferCost * 10000 / (builderCost * 100)) - 100) + "%n"); } . public static String concatBuffer(String s1, String s2, String s3) { StringPuffer sb = new StringPuffer(); sb.append(s1); sb.append(s2); sb.append(s3); return sb.toString(); } . public static String concatBuilder(String s1, String s2, String s3) { StringBuilder sb = new StringBuilder(); sb.append(s1); sb.append(s2); sb.append(s3); return sb.toString(); } }
Verfasst von
Jeroen Borgers
Unsere Ideen
Weitere Blogs
Contact



