Was kann die enorme Leistungseinbuße beim Schreiben eines Verweises auf einen Heap-Speicherort erklären?

9

Bei der Untersuchung der subtileren Konsequenzen von generationsbedingten Speicherbereinigungen auf die Anwendungsleistung habe ich eine ziemlich erstaunliche Diskrepanz in der Leistung einer sehr einfachen Operation - eines einfachen Schreibens in eine Heap-Position - in Bezug darauf, ob der geschriebene Wert primitiv oder ist, getroffen eine Referenz.

Das Microbenchmark

%Vor%

Die Ergebnisse

%Vor%

Da die ganze Schleife fast 8 mal langsamer ist, ist der Schreibvorgang wahrscheinlich mehr als 10 mal langsamer. Was könnte eine solche Verlangsamung erklären?

Die Geschwindigkeit des Schreibens des primitiven Arrays beträgt mehr als 10 Schreibvorgänge pro Nanosekunde. Vielleicht sollte ich die Kehrseite meiner Frage fragen: Was macht primitives Schreiben so schnell? ? (BTW habe ich überprüft, skalieren die Zeiten linear mit Array-Größe.)

Beachten Sie, dass dies alles single-threaded ist; Die Angabe von @Threads(2) erhöht beide Messungen, aber das Verhältnis wird ähnlich sein.

Ein bisschen Hintergrund: die Kartentabelle und die zugehörige Schreibsperre

Ein Objekt in der jungen Generation könnte nur von einem Objekt in der alten Generation aus erreichbar sein. Um das Sammeln von Live-Objekten zu vermeiden, muss der YG-Collector über alle Referenzen Bescheid wissen, die seit der letzten YG-Sammlung in den Bereich "Alte Generation" geschrieben wurden. Dies wird durch eine Art "schmutzige Flagtabelle" erreicht, die Kartentabelle genannt wird, die ein Flag für jeden Block von 512 Bytes Heap besitzt.

Der "hässliche" Teil des Schemas kommt, wenn wir erkennen, dass jeder Schreibvorgang einer Referenz von einem karteninvarianten -aufrechterhaltenden Stück Code begleitet sein muss: der Position in der Kartentabelle Die schreibgeschützte Adresse muss als dirty gekennzeichnet sein. Dieser Codeabschnitt wird als Schreibsperre bezeichnet.

Bei einem bestimmten Maschinencode sieht das wie folgt aus:

%Vor%

Und das ist alles, was es für die gleiche Operation auf hoher Ebene braucht, wenn der geschriebene Wert primitiv ist:

%Vor%

Die Schreibbarriere scheint "nur" einen weiteren Schreibvorgang zu liefern, aber meine Messungen zeigen, dass dies zu einer Größenordnungsabschwächung führt. Ich kann das nicht erklären.

UseCondCardMark macht es nur schlimmer

Es gibt ein ziemlich obskures JVM-Flag, das verhindern soll, dass die Kartentabelle schreibt, wenn der Eintrag bereits als schmutzig markiert ist. Dies ist in erster Linie in einigen degenerierten Fällen wichtig, in denen eine Menge des Schreibens von Kartentabellen falsches Teilen zwischen Threads über CPU-Caches verursacht. Jedenfalls habe ich mit dieser Flagge versucht:

%Vor%     
Marko Topolnik 03.02.2014, 09:44
quelle

1 Antwort

4

Zitieren der autoritativen Antwort von Vladimir Kozlov auf hotspot-compiler-dev Mailingliste:

  

Hallo Marko,

     

Für primitive Arrays verwenden wir handgeschriebenen Assembler-Code, der XMM verwendet   registriert sich als Vektoren für die Initialisierung. Für Objekt-Arrays haben wir das nicht getan   optimieren Sie es, weil es nicht üblich ist. Wir können es ähnlich verbessern   was wir für arracopy gemacht haben, aber wir entschieden uns, es für jetzt zu verlassen.

     

Grüße,   Vladimir

Ich habe mich auch gefragt, warum der optimierte Code nicht inline ist, und habe auch diese Antwort erhalten:

  

Der Code ist nicht klein, also haben wir uns entschieden, ihn nicht einzubinden. Schau auf   MacroAssembler :: generate_fill () in macroAssembler_x86.cpp:

     

Ссылка

Meine ursprüngliche Antwort:

Ich habe ein wichtiges Bit im Maschinencode verpasst, anscheinend weil ich die On-Stack-Ersetzung-Version der kompilierten Methode anstelle der für nachfolgende Aufrufe verwendeten angesehen habe. Es stellt sich heraus, dass HotSpot beweisen konnte, dass meine Schleife dem entspricht, was ein Aufruf von Arrays.fill getan hätte, und ersetzte die gesamte Schleife durch eine Anweisung call für diesen Code. Ich kann den Code dieser Funktion nicht sehen, aber wahrscheinlich verwendet sie jeden möglichen Trick, wie zum Beispiel MMX-Anweisungen, um einen Speicherblock mit demselben 32-Bit-Wert zu füllen.

Das hat mir die Idee gegeben, die tatsächlichen Aufrufe von Arrays.fill zu messen. Ich habe mehr Überraschung:

%Vor%

Die Ergebnisse mit einer Schleife und mit einem Aufruf von fill sind identisch. Wenn überhaupt, ist dies noch verwirrender als die Ergebnisse, die die Frage motiviert haben. Ich hätte zumindest fill erwartet, um von den gleichen Optimierungsideen unabhängig vom Werttyp zu profitieren.

    
Marko Topolnik 03.02.2014, 10:38
quelle