Nach dem Lesen des JSR-133-Kochbuchs für Compiler-Writer über die Implementierung flüchtiger Elemente, insbesondere des Abschnitts "Interaktionen mit atomaren Anweisungen", gehe ich davon aus, dass für das Lesen einer flüchtigen Variablen ohne Aktualisierung eine LoadLoad- oder eine LoadStore-Barriere erforderlich ist. Weiter unten auf der Seite sehe ich, dass LoadLoad und LoadStore auf X86-CPUs praktisch keine Operationen sind. Bedeutet dies, dass flüchtige Lesevorgänge ohne explizite Cache-Ungültigmachung auf x86 ausgeführt werden können und so schnell sind wie ein normaler Lesevorgang (ohne Berücksichtigung der Neuordnungsbeschränkungen für flüchtige Verbindungen)?
Ich glaube, ich verstehe das nicht richtig. Könnte es jemanden interessieren, mich aufzuklären?
EDIT: Ich frage mich, ob es Unterschiede in Multiprozessor-Umgebungen gibt. Auf Systemen mit einer CPU sieht die CPU möglicherweise ihre eigenen Thread-Caches aus, wie John V. feststellt. Auf Systemen mit mehreren CPUs muss es jedoch eine Konfigurationsoption für die CPUs geben, die nicht ausreicht, und der Hauptspeicher muss getroffen werden, wodurch die Volatilität langsamer wird auf Multi-CPU-Systemen, richtig?
PS: Auf meinem Weg, mehr darüber zu erfahren, bin ich über die folgenden großartigen Artikel gestolpert. Da diese Frage für andere interessant sein kann, werde ich meine Links hier teilen:
Antworten:
Unter Intel ist ein unbestrittener flüchtiger Lesevorgang recht günstig. Wenn wir den folgenden einfachen Fall betrachten:
Mit der Fähigkeit von Java 7, Assemblycode zu drucken, sieht die Ausführungsmethode folgendermaßen aus:
Wenn Sie sich die 2 Verweise auf getstatic ansehen, beinhaltet die erste das Laden aus dem Speicher, die zweite überspringt das Laden, wenn der Wert aus den Registern wiederverwendet wird, in die er bereits geladen ist (lang ist 64 Bit und auf meinem 32-Bit-Laptop) es werden 2 Register verwendet).
Wenn wir die Variable l flüchtig machen, ist die resultierende Anordnung anders.
In diesem Fall beinhalten beide getstatischen Verweise auf die Variable l eine Last aus dem Speicher, dh der Wert kann nicht über mehrere flüchtige Lesevorgänge in einem Register gehalten werden. Um sicherzustellen, dass es einen atomaren Lesevorgang gibt, wird der Wert aus dem Hauptspeicher in ein MMX-Register
movsd 0x6fb7b2f0(%ebp),%xmm0
gelesen, wodurch die Leseoperation zu einem einzigen Befehl wird (aus dem vorherigen Beispiel haben wir gesehen, dass der 64-Bit-Wert normalerweise zwei 32-Bit-Lesevorgänge auf einem 32-Bit-System erfordert).Die Gesamtkosten eines flüchtigen Lesevorgangs entsprechen also in etwa einer Speicherlast 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, was einen Hauptspeicher oder möglicherweise einen L3-Cache-Zugriff erfordert. Die tatsächlichen Kosten hängen stark von der CPU-Architektur ab. Selbst zwischen Intel und AMD unterscheiden sich die Cache-Kohärenzprotokolle.
quelle
Im Allgemeinen ist bei den meisten modernen Prozessoren eine flüchtige Last mit einer normalen Last vergleichbar. Ein flüchtiger Speicher ist ungefähr 1/3 der Zeit eines Montior-Enter / Monitor-Exits. Dies ist auf Systemen zu sehen, die Cache-kohärent sind.
Um die Frage des OP zu beantworten, sind flüchtige Schreibvorgänge teuer, während dies beim Lesen normalerweise nicht der Fall ist.
Ja, manchmal erreicht die CPU beim Überprüfen eines Felds nicht einmal den Hauptspeicher, sondern spioniert stattdessen andere Thread-Caches aus und erhält den Wert von dort (sehr allgemeine Erklärung).
Ich stimme jedoch Neils Vorschlag zu, dass Sie ein Feld, auf das mehrere Threads zugreifen, als AtomicReference umschließen sollten. Als AtomicReference führt es ungefähr den gleichen Durchsatz für Lese- / Schreibvorgänge aus, aber es ist auch offensichtlicher, dass auf das Feld von mehreren Threads zugegriffen und diese geändert werden.
Bearbeiten, um die Bearbeitung von OP zu beantworten:
Die Cache-Kohärenz ist ein kompliziertes Protokoll, aber kurz gesagt: CPUs teilen sich eine gemeinsame Cache-Zeile, die an den Hauptspeicher angeschlossen ist. Wenn eine CPU Speicher lädt und keine andere CPU über diesen verfügt, geht die CPU davon aus, dass dies der aktuellste Wert ist. Wenn eine andere CPU versucht, denselben Speicherort zu laden, ist sich die bereits geladene CPU dessen bewusst und teilt tatsächlich den zwischengespeicherten Verweis auf die anfordernde CPU. Jetzt hat die anfordernde CPU eine Kopie dieses Speichers in ihrem CPU-Cache. (Es musste nie im Hauptspeicher nach der Referenz suchen)
Es gibt einiges mehr Protokoll, aber dies gibt eine Vorstellung davon, was los ist. Um auch Ihre andere Frage zu beantworten: Da mehrere Prozessoren fehlen, können flüchtige Lese- / Schreibvorgänge tatsächlich schneller sein als bei mehreren Prozessoren. Es gibt einige Anwendungen, die tatsächlich schneller gleichzeitig mit einer einzelnen CPU als mit mehreren ausgeführt werden.
quelle
In den Worten des Java-Speichermodells (wie für Java 5+ in JSR 133 definiert) erstellt jede Operation - Lesen oder Schreiben - für eine
volatile
Variable eine Vor-Vor- Beziehung in Bezug auf jede andere Operation für dieselbe Variable. Dies bedeutet, dass der Compiler und die JIT gezwungen sind, bestimmte Optimierungen zu vermeiden, z. B. das Neuordnen von Anweisungen innerhalb des Threads oder das Ausführen von Vorgängen nur im lokalen Cache.Da einige Optimierungen nicht verfügbar sind, ist der resultierende Code notwendigerweise langsamer als er gewesen wäre, wenn auch wahrscheinlich nicht sehr viel.
Sie sollten jedoch keine Variable
volatile
erstellen, es sei denn, Sie wissen, dass von mehreren Threads außerhalb vonsynchronized
Blöcken auf sie zugegriffen wird . Selbst dann sollten Sie überlegen, ob Volatile die beste Wahl istsynchronized
,AtomicReference
und seine Freunde, die explizitenLock
Klassen usw.quelle
Der Zugriff auf eine flüchtige Variable ähnelt in vielerlei Hinsicht dem Umschließen des Zugriffs auf eine normale Variable in einen synchronisierten Block. Zum Beispiel verhindert der Zugriff auf eine flüchtige Variable, dass die CPU die Anweisungen vor und nach dem Zugriff neu anordnet, und dies verlangsamt im Allgemeinen die Ausführung (obwohl ich nicht sagen kann, um wie viel).
Im Allgemeinen sehe ich auf einem Multiprozessorsystem nicht, wie der Zugriff auf eine flüchtige Variable ohne Strafe erfolgen kann - es muss eine Möglichkeit geben, sicherzustellen, dass ein Schreibvorgang auf Prozessor A mit einem Lesevorgang auf Prozessor B synchronisiert wird.
quelle