Für {A=a; B=b; }, wird “A=a” strikt vor “B=b” ausgeführt?

Lesezeit: 8 Minuten

Benutzeravatar von ACreator
EinSchöpfer

Vermuten A, B, aund b sind alle Variablen, und die Adressen von A, B, aund b sind alle unterschiedlich. Dann für den folgenden Code:

A = a;
B = b;

Verlangt der C- und C++-Standard ausdrücklich A=a vorher streng ausgeführt werden B=b? Da die Adressen von A, B, aund b sind alle unterschiedlich, dürfen Compiler die Ausführungsreihenfolge zweier Anweisungen für bestimmte Zwecke wie die Optimierung vertauschen?

Wenn die Antwort auf meine Frage in C und C++ unterschiedlich ist, würde ich gerne beide wissen.

Edit: Der Hintergrund der Frage ist folgender. Im Brettspiel-KI-Design wird es zur Optimierung verwendet gemeinsam genutzte Hash-Tabelle ohne Sperrederen Richtigkeit stark von der Ausführungsreihenfolge abhängt, wenn wir nicht hinzufügen volatile Beschränkung.

  • Selbst wenn der Compiler garantiert Code in dieser Reihenfolge generiert, führt die CPU selbst eine Ausführung außerhalb der Reihenfolge durch.

    – KitsuneYMG

    15. September 2014 um 11:51 Uhr

  • Nicht nur Compiler dürfen dies tun, CPUs dürfen dies tun, Speichercontroller dürfen dies tun, Caches dürfen dies tun und so weiter.

    – David Schwartz

    15. September 2014 um 12:06 Uhr


  • Jedes Mal, wenn Sie Multithreading betreiben, gelangen Sie in eine völlig andere Dimension. Selbst wenn der Code sequentiell ausgeführt wird, haben Sie (ohne weitere Kontrollen) keine Garantie dafür, dass die Ausführung aus der Sicht eines anderen Prozessors sequentiell erscheint. Wenn Sie versuchen, etwas Ähnliches wie Ihre gemeinsam genutzte Hash-Tabelle zu tun, müssen Sie VIEL Zeit damit verbringen, sich mit Synchronisierungsproblemen zu befassen.

    – Heiße Licks

    15. September 2014 um 12:13 Uhr

  • @ACcreator: Ja, das ist möglich, abhängig vom verwendeten Cache-Kohärenzprotokoll. Beispielsweise bietet x86 stärkere Garantien als Itanium. Bei Multithreading müssen Sie sich auch um Tearing und spekulative Schreibvorgänge kümmern.

    – Ben Voigt

    15. September 2014 um 15:01 Uhr


  • @ACcreator: Verwenden Sie einen Speicherzaun. Das ist billiger als ein kritischer Abschnitt, stellt aber immer noch sicher, dass die Caches auch in der richtigen Reihenfolge synchronisiert werden müssen.

    – Ben Voigt

    15. September 2014 um 15:12 Uhr

Benutzeravatar von David Heffernan
David Heffernan

Beide Standards erlauben es, diese Anweisungen außerhalb der Reihenfolge auszuführen, solange dies das beobachtbare Verhalten nicht ändert. Dies ist als Als-Ob-Regel bekannt:

Beachten Sie, dass, wie in den Kommentaren darauf hingewiesen wird, mit “beobachtbarem Verhalten” das beobachtbare Verhalten eines Programms mit definiertem Verhalten gemeint ist. Wenn Ihr Programm ein undefiniertes Verhalten hat, ist der Compiler davon entbunden, darüber nachzudenken.

  • Beides konnte auch nicht durchgeführt werden, wenn es das beobachtbare Verhalten des Programms nicht beeinflusste. (dh vollständig optimiert)

    – MM

    15. September 2014 um 12:01 Uhr

  • Es ist wahrscheinlich erwähnenswert, dass der Zugriff auf oder die Änderung einer Variablen nur dann als “beobachtbares Verhalten” zählt, wenn die Variable flüchtig ist.

    – Mike Seymour

    15. September 2014 um 12:02 Uhr

  • @DavidHeffernan: Ja, ich hätte präziser sein sollen, sorry. Ich meinte “auf eine grundlegend typisierte Variable zugreifen oder diese ändern”. Natürlich könnten benutzerdefinierte Operationen beobachtbares Verhalten haben.

    – Mike Seymour

    15. September 2014 um 12:06 Uhr

  • es wird sicherlich das beobachtbare Verhalten in schlecht geschriebenen Multithread-Programmen ändern! LOL!

    – Gianluca Ghettini

    15. September 2014 um 12:46 Uhr

  • In diesem Fall denke ich, dass es sich lohnt zu betonen (wie G_G impliziert), dass die „Als-ob“-Anforderung das beobachtbare Verhalten ist eines Programms mit definiertem Verhalten ändert sich nicht. Der Fragesteller darf diese Antwort nicht so verstehen, dass jede Änderung der Befehlsreihenfolge garantiert nicht das Verhalten seines lockless Hashtable ändert. Tatsächlich, wenn er diese Frage stellt, liegt das daran, dass dieser Code einen Datenwettlauf enthält, also ist sein Verhalten nicht definiert und kann sich sehr gut aufgrund von Optimierungen, Planungsunfällen und so weiter ändern.

    – Steve Jessop

    15. September 2014 um 21:11 Uhr


Benutzeravatar von Shafik Yaghmour
Shafik Yaghmur

Der Compiler ist nur verpflichtet, das beobachtbare Verhalten eines Programms zu emulieren, wenn also eine Neuordnung dieses Prinzip nicht verletzen würde, wäre sie erlaubt. Angenommen, das Verhalten ist gut definiert, wenn Ihr Programm enthält undefiniertes Verhalten B. ein Datenrennen, dann ist das Verhalten des Programms unvorhersehbar und würde, wie kommentiert, die Verwendung irgendeiner Form von Synchronisation erfordern, um den kritischen Abschnitt zu schützen.

Eine nützliche Referenz

Ein interessanter Artikel, der dies behandelt, ist Speicherbestellung zur Kompilierzeit und es heißt:

Die Kardinalregel der Speicherumordnung, die allgemein von Compiler-Entwicklern und CPU-Anbietern befolgt wird, könnte wie folgt formuliert werden:

Sie dürfen das Verhalten eines Singlethread-Programms nicht ändern.

Ein Beispiel

Der Artikel enthält ein einfaches Programm, in dem wir diese Neuordnung sehen können:

int A, B;  // Note: static storage duration so initialized to zero

void foo()
{
    A = B + 1;
    B = 0;
}

und zeigt auf höheren Optimierungsstufen B = 0 ist vorher erledigt A = B + 1und wir können dieses Ergebnis mit reproduzieren Gottriegeldie während der Verwendung -O3 ergibt folgendes (live sehen):

movl    $0, B(%rip) #, B
addl    $1, %eax    #, D.1624

Wieso den?

Warum ordnet der Compiler neu? Der Artikel erklärt, dass der Prozessor dies aufgrund der Komplexität der Architektur aus genau demselben Grund tut:

Wie ich eingangs erwähnt habe, ändert der Compiler die Reihenfolge der Speicherinteraktionen aus dem gleichen Grund wie der Prozessor – Leistungsoptimierung. Solche Optimierungen sind eine direkte Folge der modernen CPU-Komplexität.

Normen

Im Entwurf des C++-Standards wird dies im Abschnitt behandelt 1.9 Programmausführung was sagt (Hervorhebung von mir für die Zukunft):

Die semantischen Beschreibungen in dieser Internationalen Norm definieren eine parametrisierte nichtdeterministische abstrakte Maschine. Diese Internationale Norm stellt keine Anforderungen an die Struktur konformer Implementierungen. Insbesondere müssen sie die Struktur der abstrakten Maschine nicht kopieren oder emulieren. Vielmehr sind konforme Implementierungen erforderlich, um (nur) das beobachtbare Verhalten der abstrakten Maschine zu emulieren wie unten erklärt.5

Fußnote 5 sagt uns, dass dies auch als bekannt ist Als-ob-Regel:

Diese Bestimmung wird manchmal als die „Als-ob“-Regelweil eine Implementierung ist frei, jede Anforderung zu missachten dieser Internationalen Norm solange das Ergebnis so ist, als wäre die Anforderung erfüllt worden, soweit dies anhand des beobachtbaren Verhaltens festgestellt werden kann des Programms. Beispielsweise muss eine tatsächliche Implementierung einen Teil eines Ausdrucks nicht auswerten, wenn daraus abgeleitet werden kann, dass sein Wert nicht verwendet wird und dass keine Nebeneffekte erzeugt werden, die das beobachtbare Verhalten des Programms beeinträchtigen.

Der Entwurf C99 und der Entwurf C11 behandeln dies in Abschnitten 5.1.2.3 Programmausführung obwohl wir zum Index gehen müssen, um zu sehen, dass es heißt Als-ob-Regel auch im C-Standard:

Als-ob-Regel, 5.1.2.3

Update zu Lock-Free-Überlegungen

Der Artikel Eine Einführung in die blockierungsfreie Programmierung deckt dieses Thema gut und für die Anliegen des OPs ab gemeinsam genutzte Hash-Tabelle ohne Sperre Implementierung Dieser Abschnitt ist wahrscheinlich der relevanteste:

Speicherbestellung

Wie das Flussdiagramm andeutet, jedes Mal, wenn Sie lock-free-Programmierung für Multicore (oder beliebige Symmetrischer Multiprozessor) und Ihre Umgebung keine sequentielle Konsistenz garantiert, müssen Sie überlegen, wie Sie dies verhindern können
Speicher neu ordnen.

Auf heutigen Architekturen fallen die Tools zum Erzwingen einer korrekten Speicherordnung im Allgemeinen in drei Kategorien, die beides verhindern Compiler neu ordnen und Prozessor-Neuordnung:

  • Eine leichte Synchronisations- oder Zaunanweisung, über die ich in sprechen werde zukünftige Nachrichten;
  • Eine vollständige Memory-Fence-Anweisung, die ich habe zuvor demonstriert;
  • Speicheroperationen, die eine Erfassungs- oder Freigabesemantik bereitstellen.

Die Erwerbssemantik verhindert eine Speicherumordnung von Operationen, die ihr in der Programmreihenfolge folgen, und eine Freigabesemantik verhindert eine Speicherumordnung von Operationen, die ihr vorangehen. Diese Semantik eignet sich besonders in Fällen, in denen es eine Producer/Consumer-Beziehung gibt, bei der ein Thread Informationen veröffentlicht und der andere sie liest. Ich werde auch darüber in einem zukünftigen Beitrag mehr sprechen.

  • Ich frage mich nur, warum GCC asm produziert addl $1, %eax Anstatt von incl %eax? Sogar für a++ es produziert nur a += 1 … ICC verhält sich aber wie erwartet.

    Benutzer719662

    15. September 2014 um 16:38 Uhr

  • @vaxquis ist mir nicht klar, scheint eine Art versuchter Optimierung zu sein, wahrscheinlich abhängig von den getroffenen Annahmen gcc.

    – Shafik Yaghmour

    16. September 2014 um 15:13 Uhr

Besteht keine Abhängigkeit von Weisungen, können diese außer der Reihe ausgeführt werden, auch wenn das Endergebnis davon nicht betroffen ist. Sie können dies beobachten, während Sie einen Code debuggen, der auf einer höheren Optimierungsstufe kompiliert wurde.

Da A = a; und B = b; in Bezug auf Datenabhängigkeiten unabhängig sind, sollte dies keine Rolle spielen. Wenn es eine Ausgabe/ein Ergebnis der vorherigen Anweisung gab, die die Eingabe der nachfolgenden Anweisung beeinflusste, dann ist die Reihenfolge wichtig, andernfalls nicht. dies ist normalerweise eine strikt sequentielle Ausführung.

Meine Lektüre ist, dass dies erforderlich ist, um nach dem C++-Standard zu funktionieren; Wenn Sie jedoch versuchen, dies für die Multithreading-Steuerung zu verwenden, funktioniert es in diesem Zusammenhang nicht, da hier nichts garantiert, dass die Register in der richtigen Reihenfolge in den Speicher geschrieben werden.

Wie Ihre Bearbeitung zeigt, versuchen Sie, es genau dort zu verwenden, wo es nicht funktioniert.

Benutzeravatar von Shafik Yaghmour
Shafik Yaghmur

Es kann von Interesse sein, dass, wenn Sie dies tun:

{ A=a, B=b; /*etc*/ }

Beachten Sie das Komma anstelle des Semikolons.

Dann müssen die C++-Spezifikation und jeder bestätigende Compiler die Ausführungsreihenfolge garantieren, da die Operanden des Kommaoperators immer von links nach rechts ausgewertet werden. Dies kann in der Tat verwendet werden, um zu verhindern, dass der Optimierer Ihre Thread-Synchronisation durch Neuordnung untergräbt. Das Komma wird effektiv zu einer Barriere, über die eine Neuordnung nicht zulässig ist.

1409510cookie-checkFür {A=a; B=b; }, wird “A=a” strikt vor “B=b” ausgeführt?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy