Was bedeutet das Attribut [[Übertragsabhängigkeit]]?

74

Kann jemand es in einer Sprache erklären, die nur Sterbliche verstehen?

Yakov Galka
quelle
1
@DumbCoder: Danke, das ist definitiv besser als N2390 selbst, leider leitet es zu vielen anderen Papieren weiter, die "notwendig sind, um diesen Vorschlag zu verstehen" ... Scheint, als wäre meine Frage zu weit gefasst :)
Yakov Galka
1
In der normalen Sprache ist es ein optionaler Optimierungshinweis (der derzeit von jedem Compiler entweder nicht implementiert oder ignoriert wird), der es einem Compiler theoretisch ermöglichen kann, etwas besseren Multithread-Code zu generieren, wenn selten geänderte, häufig gelesene Daten gemeinsam genutzt werden. Gute Arbeit, dass der Wortlaut so verzerrt ist, dass ihn sowieso niemand verwenden wird :-)
Damon
Ich verweise Sie auch auf diese Frage, in der ein schönes Buch von Anthony Williams erwähnt wird: stackoverflow.com/questions/4938258/…
Omnifarious
1
@Damon Es ist nicht so, dass der Wortlaut verzerrt ist, es ist so, dass die Semantik absolut albern ist: d?a:bbricht die Abhängigkeit, aber d->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.
Neugieriger

Antworten:

55

[[carries_dependency]]wird verwendet, um Abhängigkeiten über Funktionsaufrufe hinweg zu übertragen. Auf diese Weise kann der Compiler möglicherweise besseren Code generieren, wenn er std::memory_order_consumezum Ü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_consumean 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_consumeeinem 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 localdereferenziert 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 printist undurchsichtig (vorausgesetzt , es nicht inlined ist), so dass der Compiler einen Zaun um ausstellen muss , dass das Lesen , um sicherzustellen , *pin printgibt den richtigen Wert.

In Zeile (3) kann der Compiler davon ausgehen, dass print2die 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 von print2diese Abhängigkeit tatsächlich beibehalten, sodass sich das Attribut auch auf den generierten Code für auswirkt print2.

Anthony Williams
quelle
17
Dies ist eine großartige Antwort. Aber ... wie würden Sie die Funktion codieren, um die Abhängigkeit zu erhalten? Wie würde eine falsch codierte Funktion aussehen und welche Konsequenzen hätte dies?
Omnifarious
2
Übrigens habe ich eine Vorabversion Ihres Buches als PDF erhalten. Es ist ein fantastisches Buch. Ich wünschte wirklich, Sie hätten Ihre Metapher "Person in einer Kabine, die Anrufe empfängt" bis zum Ende weitergeführt. Das war ein großartiges Werkzeug, um zu verstehen, was los war.
Omnifarious
2
Aus dem POV der Quelle müssen Sie lediglich das [[carries_dependency]]Attribut verwenden und nicht aufrufen, es std::kill_dependencysei denn, Sie meinen es ernst. Der Compiler stellt dann sicher, dass die Abhängigkeitskette im generierten Code nicht unterbrochen wird.
Anthony Williams
8
@AnthonyWilliams: Ich bin hier bei Omnifarious: Es hört sich so an, als müssten Sie nur alle Funktionsdeklarationen mit verputzen, [[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üssten std::kill_dpendency.
Marc Mutz - mmutz
2
@ MarcMutz-mmutz " Der Compiler generiert auf magische Weise schnelleren Code " Falsch. Der Compiler generiert gleichen oder weniger optimierten (langsameren) Code.
Neugieriger
-2

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 ....> :-)

user3221894
quelle