2013-05-23 7 views
12

Ich hatte den Eindruck, dass Speicherbelastungen nicht über eine Acquiring-Last im C++ 11-Speichermodell gehisst werden konnten. Aber wenn man sich den Code anschaut, den gcc 4.8 erzeugt, scheint das nur für andere atomare Lasten zu gelten, nicht für den gesamten Speicher. Wenn das stimmt und das Laden von Lasten nicht den gesamten Speicher synchronisiert (nur std::atomics), dann bin ich nicht sicher, wie es möglich wäre, allgemeine Mutexe in Bezug auf std :: atomic zu implementieren.Heben von nicht-atomaren Lasten durch Erfassen atomarer Lasten

Der folgende Code:

extern std::atomic<unsigned> seq; 
extern std::atomic<int> data; 

int reader() { 
    int data_copy; 
    unsigned seq0; 
    unsigned seq1; 
    do { 
     seq0 = seq.load(std::memory_order_acquire); 
     data_copy = data.load(std::memory_order_relaxed); 
     std::atomic_thread_fence(std::memory_order_acquire); 
     seq1 = seq.load(std::memory_order_relaxed); 
    } while (seq0 != seq1); 
    return data_copy; 
} 

Produziert:

_Z6readerv: 
.L3: 
    mov ecx, DWORD PTR seq[rip] 
    mov eax, DWORD PTR data[rip] 
    mov edx, DWORD PTR seq[rip] 
    cmp ecx, edx 
    jne .L3 
    rep ret 

Was mir richtig aussieht.

jedoch das Ändern von Daten ein int eher als std::atomic:

extern std::atomic<unsigned> seq; 
extern int data; 

int reader() { 
    int data_copy; 
    unsigned seq0; 
    unsigned seq1; 
    do { 
     seq0 = seq.load(std::memory_order_acquire); 
     data_copy = data; 
     std::atomic_thread_fence(std::memory_order_acquire); 
     seq1 = seq.load(std::memory_order_relaxed); 
    } while (seq0 != seq1); 
    return data_copy; 
} 

Dies erzeugt:

_Z6readerv: 
    mov eax, DWORD PTR data[rip] 
.L3: 
    mov ecx, DWORD PTR seq[rip] 
    mov edx, DWORD PTR seq[rip] 
    cmp ecx, edx 
    jne .L3 
    rep ret 

Also, was ist los?

+0

Wenn Sie atomare Operationen umschreiben, um 'laden (rel); fence (acq); 'ändert sich in der zweiten Version die Ausgabe asm? – yohjp

+0

@yoyjp Beziehen Sie sich auf das Laden von 'seq0'? Wenn ja, dann hat das keinen Einfluss auf den generierten Code. – jleahy

+0

Nein, ich erwähnte 'seq1'. Ein "acquire fence", der eine Semantik besitzt, besteht aus 'seq1.load (relaxed) -> fence (acquire)' ops order, nicht 'fence (acquire) -> seq1.load (relaxed)' in C++ 11 memory Modell. C++ 's "Zaun" ** nur ** beeinflusst _happens-before-Beziehung_ zwischen atomaren Operationen und/oder Zäunen, es hat ** no ** direkten Einfluss auf nicht-atomare vars. In diesem Punkt unterscheidet sich C++ 'Fence' ziemlich von der Speicherbarrierenanweisung des Prozessors/Compilers (wie zB mfence of x86). – yohjp

Antwort

4

Warum eine Last über einem acquire gehisst wurde

Ich habe dies auf der gcc bugzilla geschrieben und sie haben es als Fehler bestätigt.

der MEM-Alias-Satz von -1 (ALIAS_SET_MEMORY_BARRIER) soll diese aber PRE nicht wissen, über diese besondere Eigenschaft verhindern, (es sollte "kill" alle Refs es Kreuzung).

Es sieht aus wie die gcc wiki eine schöne Seite über diese hat.

Allgemeinen Freisetzung ist ein Sperrcode sinkend, erwerben ist ein Sperrcode Hissen.

Warum dieser Code noch gebrochen

Wie pro this paper mein Code ist immer noch falsch, weil es ein Daten Rennen führt. Obwohl das gepatchte GCC den richtigen Code generiert, ist es immer noch nicht korrekt, auf data zuzugreifen, ohne es in std::atomic zu verpacken.Der Grund dafür ist, dass Datenrennen ein undefiniertes Verhalten sind, selbst wenn die daraus resultierenden Berechnungen verworfen werden.

Ein Beispiel mit freundlicher Genehmigung von AdamH.Peterson:

int foo(unsigned x) { 
    if (x < 10) { 
     /* some calculations that spill all the 
      registers so x has to be reloaded below */ 
     switch (x) { 
     case 0: 
      return 5; 
     case 1: 
      return 10; 
     // ... 
     case 9: 
      return 43; 
     } 
    } 
    return 0; 
} 

hier ein Compiler könnte den Schalter in eine Sprungtabelle optimieren und dank der if-Anweisung über die Lage wäre, eine Bereichsprüfung zu vermeiden. Wenn Datenrennen jedoch nicht undefiniert sind, ist eine zweite Bereichsüberprüfung erforderlich.

+4

Die 2 sind nicht inkompatibel. Ihr Code hat ein Data Race und der C++ - Standard sagt deutlich (1.10 21), dass Ihr Code auf undefiniertes Verhalten beruht. Der Code ist falsch (oder fehlt zumindest die richtige Synchronisation, um Ihren Punkt zu beweisen). Noch einmal, das HP-Papier macht dies auch klar (der Autor ist einer der Architekten des C++ 11-Speichermodells) 1.10 13 sagt, dass gcc ist nicht erlaubt, Code-hissen zu tun. Wenn dies bei gültigem C++ - Code passiert, handelt es sich um einen Fehler. Der springende Punkt war, dass * wenn * es keine Datenrennen gab, der erzeugte Code korrekt wäre (zumindest sehe ich nicht warum) – Guillaume

+4

@GuillaumeMorin: Ich bin versucht zuzustimmen. Die Freigabesequenz, die durch den Speicher und die Last gebildet wird, ist nur eine Komponente in der Kette "Vorgefallen". Der Code wäre korrekt, wenn der zweite Thread so etwas wie 'if (seq_copy == 2) {data_copy = data; } '. In diesem Fall geschieht "data = 2" * vor dem atomaren Speicher, der * mit der atomaren Ladung, die * vor * der 'data_copy = data' stattfindet. Mit dem Code wie gepostet verursacht der Zugriff auf "Daten" ein Rennen. (Und der korrigierte Code erzeugt auch korrekte Ausgabe für mich.) –

+0

@GuillaumeMorin Ich erkenne, dass du jetzt richtig bist, ich habe meine Antwort geändert, um anderen zu helfen, die das sehen. Es ist eine Schande, dass das Wasser durch die Tatsache getrübt wurde, dass es auch einen Fehler in gcc gab. – jleahy

0

Ich bin noch neu in der Argumentation über diese nicht sequentiell konsistenten Speicherreihenfolge Operationen und Barrieren, aber es könnte sein, dass diese Code-Generierung korrekt (oder eher zulässig) ist. Auf den ersten Blick sieht es sicherlich fischig aus, aber ich wäre nicht überrascht, wenn es für ein standardkonformes Programm nicht möglich wäre, zu sagen, dass die Last von Daten gehisst wurde (was bedeuten würde, dass dieser Code unter dem "Als ob" korrekt ist) "Regel).

Das Programm liest zwei aufeinanderfolgende Werte aus einem atomaren, einen vor dem Ladevorgang und einen nach dem Ladevorgang und gibt den Ladevorgang erneut aus, wenn sie nicht übereinstimmen. Im Prinzip gibt es keinen Grund, warum die beiden atomaren Lesewerte unterschiedliche Werte voneinander haben. Selbst wenn gerade ein atomarer Schreibvorgang aufgetreten ist, kann dieser Thread nicht erkennen, dass er den alten Wert nicht erneut gelesen hat. Der Thread würde dann zurück in die Schleife gehen und schließlich zwei konsistente Werte aus der atomaren, dann zurück, aber da seq0 und seq1 dann verworfen werden, kann das Programm nicht sagen, dass der Wert in seq0 nicht mit dem Wert gelesen entspricht von data. Nun, im Prinzip deutet dies auch darauf hin, dass die gesamte Schleife hätte entfernt werden können und nur die Last von data tatsächlich für die Korrektheit erforderlich ist, aber das Versagen, die Schleife zu beenden, ist nicht notwendigerweise ein Korrektheitsproblem.

Wenn reader() ein pair<int,unsigned> zurückzukehren waren die seq0 enthielt (oder seq1) und die gleiche gehisst Schleife erzeugt wurden, ich denke, es ist wahrscheinlich falscher Code (aber wieder ich bin neu in dieser nicht-sequentiell konsistenten Operationen Argumentation).

+0

Ich bin mir nicht sicher, ob Sie hier richtig liegen. Wenn das, was Sie sagen, wahr ist, reicht diese Kombination von Barrieren nicht aus, um ein Seq-Lock zu implementieren, was im Gegensatz zu dem steht, was HP in diesem Artikel geschrieben hat: http://www.hpl.hp.com/techreports/ 2012/HPL-2012-68.pdf. Außerdem würden Sie immer noch erwarten, dass der Compiler für beide Eingaben den gleichen Code erzeugt (und wahrscheinlich nur die gesamte Schleife, wie es ohne Barrieren geht). – jleahy

+0

@jleahy, ich denke, dass Papier ist nicht ganz auf dem Punkt. Zuerst werden in dem Papier alle gemeinsamen Lese- und Schreibvorgänge an tatsächlichen atomaren Werten durchgeführt (außer in Beispielen, die sie als inkorrekt anzeigen), da gewöhnliche Variablen den Beschränkungen der Datenumgebung unterliegen (keine in Konflikt stehenden Zugriffe), um undefiniertes Verhalten zu vermeiden. Zweitens führen die Schleifen, die sie zum Markieren von Lese- und Schreibvorgängen ausführen, tatsächlich einige nicht-triviale Berechnungen an den Lesesequenzwerten durch, um die Konsistenz zu überprüfen, was in Ihrem Beispiel nicht zutrifft. IME, einer dieser Punkte ist genug, um den Code deutlich von einem Optimierer zu unterscheiden. –

1

Ich glaube nicht, dass Ihre atomic_thread_fence korrekt ist. Das einzige C++ 11-Speichermodell, das mit Ihrem Code arbeitet, wäre seq_cst one. Aber das ist sehr teuer (Sie werden einen vollen Speicherzaun bekommen) für das, was Sie brauchen.

Der ursprüngliche Code funktioniert und ich denke, das ist die beste Performance-Abwägung.

EDIT basierend auf Ihren Updates:

Wenn Sie nach dem formalen Grund suchen, warum der Code mit einem regelmäßigen int ist die Art und Weise nicht funktioniert Sie möchten, ich das sehr Papier glauben Sie zitiert (http://www.hpl.hp.com/techreports/2012/HPL-2012-68.pdf) gibt die Antwort. Sehen Sie sich das Ende von Abschnitt 2 an. Ihr Code hat das gleiche Problem wie der Code in Abbildung 1. Er hat Datenrennen. Mehrere Threads können gleichzeitig Operationen im selben Speicher des regulären Int ausführen. Es ist durch das C++ 11 Speichermodell verboten, dieser Code ist formal kein gültiger C++ Code.

gcc erwartet, dass der Code keine Datenrennen hat, d. H. Ein gültiger C++ - Code ist. Da es keine Rasse gibt und der Code den Int bedingungslos lädt, kann eine Ladung irgendwo im Körper der Funktion ausgegeben werden. Also gcc ist schlau und es emittiert es nur einmal, da es nicht flüchtig ist. Die bedingte Anweisung, die normalerweise mit einer Übernahmesperre einhergeht, spielt eine wichtige Rolle bei dem, was der Compiler tun wird.

Im formalen Slang des Standards sind die atomaren Lasten und die regulären int Lasten nicht sequenziert. Die Einführung beispielsweise einer Bedingung würde einen Sequenzpunkt erzeugen und würde den Compiler dazu zwingen, das reguläre int nach dem Sequenzpunkt (http://msdn.microsoft.com/en-us/library/d45c7a5d.aspx) auszuwerten. Dann würde das C++ Speichermodell den Rest erledigen (d. H. Die Sichtbarkeit durch die CPU sicherstellen, die die Anweisungen ausführt)

Also keine Ihrer Aussagen sind wahr. Sie können definitiv ein Schloss mit C++ 11 bauen, nur nicht mit Datenrennen :-) Normalerweise würde eine Sperre warten, bevor Sie lesen (was offensichtlich ist, was Sie hier vermeiden wollen), also haben Sie nicht diese Art von Probleme.

Beachten Sie, dass Ihr ursprünglicher Seqlock fehlerhaft ist, weil Sie Seq0! = Seq1 nicht einfach überprüfen möchten (Sie könnten mitten in einem Update sein). Das Seqlock-Papier hat den korrekten Zustand.

+0

Nichts davon beantwortet, warum entspannte und nicht atomare Lasten für die Speicherordnung unterschiedlich behandelt werden. – jleahy

+0

Sie sind nicht. Was Sie von std :: atomic erhalten, ist die "Flüchtigkeit" des zugrunde liegenden Typs. Sie würden das gleiche Verhalten in Ihrem 2. Fall erhalten, wenn Sie Ihre 'int Daten' in 'flüchtige int Daten' ändern. Aber das ist mehr ein Trick. Was wirklich im Sinne von C++ 11 ist, ist entweder ein int, aber mit dem richtigen Speichermodell (seq const, sehr teuer, so dass ich es nicht empfehle) oder Daten mit entspannten Lasten zu einem atomaren machen. – Guillaume

+0

Ich bin ziemlich sicher, dass Sie seq const nicht brauchen, das ist ein mfence auf x86 und dieser Code funktioniert gut mit asm volatile und no mfence. (Ich habe es Milliarden von Malen benutzt). Das Papier, das ich in meinem anderen Kommentar erwähnt habe (hpl.hp.com/techreports/2012/HPL-2012-68.pdf), stellt ebenfalls fest, dass dies eine ausreichende Einschränkung ist. – jleahy

Verwandte Themen