Speicherbarriere - Memory barrier

Eine Speicherbarriere , auch als Membar- , Memory- Fence- oder Fence-Anweisung bekannt , ist eine Art von Barrier- Anweisung , die eine Zentraleinheit (CPU) oder einen Compiler veranlasst , eine Ordnungsbeschränkung für Speicheroperationen zu erzwingen , die vor und nach der Barrier-Anweisung ausgegeben werden. Dies bedeutet in der Regel, dass Operationen, die vor der Barriere ausgegeben wurden, garantiert ausgeführt werden, bevor Operationen nach der Barriere ausgegeben wurden.

Speicherbarrieren sind notwendig, da die meisten modernen CPUs Leistungsoptimierungen verwenden, die zu einer ungeordneten Ausführung führen können . Diese Neuordnung von Speicheroperationen (Laden und Speichern) bleibt normalerweise innerhalb eines einzelnen Ausführungsthreads unbemerkt , kann jedoch bei gleichzeitigen Programmen und Gerätetreibern zu unvorhersehbarem Verhalten führen, wenn sie nicht sorgfältig kontrolliert wird. Die genaue Natur einer Ordnungsbeschränkung ist hardwareabhängig und wird durch das Speicherordnungsmodell der Architektur definiert . Einige Architekturen bieten mehrere Barrieren zum Erzwingen unterschiedlicher Ordnungsbeschränkungen.

Speicherbarrieren werden normalerweise verwendet, wenn Low-Level- Maschinencode implementiert wird , der auf Speicher arbeitet, der von mehreren Geräten gemeinsam genutzt wird. Ein solcher Code umfasst Synchronisationsprimitive und blockierungsfreie Datenstrukturen auf Mehrprozessorsystemen und Gerätetreiber, die mit Computerhardware kommunizieren .

Beispiel

Wenn ein Programm auf einem Einzel-CPU-Rechner ausgeführt wird, führt die Hardware die erforderliche Buchhaltung durch, um sicherzustellen, dass das Programm so ausgeführt wird, als ob alle Speicheroperationen in der vom Programmierer angegebenen Reihenfolge (Programmreihenfolge) ausgeführt würden, sodass keine Speicherbarrieren erforderlich sind. Wenn der Speicher jedoch von mehreren Geräten gemeinsam genutzt wird, wie z. B. anderen CPUs in einem Mehrprozessorsystem oder speicherabgebildeten Peripheriegeräten , kann sich ein Zugriff außerhalb der Reihenfolge auf das Programmverhalten auswirken. Beispielsweise kann eine zweite CPU von der ersten CPU vorgenommene Speicheränderungen in einer von der Programmreihenfolge abweichenden Reihenfolge sehen.

Ein Programm wird durch ein Verfahren ausgeführt werden kann , die Multi-Threading (dh einen Software - Thread wie p - Strang an einen Hardware - Thread im Gegensatz). Verschiedene Prozesse teilen sich keinen Speicherplatz, daher gilt diese Diskussion nicht für zwei Programme, von denen jedes in einem anderen Prozess läuft (daher ein anderer Speicherplatz). Sie gilt für zwei oder mehr (Software-)Threads, die in einem einzigen Prozess laufen (dh ein einzelner Speicherplatz, wobei sich mehrere Software-Threads einen einzigen Speicherplatz teilen). Mehrere Software-Threads innerhalb eines einzigen Prozesses können gleichzeitig auf einem Multi-Core-Prozessor ausgeführt werden .

Das folgende Multithread-Programm, das auf einem Multi-Core-Prozessor ausgeführt wird, zeigt ein Beispiel dafür, wie sich eine solche Ausführung außerhalb der Reihenfolge auf das Programmverhalten auswirken kann:

Anfänglich halten Speicherorte xund fbeide den Wert 0. Der auf Prozessor Nr. 1 ausgeführte Software-Thread durchläuft eine Schleife, während der Wert von fNull ist, und gibt dann den Wert von aus x. Der auf Prozessor Nr. 2 ausgeführte Software-Thread speichert den Wert 42in xund speichert den Wert dann 1in f. Pseudocode für die beiden Programmfragmente ist unten gezeigt.

Die Schritte des Programms entsprechen einzelnen Prozessoranweisungen.

Thread #1 Kern #1:

 while (f == 0);
 // Memory fence required here
 print x;

Thread #2 Kern #2:

 x = 42;
 // Memory fence required here
 f = 1;

Man könnte erwarten, dass die print-Anweisung immer die Zahl "42" ausgibt; jedoch werden , wenn Thread # 2 der Speicheroperationen ausgeführt out-of-order, ist es möglich faktualisiert werden , bevor x , und die print - Anweisung könnte daher „0“ drucken. In ähnlicher Weise können die Ladeoperationen von Thread Nr. 1 außerhalb der Reihenfolge ausgeführt werden und xkönnen gelesen werden, bevor sie f überprüft werden, und die print-Anweisung könnte daher wiederum einen unerwarteten Wert ausgeben. Für die meisten Programme ist keine dieser Situationen akzeptabel. Vor der Zuweisung von Thread #2 muss eine Speicherbarriere eingefügt werden, fum sicherzustellen, dass der neue Wert von xfür andere Prozessoren bei oder vor der Änderung des Wertes von sichtbar ist f. Ein weiterer wichtiger Punkt ist, dass auch vor dem Zugriff von Thread #1 eine Speicherbarriere eingefügt werden muss, xum sicherzustellen, dass der Wert von xnicht gelesen wird, bevor die Änderung des Wertes von gesehen wird f.

Ein weiteres Beispiel ist, wenn ein Fahrer die folgende Sequenz ausführt:

 prepare data for a hardware module
 // Memory fence required here
 trigger the hardware module to process the data

Wenn die Speicheroperationen des Prozessors außerhalb der Reihenfolge ausgeführt werden, kann das Hardwaremodul getriggert werden, bevor die Daten im Speicher bereit sind.

Ein weiteres veranschaulichendes Beispiel (ein nicht triviales, das in der Praxis auftritt) finden Sie unter doppelt geprüftes Sperren .

Multithreaded-Programmierung und Speichersichtbarkeit

Multithreaded - Programme in der Regel Synchronisation verwenden Primitiven durch eine High-Level - Programmierumgebung, wie vorgesehen , Java und .NET Framework oder ein Application Programming Interface (API) wie POSIX Threads oder Windows - API . Synchronisationsprimitive wie Mutexe und Semaphoren werden bereitgestellt, um den Zugriff auf Ressourcen von parallelen Ausführungsthreads zu synchronisieren. Diese Primitive werden normalerweise mit den Speicherbarrieren implementiert, die erforderlich sind, um die erwartete Semantik der Speichersichtbarkeit bereitzustellen . In solchen Umgebungen ist die explizite Verwendung von Speicherbarrieren im Allgemeinen nicht erforderlich.

Jede API oder Programmierumgebung hat im Prinzip ihr eigenes High-Level-Speichermodell, das ihre Speichersichtbarkeitssemantik definiert. Obwohl Programmierer in solchen Umgebungen auf hoher Ebene normalerweise keine Speicherbarrieren verwenden müssen, ist es wichtig, ihre Semantik der Speichersichtbarkeit so weit wie möglich zu verstehen. Ein solches Verständnis ist nicht unbedingt leicht zu erreichen, da die Semantik der Speichersichtbarkeit nicht immer konsistent spezifiziert oder dokumentiert ist.

So wie die Semantik von Programmiersprachen auf einer anderen Abstraktionsebene definiert ist als Maschinensprachen- Opcodes , ist das Speichermodell einer Programmierumgebung auf einer anderen Abstraktionsebene definiert als das eines Hardware-Speichermodells. Es ist wichtig, diesen Unterschied zu verstehen und zu erkennen, dass es nicht immer eine einfache Zuordnung zwischen der Low-Level-Hardware-Speicherbarriere-Semantik und der High-Level-Speichersichtbarkeitssemantik einer bestimmten Programmierumgebung gibt. Infolgedessen kann die Implementierung von POSIX-Threads auf einer bestimmten Plattform stärkere Barrieren verwenden, als von der Spezifikation gefordert. Programme, die die Speichersichtbarkeit wie implementiert statt wie angegeben nutzen, sind möglicherweise nicht portierbar.

Out-of-order-Execution versus Compiler-Neuordnungsoptimierungen

Speicherbarrierebefehle adressieren Neuordnungseffekte nur auf Hardwareebene. Compiler können als Teil des Programmoptimierungsprozesses auch Anweisungen neu anordnen . Obwohl die Auswirkungen auf das parallele Programmverhalten in beiden Fällen ähnlich sein können, ist es im Allgemeinen notwendig, separate Maßnahmen zu ergreifen, um die Neuordnungsoptimierungen des Compilers für Daten zu verhindern, die von mehreren Ausführungsthreads gemeinsam genutzt werden können. Beachten Sie, dass solche Maßnahmen normalerweise nur für Daten erforderlich sind, die nicht durch Synchronisationsprimitive geschützt sind, wie sie im vorherigen Abschnitt beschrieben wurden.

In C und C++ sollte das Schlüsselwort volatile C- und C++-Programmen den direkten Zugriff auf speicherabgebildete E/A ermöglichen . Memory-mapped I/O erfordert im Allgemeinen, dass die im Quellcode angegebenen Lese- und Schreibvorgänge in der genauen angegebenen Reihenfolge ohne Auslassungen erfolgen. Auslassungen oder Neuordnungen von Lese- und Schreibvorgängen durch den Compiler würden die Kommunikation zwischen dem Programm und dem Gerät unterbrechen, auf das über speicherabgebildete E/A zugegriffen wird. Der AC- oder C++-Compiler darf weder Lesevorgänge von und Schreibvorgängen in flüchtige Speicherorte auslassen, noch darf er Lese-/Schreibvorgänge relativ zu anderen solchen Aktionen für denselben flüchtigen Speicherort (Variable) neu anordnen. Das Schlüsselwort volatile garantiert keine Speicherbarriere , um die Cache-Konsistenz zu erzwingen. Daher reicht die Verwendung von volatile allein nicht aus, um eine Variable für die Interthread-Kommunikation auf allen Systemen und Prozessoren zu verwenden.

Die C- und C++-Standards vor C11 und C++11 adressieren nicht mehrere Threads (oder mehrere Prozessoren), und daher hängt die Nützlichkeit von volatile vom Compiler und der Hardware ab. Obwohl volatile garantiert, dass die flüchtigen Lese- und Schreibvorgänge in der genauen Reihenfolge erfolgen, die im Quellcode angegeben ist, kann der Compiler Code generieren (oder die CPU kann die Ausführung neu anordnen), sodass ein flüchtiger Lese- oder Schreibvorgang in Bezug auf nicht -volatile liest oder schreibt, wodurch seine Nützlichkeit als Inter-Thread-Flag oder Mutex eingeschränkt wird. Das zu verhindern ist Compiler-spezifisch, aber einige Compiler, wie gcc , werden Operationen um Inline-Assembly-Code mit volatilen und "memory" -Tags nicht neu anordnen, wie in: asm volatile ("" ::: "memory"); (Weitere Beispiele finden Sie unter Speicherreihenfolge#Compile-time memory ordering ). Darüber hinaus ist nicht garantiert, dass flüchtige Lese- und Schreibvorgänge von anderen Prozessoren oder Kernen aufgrund von Caching, Cache-Kohärenzprotokoll und gelockerter Speicherreihenfolge in derselben Reihenfolge gesehen werden , was bedeutet, dass flüchtige Variablen allein möglicherweise nicht einmal als Inter-Thread-Flags oder Mutexes funktionieren .

Siehe auch

Verweise

Externe Links