129 Stimmen

Ist flüchtig teuer?

Nach der Lektüre Das JSR-133-Kochbuch für Compiler-Autoren über die Implementierung von volatile, insbesondere den Abschnitt "Interaktionen mit atomaren Befehlen", gehe ich davon aus, dass das Lesen einer flüchtigen Variablen, ohne sie zu aktualisieren, eine LoadLoad- oder eine LoadStore-Sperre erfordert. Weiter unten auf der Seite sehe ich, dass LoadLoad und LoadStore auf X86-CPUs praktisch keine Operationen sind. Bedeutet dies, dass flüchtige Leseoperationen ohne explizite Cache-Invalidierung auf x86 durchgeführt werden können und genauso schnell sind wie ein normales Lesen von Variablen (ohne Berücksichtigung der Umordnungsbeschränkungen von volatile)?

Ich glaube, ich habe das nicht richtig verstanden. Könnte mich jemand aufklären?

EDIT: Ich frage mich, ob es in Multiprozessor-Umgebungen Unterschiede gibt. Auf Single-CPU-Systemen könnte die CPU auf ihre eigenen Thread-Caches zugreifen, wie John V. sagt, aber auf Multi-CPU-Systemen muss es eine Konfigurationsoption für die CPUs geben, dass dies nicht ausreicht und der Hauptspeicher verwendet werden muss, wodurch die Flüchtigkeit auf Multi-CPU-Systemen langsamer wird, richtig?

PS: Auf meinem Weg, mehr darüber zu erfahren, bin ich über die folgenden großartigen Artikel gestolpert, und da diese Frage auch für andere interessant sein könnte, werde ich meine Links hier teilen:

1 Stimmen

Sie können meinen Beitrag über die Konfiguration mit mehreren CPUs lesen, auf die Sie sich beziehen. Es kann vorkommen, dass auf Multi-CPU-Systemen für eine kurzzeitige Referenz nicht mehr als ein einziges Lesen/Schreiben im Hauptspeicher stattfindet.

2 Stimmen

Das flüchtige Lesen selbst ist nicht teuer. Die Hauptkosten liegen darin, dass es Optimierungen verhindert. In der Praxis sind diese Kosten im Durchschnitt auch nicht sehr hoch, es sei denn, flüchtiges Lesen wird in einer engen Schleife verwendet.

2 Stimmen

Dieser Artikel auf infoq ( infoq.com/articles/memory_barriers_jvm_concurrency ) könnte Sie auch interessieren. Es zeigt die Auswirkungen von volatile und synchronized auf den generierten Code für verschiedene Architekturen. Dies ist auch ein Fall, in dem der jvm besser abschneiden kann als ein "ahead of time"-Compiler, da er weiß, ob er auf einem Uniprozessorsystem läuft und einige Speicherbarrieren auslassen kann.

130voto

Michael Barker Punkte 13553

Bei Intel ist ein unkontrolliertes flüchtiges Lesen recht billig. Betrachten wir den folgenden einfachen Fall:

public static long l;

public static void run() {        
    if (l == -1)
        System.exit(-1);

    if (l == -2)
        System.exit(-1);
}

Mit der Fähigkeit von Java 7, Assembler-Code zu drucken, sieht die Ausführungsmethode etwa so aus:

# {method} 'run2' '()V' in 'Test2'
#           [sp+0x10]  (sp of caller)
0xb396ce80: mov    %eax,-0x3000(%esp)
0xb396ce87: push   %ebp
0xb396ce88: sub    $0x8,%esp          ;*synchronization entry
                                    ; - Test2::run2@-1 (line 33)
0xb396ce8e: mov    $0xffffffff,%ecx
0xb396ce93: mov    $0xffffffff,%ebx
0xb396ce98: mov    $0x6fa2b2f0,%esi   ;   {oop('Test2')}
0xb396ce9d: mov    0x150(%esi),%ebp
0xb396cea3: mov    0x154(%esi),%edi   ;*getstatic l
                                    ; - Test2::run@0 (line 33)
0xb396cea9: cmp    %ecx,%ebp
0xb396ceab: jne    0xb396ceaf
0xb396cead: cmp    %ebx,%edi
0xb396ceaf: je     0xb396cece         ;*getstatic l
                                    ; - Test2::run@14 (line 37)
0xb396ceb1: mov    $0xfffffffe,%ecx
0xb396ceb6: mov    $0xffffffff,%ebx
0xb396cebb: cmp    %ecx,%ebp
0xb396cebd: jne    0xb396cec1
0xb396cebf: cmp    %ebx,%edi
0xb396cec1: je     0xb396ceeb         ;*return
                                    ; - Test2::run@28 (line 40)
0xb396cec3: add    $0x8,%esp
0xb396cec6: pop    %ebp
0xb396cec7: test   %eax,0xb7732000    ;   {poll_return}
;... lines removed

Wenn Sie sich die 2 Verweise auf getstatic ansehen, beinhaltet der erste ein Laden aus dem Speicher, der zweite überspringt das Laden, da der Wert aus den Registern wiederverwendet wird, in die er bereits geladen ist (long ist 64 Bit und auf meinem 32-Bit-Laptop verwendet er 2 Register).

Wenn wir die Variable l flüchtig machen, ist die resultierende Baugruppe anders.

# {method} 'run2' '()V' in 'Test2'
#           [sp+0x10]  (sp of caller)
0xb3ab9340: mov    %eax,-0x3000(%esp)
0xb3ab9347: push   %ebp
0xb3ab9348: sub    $0x8,%esp          ;*synchronization entry
                                    ; - Test2::run2@-1 (line 32)
0xb3ab934e: mov    $0xffffffff,%ecx
0xb3ab9353: mov    $0xffffffff,%ebx
0xb3ab9358: mov    $0x150,%ebp
0xb3ab935d: movsd  0x6fb7b2f0(%ebp),%xmm0  ;   {oop('Test2')}
0xb3ab9365: movd   %xmm0,%eax
0xb3ab9369: psrlq  $0x20,%xmm0
0xb3ab936e: movd   %xmm0,%edx         ;*getstatic l
                                    ; - Test2::run@0 (line 32)
0xb3ab9372: cmp    %ecx,%eax
0xb3ab9374: jne    0xb3ab9378
0xb3ab9376: cmp    %ebx,%edx
0xb3ab9378: je     0xb3ab93ac
0xb3ab937a: mov    $0xfffffffe,%ecx
0xb3ab937f: mov    $0xffffffff,%ebx
0xb3ab9384: movsd  0x6fb7b2f0(%ebp),%xmm0  ;   {oop('Test2')}
0xb3ab938c: movd   %xmm0,%ebp
0xb3ab9390: psrlq  $0x20,%xmm0
0xb3ab9395: movd   %xmm0,%edi         ;*getstatic l
                                    ; - Test2::run@14 (line 36)
0xb3ab9399: cmp    %ecx,%ebp
0xb3ab939b: jne    0xb3ab939f
0xb3ab939d: cmp    %ebx,%edi
0xb3ab939f: je     0xb3ab93ba         ;*return
;... lines removed

In diesem Fall beinhalten beide getstatic-Verweise auf die Variable l ein Laden aus dem Speicher, d.h. der Wert kann nicht über mehrere flüchtige Lesevorgänge hinweg in einem Register gehalten werden. Um sicherzustellen, dass ein atomares Lesen erfolgt, wird der Wert aus dem Hauptspeicher in ein MMX-Register gelesen movsd 0x6fb7b2f0(%ebp),%xmm0 was den Lesevorgang zu einer einzigen Anweisung macht (aus dem vorherigen Beispiel haben wir gesehen, dass ein 64-Bit-Wert normalerweise zwei 32-Bit-Lesevorgänge auf einem 32-Bit-System erfordern würde).

Somit entsprechen die Gesamtkosten eines flüchtigen Lesevorgangs in etwa denen einer Speicherladung und können so günstig sein wie ein L1-Cache-Zugriff. Wenn jedoch ein anderer Kern in die flüchtige Variable schreibt, wird die Cache-Zeile ungültig und erfordert einen Hauptspeicher- oder vielleicht einen L3-Cache-Zugriff. Die tatsächlichen Kosten hängen stark von der CPU-Architektur ab. Sogar zwischen Intel und AMD sind die Cache-Kohärenzprotokolle unterschiedlich.

0 Stimmen

Nebenbei bemerkt, Java 6 hat die gleiche Fähigkeit, Versammlungen zu zeigen (es ist der Hotspot, der dies tut)

0 Stimmen

+1 In JDK5 kann volatile nicht neu geordnet werden in Bezug auf cualquier lesen/schreiben (was z.B. das Double-Check-Locking behebt). Bedeutet das, dass es auch Auswirkungen darauf hat, wie nichtflüchtige Felder manipuliert werden? Es wäre interessant, den Zugriff auf flüchtige und nichtflüchtige Felder zu mischen.

0 Stimmen

@evemli, Sie müssen vorsichtig sein, ich habe diese Behauptung selbst einmal aufgestellt, aber sie hat sich als falsch erwiesen. Es gibt einen Grenzfall. Das Java-Speichermodell erlaubt die "Roach Motel"-Semantik, wenn Speicher vor flüchtigen Speichern neu geordnet werden können. Wenn Sie dies aus dem Artikel von Brian Goetz auf der IBM-Website entnommen haben, dann ist es erwähnenswert, dass dieser Artikel die JMM-Spezifikation zu sehr vereinfacht.

22voto

John Vint Punkte 38604

Im Allgemeinen ist bei den meisten modernen Prozessoren eine flüchtige Last mit einer normalen Last vergleichbar. Ein flüchtiger Speicher benötigt etwa 1/3 der Zeit eines Monitor-Entry/Monitor-Exit. Dies ist bei Systemen mit Cache-Kohärenz der Fall.

Um die Frage des Auftraggebers zu beantworten: Flüchtige Schreibvorgänge sind teuer, während Lesevorgänge in der Regel nicht teuer sind.

Bedeutet dies, dass flüchtige Lese Operationen ohne eine explizite explizite Invalidierung des Cache auf x86, und genauso schnell ist wie ein normales Lesen von Variablen (ohne Berücksichtigung der Reordering Einschränkungen von volatile)?

Ja, manchmal greift die CPU bei der Validierung eines Feldes nicht einmal auf den Hauptspeicher zu, sondern spioniert andere Thread-Caches aus und holt sich den Wert von dort (sehr allgemeine Erklärung).

Ich schließe mich jedoch Neils Vorschlag an, dass man ein Feld, auf das mehrere Threads zugreifen, als AtomicReference einpacken sollte. Als AtomicReference hat es ungefähr den gleichen Durchsatz für Lese-/Schreibvorgänge, aber es ist auch offensichtlicher, dass auf das Feld von mehreren Threads zugegriffen wird und es geändert wird.

Bearbeiten, um auf OPs Bearbeitung zu antworten:

Die Cache-Kohärenz ist ein etwas kompliziertes Protokoll, aber kurz gesagt: Die CPUs teilen sich eine gemeinsame Cache-Zeile, die mit dem Hauptspeicher verbunden ist. Wenn eine CPU Speicher lädt und keine andere CPU diesen Speicher hat, geht diese CPU davon aus, dass es sich um den aktuellsten Wert handelt. Wenn eine andere CPU versucht, denselben Speicherplatz zu laden, weiß die bereits geladene CPU davon und gibt den Cache-Verweis an die anfordernde CPU weiter - nun hat die anfordernde CPU eine Kopie dieses Speichers in ihrem CPU-Cache. (Sie musste nie im Hauptspeicher nach dem Verweis suchen)

Es gibt noch eine ganze Reihe weiterer Protokolle, die aber einen Eindruck davon vermitteln, was vor sich geht. Um auch Ihre andere Frage zu beantworten: Ohne mehrere Prozessoren können flüchtige Lese- und Schreibvorgänge tatsächlich schneller sein als mit mehreren Prozessoren. Es gibt einige Anwendungen, die mit einer einzigen CPU tatsächlich schneller laufen als mit mehreren.

6 Stimmen

Eine AtomicReference ist nur ein Wrapper für ein flüchtiges Feld mit zusätzlichen nativen Funktionen, die zusätzliche Funktionen wie getAndSet, compareAndSet usw. bereitstellen, so dass die Verwendung aus Leistungssicht nur dann sinnvoll ist, wenn Sie die zusätzlichen Funktionen benötigen. Aber ich frage mich, warum Sie sich hier auf das Betriebssystem beziehen? Die Funktionalität ist doch direkt in CPU-Opcodes implementiert. Und bedeutet dies, dass auf Multiprozessorsystemen, bei denen eine CPU keine Kenntnis vom Cache-Inhalt der anderen CPUs hat, die Volatilität langsamer ist, weil die CPUs immer auf den Hauptspeicher zugreifen müssen?

0 Stimmen

Youre Recht ich Miss sprach über das OS shouldve schrieb CPU, Festsetzung, dass jetzt. Und ja, ich weiß, AtomicReference ist einfach ein Wrapper für flüchtige Felder, aber es fügt auch als eine Art von Dokumentation, dass das Feld selbst wird von mehreren Threads zugreifen.

0 Stimmen

@John, warum würden Sie eine weitere Umleitung über eine AtomicReference hinzufügen? Wenn Sie CAS benötigen - ok, aber AtomicUpdater könnte eine bessere Option sein. Soweit ich mich erinnere gibt es keine intrinsics über AtomicReference.

14voto

Neil Bartlett Punkte 23356

In den Worten des Java-Speichermodells (wie es für Java 5+ in JSR 133 definiert ist), wird jede Operation - Lesen oder Schreiben - auf einer volatile Variable erzeugt eine passiert-vor Beziehung zu jeder anderen Operation an der gleichen Variablen. Dies bedeutet, dass der Compiler und das JIT gezwungen sind, bestimmte Optimierungen zu vermeiden, wie z. B. die Neuordnung von Anweisungen innerhalb des Threads oder die Durchführung von Operationen nur innerhalb des lokalen Cache.

Da einige Optimierungen nicht verfügbar sind, ist der resultierende Code zwangsläufig langsamer als er es gewesen wäre, wenn auch wahrscheinlich nicht sehr viel.

Trotzdem sollten Sie keine Variable machen volatile es sei denn, Sie wissen, dass der Zugriff von mehreren Threads außerhalb von synchronized Blöcke. Selbst dann sollten Sie abwägen, ob flüchtige Blöcke die beste Wahl sind gegenüber synchronized , AtomicReference und seine Freunde, die explizit Lock Klassen, etc.

4voto

krakover Punkte 2909

Der Zugriff auf eine flüchtige Variable ist in vielerlei Hinsicht ähnlich wie der Zugriff auf eine gewöhnliche Variable in einem synchronisierten Block zu verpacken. Zum Beispiel verhindert der Zugriff auf eine flüchtige Variable, dass die CPU die Anweisungen vor und nach dem Zugriff neu anordnet, was im Allgemeinen die Ausführung verlangsamt (ich kann allerdings nicht sagen, um wie viel).

Generell sehe ich nicht, wie auf einem Multiprozessorsystem der Zugriff auf eine flüchtige Variable straffrei erfolgen kann - es muss eine Möglichkeit geben, um sicherzustellen, dass ein Schreibvorgang auf Prozessor A mit einem Lesevorgang auf Prozessor B synchronisiert wird.

4 Stimmen

Das Lesen von flüchtigen Variablen hat dieselben Nachteile wie ein Monitor-Entry, was die Umordnungsmöglichkeiten von Anweisungen betrifft, während das Schreiben einer flüchtigen Variablen einem Monitor-Exit gleichkommt. Ein Unterschied könnte darin bestehen, welche Variablen (z. B. Prozessor-Caches) geleert oder für ungültig erklärt werden. Während bei synchronized alles geleert oder invalidiert wird, sollte der Zugriff auf die flüchtige Variable immer cache-ignorierend sein.

20 Stimmen

-1, Der Zugriff auf eine flüchtige Variable ist etwas ganz anderes als die Verwendung eines synchronisierten Blocks. Die Eingabe eines synchronisierten Blocks erfordert ein atomares compareAndSet-basiertes Schreiben, um die Sperre aufzuheben, und ein flüchtiges Schreiben, um sie freizugeben. Wenn die Sperre zufriedenstellend ist, muss die Kontrolle vom Benutzerraum in den Kernelraum übergehen, um die Sperre zu vermitteln (das ist der teure Teil). Der Zugriff auf eine flüchtige Sperre bleibt immer im Benutzerraum.

0 Stimmen

@MichaelBarker: Sind Sie sicher, dass alle Monitore vom Kernel und nicht von der App bewacht werden müssen?

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X