Implementierung eines Acquires für eine Version von Unsafe.putOrdered * ()?

8

Was ist Ihrer Meinung nach der beste Weg, den Erwerbsteil eines Releasepaares in Java zu implementieren?

Ich versuche, einige der Aktionen in einer Anwendung von mir zu modellieren, indem ich die klassische Semantik von release / acquire verwende (ohne StoreLoad und ohne sequentielle Konsistenz über Threads hinweg).

Es gibt mehrere Möglichkeiten, um das ungefähre Äquivalent eines Store-Releases im JDK zu erreichen. java.util.concurrent.Atomic*.lazySet() und die zugrunde liegenden sun.misc.Unsafe.putOrdered*() sind die am häufigsten genannten Ansätze dafür. Es gibt jedoch keinen offensichtlichen Weg, ein Load-Acquiry zu implementieren.

  • Die JDK-APIs, die lazySet() erlauben, verwenden volatile variables meist intern, so dass ihre Store-Releases mit flüchtigen Lasten kombiniert werden. Theoretisch sollten volatile Lasten teurer sein als Load-Acquires und sollten nicht mehr als ein reines Load-Acquiring im Kontext einer vorangegangenen Store-Release liefern.

  • sun.misc.Unsafe stellt keine getAcquire()* -Äquivalente der putOrdered*() -Methoden zur Verfügung, obwohl solche Erwerbsmethoden für die kommende VarHandles-API geplant sind.

  • Etwas, das klingt, als würde es funktionieren, ist eine einfache Ladung, gefolgt von sun.misc.Unsafe.loadFence() . Es ist etwas beunruhigend, dass ich das nirgendwo sonst gesehen habe. Dies kann mit der Tatsache zusammenhängen, dass es ein ziemlich hässlicher Hack ist.

P.S. Ich verstehe gut, dass diese Mechanismen nicht vom JMM abgedeckt werden, dass sie nicht ausreichen, um die sequentielle Konsistenz zu erhalten, und dass die von ihnen erzeugten Aktionen keine Synchronisationsaktionen sind (z. B. verstehe ich, dass sie IRIW brechen). Ich verstehe auch, dass die Store-Releases, die von Atomic*/Unsafe zur Verfügung gestellt werden, am häufigsten zum eifrigen Ausgleichen von Referenzen oder in Producer / Consumer-Szenarios als optimierter Message-Passing-Mechanismus für einen wichtigen Index verwendet werden.

    
Dimitar Dimitrov 08.05.2016, 22:14
quelle

2 Antworten

3

Volatile lesen ist genau das, was Sie suchen.

In der Tat haben entsprechende volatile Operationen bereits eine Semantik der Freigabe / Akquirierung (sonst passiert-vorher ist nicht möglich für gepaartes flüchtiges Schreiben-Lesen), aber gepaarte flüchtige Operationen sollten nicht nur sequentiell konsistent sein (~ passiert-vorher), sondern auch sie sollten in Gesamtsynchronisierungsreihenfolge sein, Deshalb wird StoreLoad barrier nach volatile write eingefügt: um die Reihenfolge der flüchtigen Schreibvorgänge an verschiedenen Stellen zu garantieren, so dass alle Threads diese Werte in der gleichen Reihenfolge sehen.

Flüchtige Lese hat Semantik erwerben: Proof von Hotspot Codebase, auch hier gibt es eine direkte Empfehlung von Doug Lea im JSR-133 Kochbuch ( LoadLoad und LoadStore Barrieren nach jedem flüchtigen Lesen).

Unsafe.loadFence() hat auch Semantik ( proof ), aber nicht verwendet, um den Wert zu lesen (Sie können das gleiche mit einfach flüchtigen Lesen), aber reorder blank reads mit nachfolgenden flüchtigen lesen zu verhindern. Dies wird in StampedLock für optimistisches Lesen verwendet (siehe StampedLock#validate Methodenimplementierung und -verwendungen).

Nach Diskussion in Kommentaren aktualisieren.

Lassen Sie uns überprüfen, ob Unsafe#loadStore() und volatile read die gleichen sind und eine Semantik besitzen.

Ich betrachte den Hotspot C1-Compiler-Quellcode um das Lesen aller Optimierungen in C2 zu vermeiden. Er transformiert Bytecode (tatsächlich nicht Bytecode, aber seine Interpreterdarstellung) in LIR (Low-Level Intermediate Representation) und übersetzt Graphen in tatsächliche Opcodes, abhängig von der Zielmikroarchitektur.

Unsafe#loadFence intrinsische mit _loadFence alias. In C1 LIR Generator es erzeugt dies:

%Vor%

wobei __ Makros für die LIR-Generierung sind.

Lassen Sie sich jetzt an flüchtiger Lese aussehen Implementierung im selben LIR-Generator. Es versucht, Nulltests einzufügen, prüft IRIW, prüft, ob wir auf x32 sind und versucht 64-Bit-Werte zu lesen (um mit SSE / FPU etwas Magie zu erzeugen) und führt uns schließlich zum selben Code:

%Vor%

Der Assembler-Generator fügt dann plattformspezifische Anweisungen zum Akquirieren ein hier .

Betrachte spezifische Implementierungen (keine Links hier, aber alle können in src / cpu / {$ cpu_model} / vm / c1_LIRAssembler _ {$ cpu_model} .cpp) gefunden werden

  • SPARC

    %Vor%
  • x86

    %Vor%
  • Aarch64 (schwaches Speichermodell, Barrieren sollten vorhanden sein)

    %Vor%

    Nach AARCH Architekturbeschreibung solch ein Member wird als dmb ishld Anweisung nach dem Laden kompiliert.

  • PowerPC (auch schwaches Speichermodell)

    %Vor%

    , das dann in den spezifischen PowerPC-Befehl lwsync transformiert wird. Laut den Kommentaren lwsync ist semantisch äquivalent zu

      

    lwsync Bestellungen Store | Store,                           Laden | Speichern,                           Laden | Laden,                   aber nicht Speichern | Laden

    Aber solange PowerPC keine schwächeren Barrieren hat, ist dies die einzige Wahl, um eine Acquisition-Semantik auf PowerPC zu implementieren.

Schlussfolgerungen

Volatile Reads und Unsafe#loadFence() sind hinsichtlich der Speicherordnung gleich (aber vielleicht nicht im Hinblick auf mögliche Compiler-Optimierungen), auf den meisten populären x86-Systemen ist es kein Op, und PowerPC ist die einzige unterstützte Architektur, die keine genauen Barrieren besitzt .

    
qwwdfsad 08.05.2016, 23:29
quelle
1

Abhängig von Ihren genauen Anforderungen ist die Ausführung einer nichtflüchtigen Last, möglicherweise gefolgt von einer möglichen flüchtigen Last, die beste, die Sie in Java erhalten können.

Sie können dies mit einer Kombination von

tun %Vor%

Dieses Muster kann in Ringpuffern verwendet werden, um die Abwanderung von Cache-Zeilen zu minimieren.

    
Peter Lawrey 08.05.2016 22:21
quelle