Kann jemand es in einer Sprache erklären, die nur Sterbliche verstehen?
c++
multithreading
memory-model
stdatomic
carries-dependency
Yakov Galka
quelle
quelle
d?a:b
bricht die Abhängigkeit, aberd->static_fun()
nicht ... das macht keinen Sinn. Und es erlaubt keinen "etwas besseren Multithread-Code". Bei einigen Prozessoren ist es erheblich besser, einen Zaun für einen häufigen Betrieb zu vermeiden. " Wenn selten geändert, werden häufig gelesene Daten gemeinsam genutzt " Consume gilt auch für häufig geänderte Daten, sofern ein Zeiger darauf vorhanden ist und der Datensatz nur einmal veröffentlicht wird, was ohnehin die Norm ist.Antworten:
[[carries_dependency]]
wird verwendet, um Abhängigkeiten über Funktionsaufrufe hinweg zu übertragen. Auf diese Weise kann der Compiler möglicherweise besseren Code generieren, wenn erstd::memory_order_consume
zum Übertragen von Werten zwischen Threads auf Plattformen mit schwach geordneten Architekturen wie der POWER-Architektur von IBM verwendet wird.Insbesondere wenn ein mit gelesener Wert
memory_order_consume
an eine Funktion übergeben wird[[carries_dependency]]
, muss der Compiler möglicherweise einen Speicherzaunbefehl ausgeben, um sicherzustellen, dass die entsprechende Semantik der Speicherreihenfolge eingehalten wird. Wenn der Parameter mit kommentiert[[carries_dependency]]
ist, kann der Compiler davon ausgehen, dass der Funktionskörper die Abhängigkeit korrekt trägt, und dieser Zaun ist möglicherweise nicht mehr erforderlich.Wenn eine Funktion einen mit
memory_order_consume
einem solchen Wert geladenen oder von einem solchen Wert abgeleiteten Wert zurückgibt , muss[[carries_dependency]]
der Compiler möglicherweise auch einen Zaunbefehl einfügen, um sicherzustellen, dass die entsprechende Semantik der Speicherreihenfolge eingehalten wird. Mit der[[carries_dependency]]
Annotation ist dieser Zaun möglicherweise nicht mehr erforderlich, da der Aufrufer nun für die Pflege des Abhängigkeitsbaums verantwortlich ist.z.B
void print(int * val) { std::cout<<*val<<std::endl; } void print2(int * [[carries_dependency]] val) { std::cout<<*val<<std::endl; } std::atomic<int*> p; int* local=p.load(std::memory_order_consume); if(local) std::cout<<*local<<std::endl; // 1 if(local) print(local); // 2 if(local) print2(local); // 3
In Zeile (1) ist die Abhängigkeit explizit, sodass der Compiler weiß, dass sie
local
dereferenziert ist, und dass er sicherstellen muss, dass die Abhängigkeitskette erhalten bleibt, um einen Zaun auf POWER zu vermeiden.In Zeile (2), von der Definition
print
ist undurchsichtig (vorausgesetzt , es nicht inlined ist), so dass der Compiler einen Zaun um ausstellen muss , dass das Lesen , um sicherzustellen ,*p
inprint
gibt den richtigen Wert.In Zeile (3) kann der Compiler davon ausgehen, dass
print2
die Abhängigkeit vom Parameter zum dereferenzierten Wert im Befehlsstrom erhalten bleibt , obwohl sie ebenfalls undurchsichtig ist, und dass für POWER kein Zaun erforderlich ist. Offensichtlich muss die Definition vonprint2
diese Abhängigkeit tatsächlich beibehalten, sodass sich das Attribut auch auf den generierten Code für auswirktprint2
.quelle
[[carries_dependency]]
Attribut verwenden und nicht aufrufen, esstd::kill_dependency
sei denn, Sie meinen es ernst. Der Compiler stellt dann sicher, dass die Abhängigkeitskette im generierten Code nicht unterbrochen wird.[[carries_dependency]]
und der Compiler generiert auf magische Weise schnelleren Code. Ich würde mich für eine Beispielfunktion interessieren, die Sie nicht verwenden können[[carries_dependency]]
oder die Sie verwenden müsstenstd::kill_dpendency
.Kurz gesagt, ich denke, wenn es ein Carry_dependency-Attribut gibt, sollte der generierte Code für eine Funktion für einen Fall optimiert werden, in dem das eigentliche Argument wirklich von einem anderen Thread stammt und eine Abhängigkeit trägt. Ähnliches gilt für einen Rückgabewert. Es kann zu Leistungsmängeln kommen, wenn diese Annahme nicht zutrifft (z. B. im Single-Thread-Programm). Aber auch das Fehlen von [[Übertragsabhängigkeit]] kann im umgekehrten Fall zu einer schlechten Leistung führen ... Es sollten keine anderen Effekte als die Leistungsänderung auftreten.
Zum Beispiel hängt die Zeiger-Dereferenzierungsoperation davon ab, wie der Zeiger zuvor erhalten wurde, und wenn der Wert des Zeigers p von einem anderen Thread stammt (durch "Konsum" -Operation), wird der Wert berücksichtigt, den dieser andere Thread zuvor * p zugewiesen hat und sichtbar. Es kann einen anderen Zeiger q geben, der gleich p (q == p) ist, aber da sein Wert nicht von diesem anderen Thread stammt, kann sich der Wert von * q von dem von * p unterscheiden. Tatsächlich kann * q eine Art "undefiniertes Verhalten" hervorrufen (weil der Zugriff auf den Speicherort nicht mit dem anderen Thread übereinstimmt, der die Zuweisung vorgenommen hat).
Wirklich, es scheint, dass es in bestimmten technischen Fällen einen großen Fehler in der Funktionalität des Speichers (und des Geistes) gibt ....> :-)
quelle