Warum sollte ich std :: move std :: shared_ptr verschieben?

147

Ich habe den Clang-Quellcode durchgesehen und diesen Ausschnitt gefunden:

void CompilerInstance::setInvocation(
    std::shared_ptr<CompilerInvocation> Value) {
  Invocation = std::move(Value);
}

Warum sollte ich will std::moveein std::shared_ptr?

Gibt es einen Grund, das Eigentum an einer gemeinsam genutzten Ressource zu übertragen?

Warum sollte ich das nicht einfach stattdessen tun?

void CompilerInstance::setInvocation(
    std::shared_ptr<CompilerInvocation> Value) {
  Invocation = Value;
}
sdgfsdh
quelle

Antworten:

135

Ich denke, dass das eine, was die anderen Antworten nicht genug betonten, der Punkt der Geschwindigkeit ist .

std::shared_ptrReferenzzahl ist atomar . Das Erhöhen oder Verringern des Referenzzählers erfordert ein atomares Inkrementieren oder Dekrementieren . Dies ist hundertmal langsamer als nicht-atomares Inkrementieren / Dekrementieren, ganz zu schweigen davon, dass wir, wenn wir denselben Zähler inkrementieren und dekrementieren, die genaue Zahl erhalten und dabei eine Menge Zeit und Ressourcen verschwenden.

Indem shared_ptrwir das verschieben, anstatt es zu kopieren, "stehlen" wir den atomaren Referenzzähler und heben den anderen auf shared_ptr. Das "Stehlen" des Referenzzählers ist nicht atomar und es ist hundertmal schneller als das Kopieren des shared_ptr(und verursacht ein Inkrementieren oder Dekrementieren der atomaren Referenz).

Beachten Sie, dass diese Technik nur zur Optimierung verwendet wird. Das Kopieren (wie Sie vorgeschlagen haben) ist in Bezug auf die Funktionalität genauso gut.

David Haim
quelle
5
Ist es wirklich hundertmal schneller? Haben Sie Benchmarks dafür?
Xaviersjs
1
@xaviersjs Die Zuweisung erfordert ein atomares Inkrement, gefolgt von einem atomaren Dekrement, wenn der Wert außerhalb des Gültigkeitsbereichs liegt. Atomoperationen können Hunderte von Taktzyklen dauern. Also ja, es ist wirklich so viel langsamer.
Adisak
2
@Adisak, das ist das erste Mal, dass ich die Operation zum Abrufen und Hinzufügen ( en.wikipedia.org/wiki/Fetch-and-add ) gehört habe, die Hunderte von Zyklen länger dauern kann als ein grundlegendes Inkrement. Haben Sie eine Referenz dafür?
Xaviersjs
2
@xaviersjs: stackoverflow.com/a/16132551/4238087 Da die Registeroperationen nur wenige Zyklen umfassen, sind 100 (100-300) Zyklen für Atomic genau das Richtige. Obwohl die Messdaten aus dem Jahr 2013 stammen, scheint dies insbesondere für NUMA-Systeme mit mehreren Sockets immer noch zu gelten.
russianfool
1
Manchmal denkst du, dein Code enthält kein Threading ... aber dann kommt eine verdammte Bibliothek und ruiniert sie für dich. Verwenden Sie besser const-Referenzen und std :: move ... wenn klar und offensichtlich ist, dass Sie ... sich auf Zeigerreferenzzählungen verlassen können.
Erik Aronesty
122

Durch die Verwendung movevermeiden Sie, die Anzahl der Freigaben zu erhöhen und dann sofort zu verringern. Das könnte Ihnen einige teure atomare Operationen bei der Anzahl der Nutzungen ersparen.

Bo Persson
quelle
1
Ist es nicht vorzeitige Optimierung?
YSC
11
@YSC nicht, wenn jemand, der es dort abgelegt hat, es tatsächlich getestet hat.
OrangeDog
19
@YSC Vorzeitige Optimierung ist böse, wenn sie das Lesen oder Verwalten von Code erschwert. Dieser tut beides nicht, zumindest IMO.
Angew ist nicht mehr stolz auf SO
17
Tatsächlich. Dies ist keine vorzeitige Optimierung. Es ist stattdessen die sinnvolle Art, diese Funktion zu schreiben.
Leichtigkeitsrennen im Orbit
60

Verschiebungsoperationen (wie der Verschiebungskonstruktor) für std::shared_ptrsind billig , da sie im Grunde genommen "Zeiger stehlen" (von Quelle zu Ziel; genauer gesagt, der gesamte Statussteuerblock wird von Quelle zu Ziel "gestohlen", einschließlich der Referenzzählinformationen). .

Stattdessen erhöhen sich die Kopiervorgänge beim std::shared_ptrAufrufen der Anzahl der Atomreferenzen (dh nicht nur ++RefCountbei einem ganzzahligen RefCountDatenelement, sondern z. B. beim Aufrufen InterlockedIncrementvon Windows), was teurer ist als nur das Stehlen von Zeigern / Status.

Analyse der Ref-Count-Dynamik dieses Falls im Detail:

// shared_ptr<CompilerInvocation> sp;
compilerInstance.setInvocation(sp);

Wenn Sie einen spWert übergeben und dann eine Kopie innerhalb der CompilerInstance::setInvocationMethode erstellen, haben Sie:

  1. Bei der Eingabe der Methode wird der shared_ptrParameter kopierkonstruiert: ref count atomares Inkrement .
  2. Innerhalb des Hauptteils der Methode kopieren Sie den shared_ptrParameter in das Datenelement: ref count atomares Inkrement .
  3. Beim Beenden der Methode wird der shared_ptrParameter zerstört: ref count atomares Dekrement .

Sie haben zwei atomare Inkremente und ein atomares Dekrement für insgesamt drei atomare Operationen.

Wenn Sie stattdessen den shared_ptrParameter als Wert und dann std::moveinnerhalb der Methode übergeben (wie in Clangs Code korrekt ausgeführt), haben Sie:

  1. Bei der Eingabe der Methode wird der shared_ptrParameter kopierkonstruiert: ref count atomares Inkrement .
  2. Innerhalb des Methodenkörpers geben Sie std::moveden shared_ptrParameter in das Datenelement ein: ref count ändert sich nicht ! Sie stehlen nur Zeiger / Status: Es sind keine teuren atomaren Ref-Count-Operationen erforderlich.
  3. Beim Beenden der Methode wird der shared_ptrParameter zerstört. Aber seit Sie in Schritt 2 umgezogen sind, gibt es nichts zu zerstören, da der shared_ptrParameter auf nichts mehr zeigt. Auch in diesem Fall findet keine atomare Dekrementierung statt.

Fazit: In diesem Fall erhalten Sie nur ein Atominkrement für die Ref-Anzahl, dh nur eine Atomoperation .
Wie Sie sehen können, ist dies viel besser als zwei atomare Inkremente plus ein atomares Dekrement (für insgesamt drei atomare Operationen) für den Kopierfall.

Mr.C64
quelle
1
Ebenfalls erwähnenswert: Warum gehen sie nicht einfach an der Konstantenreferenz vorbei und vermeiden das ganze std :: move-Zeug? Da Sie mit Pass-by-Value auch einen Rohzeiger direkt übergeben können und nur ein shared_ptr erstellt wird.
Joseph Ireland
@ JosephIreland Weil Sie eine konstante Referenz nicht verschieben können
Bruno Ferreira
2
@JosephIreland, denn wenn Sie es so nennen, compilerInstance.setInvocation(std::move(sp));gibt es kein Inkrement . Sie können das gleiche Verhalten erzielen, indem Sie eine Überladung hinzufügen, die eine, shared_ptr<>&&aber warum duplizieren, wenn Sie nicht müssen.
Ratschenfreak
2
@BrunoFerreira Ich beantwortete meine eigene Frage. Sie müssten es nicht verschieben, da es sich um eine Referenz handelt. Kopieren Sie es einfach. Immer noch nur eine Kopie statt zwei. Der Grund, warum sie das nicht tun, ist, dass es unnötig neu erstellte shared_ptrs kopieren würde, z. B. von setInvocation(new CompilerInvocation)oder wie erwähnt setInvocation(std::move(sp)). Es tut mir leid, wenn mein erster Kommentar unklar war. Ich habe ihn tatsächlich versehentlich gepostet, bevor ich mit dem Schreiben fertig war, und ich habe beschlossen, ihn einfach zu verlassen
Joseph Ireland
22

Beim Kopieren von a wird shared_ptrder interne Statusobjektzeiger kopiert und der Referenzzähler geändert. Beim Verschieben werden nur Zeiger auf den internen Referenzzähler und das eigene Objekt ausgetauscht, sodass es schneller geht.

SingerOfTheFall
quelle
16

In dieser Situation gibt es zwei Gründe für die Verwendung von std :: move. Die meisten Antworten befassten sich mit dem Thema Geschwindigkeit, ignorierten jedoch das wichtige Problem, die Absicht des Codes klarer darzustellen.

Bei einem std :: shared_ptr bezeichnet std :: move eindeutig eine Eigentumsübertragung des Pointees, während ein einfacher Kopiervorgang einen zusätzlichen Eigentümer hinzufügt. Wenn der ursprüngliche Eigentümer später sein Eigentum aufgibt (z. B. indem er zulässt, dass sein std :: shared_ptr zerstört wird), wurde natürlich eine Eigentumsübertragung durchgeführt.

Wenn Sie das Eigentum mit std :: move übertragen, ist es offensichtlich, was passiert. Wenn Sie eine normale Kopie verwenden, ist es nicht offensichtlich, dass es sich bei dem beabsichtigten Vorgang um eine Übertragung handelt, bis Sie überprüfen, ob der ursprüngliche Eigentümer das Eigentum sofort aufgibt. Als Bonus ist eine effizientere Implementierung möglich, da durch eine atomare Eigentumsübertragung der vorübergehende Zustand vermieden werden kann, in dem sich die Anzahl der Eigentümer um eins erhöht hat (und die damit verbundenen Änderungen der Referenzzählungen).

Stephen C. Steel
quelle
Genau das, wonach ich suche. Überrascht, wie andere Antworten diesen wichtigen semantischen Unterschied ignorieren. Bei den Smart Pointern dreht sich alles um Eigentum.
Qweruiop