Kann modernes C ++ Ihnen Leistung kostenlos bringen?

205

Es wird manchmal behauptet, dass C ++ 11/14 Ihnen eine Leistungssteigerung bringen kann, selbst wenn Sie lediglich C ++ 98-Code kompilieren. Die Begründung erfolgt normalerweise in Anlehnung an die Bewegungssemantik, da in einigen Fällen die rvalue-Konstruktoren automatisch generiert werden oder jetzt Teil der STL sind. Jetzt frage ich mich, ob diese Fälle zuvor tatsächlich bereits von RVO oder ähnlichen Compiler-Optimierungen behandelt wurden.

Meine Frage ist dann, ob Sie mir ein aktuelles Beispiel für einen Teil des C ++ 98-Codes geben können, der ohne Änderung mit einem Compiler, der die neuen Sprachfunktionen unterstützt, schneller ausgeführt wird. Ich verstehe, dass ein standardkonformer Compiler nicht erforderlich ist, um die Kopierelision durchzuführen, und nur aus diesem Grund kann die Verschiebungssemantik zu einer Geschwindigkeit führen, aber ich würde gerne einen weniger pathologischen Fall sehen, wenn Sie so wollen.

EDIT: Um ganz klar zu sein, ich frage nicht, ob neue Compiler schneller sind als alte Compiler, sondern ob es Code gibt, durch den das Hinzufügen von -std = c ++ 14 zu meinen Compiler-Flags schneller ausgeführt wird (vermeiden Sie Kopien, aber wenn Sie kann mir außer der Bewegungssemantik noch etwas anderes einfallen lassen, das würde mich auch interessieren)

ein großer
quelle
3
Denken Sie daran, dass beim Erstellen eines neuen Objekts mit einem Kopierkonstruktor eine Optimierung der Kopierelision und des Rückgabewerts durchgeführt wird. In einem Kopierzuweisungsoperator gibt es jedoch keine Kopierelision (wie kann es sein, da der Compiler nicht weiß, was mit einem bereits erstellten Objekt zu tun ist, das kein temporäres Objekt ist). In diesem Fall gewinnt C ++ 11/14 daher stark, da Sie die Möglichkeit haben, einen Verschiebungszuweisungsoperator zu verwenden. Zu Ihrer Frage denke ich jedoch nicht, dass C ++ 98-Code schneller sein sollte, wenn er von einem C ++ 11/14-Compiler kompiliert wird. Vielleicht ist er schneller, weil der Compiler neuer ist.
vsoftco
27
Auch Code, der die Standardbibliothek verwendet, ist möglicherweise schneller, selbst wenn Sie sie vollständig mit C ++ 98 kompatibel machen, da die zugrunde liegende Bibliothek in C ++ 11/14 nach Möglichkeit die interne Verschiebungssemantik verwendet. Code, der in C ++ 98 und C ++ 11/14 identisch aussieht, ist im letzteren Fall (möglicherweise) schneller, wenn Sie die Standardbibliotheksobjekte wie Vektoren, Listen usw. verwenden und die Verschiebungssemantik einen Unterschied macht.
vsoftco
1
@vsoftco, das ist die Art von Situation, auf die ich anspielte, aber ich konnte kein Beispiel finden: Soweit ich mich erinnere, wird der Verschiebungskonstruktor nicht automatisch generiert, wenn ich den Kopierkonstruktor definieren muss, was uns zurücklässt Sehr einfache Klassen, in denen RVO meiner Meinung nach immer funktioniert. Eine Ausnahme könnte etwas in Verbindung mit den STL-Containern sein, bei denen die rvalue-Konstruktoren vom Bibliotheksimplementierer generiert werden (was bedeutet, dass ich nichts im Code ändern muss, damit es Verschiebungen verwendet).
am
Klassen müssen nicht einfach sein, um keinen Kopierkonstruktor zu haben. C ++ lebt von der Wertesemantik, und Kopierkonstruktor, Zuweisungsoperator, Destruktor usw. sollten die Ausnahme sein.
SP2danny
1
@ Eric Danke für den Link, es war interessant. Nachdem man es jedoch schnell durchgesehen hat, scheinen die Geschwindigkeitsvorteile hauptsächlich darin zu liegen std::move, Konstruktoren hinzuzufügen und zu verschieben (was Änderungen am vorhandenen Code erfordern würde). Das einzige, was wirklich mit meiner Frage zu tun hatte, war der Satz "Sie erhalten sofortige Geschwindigkeitsvorteile, indem Sie ihn einfach neu kompilieren", der nicht durch Beispiele untermauert wird (STL wird auf derselben Folie erwähnt, wie ich es in meiner Frage getan habe, aber nichts Spezifisches ). Ich habe nach einigen Beispielen gefragt. Wenn ich die Folien falsch lese, lassen Sie es mich wissen.
Alarge

Antworten:

221

Mir sind 5 allgemeine Kategorien bekannt, in denen das Neukompilieren eines C ++ 03-Compilers als C ++ 11 zu unbegrenzten Leistungssteigerungen führen kann, die praktisch nichts mit der Qualität der Implementierung zu tun haben. Dies sind alles Variationen der Bewegungssemantik.

std::vector neu zuweisen

struct bar{
  std::vector<int> data;
};
std::vector<bar> foo(1);
foo.back().data.push_back(3);
foo.reserve(10); // two allocations and a delete occur in C++03

Jedes Mal foo, wenn der Puffer in C ++ 03 neu zugewiesen wird, wird jeder vectorin kopiert bar.

In C ++ 11 wird stattdessen das bar::datas verschoben, was grundsätzlich kostenlos ist.

In diesem Fall hängt dies von Optimierungen im stdContainer ab vector. In allen folgenden Fällen werden stdContainer nur verwendet, weil es sich um C ++ - Objekte handelt, die movein C ++ 11 "automatisch" eine effiziente Semantik aufweisen, wenn Sie Ihren Compiler aktualisieren. Objekte, die es nicht blockieren und einen stdContainer enthalten, erben auch die automatisch verbesserten moveKonstruktoren.

NRVO-Fehler

Wenn NRVO (Named Return Value Optimization) fehlschlägt, wird in C ++ 03 beim Kopieren zurückgegriffen, in C ++ 11 beim Verschieben. Ausfälle von NRVO sind einfach:

std::vector<int> foo(int count){
  std::vector<int> v; // oops
  if (count<=0) return std::vector<int>();
  v.reserve(count);
  for(int i=0;i<count;++i)
    v.push_back(i);
  return v;
}

oder auch:

std::vector<int> foo(bool which) {
  std::vector<int> a, b;
  // do work, filling a and b, using the other for calculations
  if (which)
    return a;
  else
    return b;
}

Wir haben drei Werte - den Rückgabewert und zwei verschiedene Werte innerhalb der Funktion. Mit Elision können die Werte innerhalb der Funktion mit dem Rückgabewert 'zusammengeführt' werden, jedoch nicht miteinander. Sie können beide nicht mit dem Rückgabewert zusammengeführt werden, ohne miteinander zusammengeführt zu werden.

Das Grundproblem besteht darin, dass die NRVO-Elision fragil ist und Code mit Änderungen, die sich nicht in der Nähe des returnStandorts befinden, an dieser Stelle plötzlich massive Leistungseinbußen aufweisen kann, ohne dass eine Diagnose ausgegeben wird. In den meisten NRVO-Fehlerfällen endet C ++ 11 mit a move, während C ++ 03 mit einer Kopie endet.

Rückgabe eines Funktionsarguments

Elision ist auch hier nicht möglich:

std::set<int> func(std::set<int> in){
  return in;
}

In C ++ 11 ist dies billig: In C ++ 03 gibt es keine Möglichkeit, die Kopie zu umgehen. Argumente für Funktionen können nicht mit dem Rückgabewert entfernt werden, da die Lebensdauer und Position des Parameters und des Rückgabewerts vom aufrufenden Code verwaltet werden.

C ++ 11 kann jedoch von einem zum anderen verschoben werden. (In einem weniger spielerischen Beispiel könnte etwas mit dem gemacht werden set).

push_back oder insert

Schließlich findet keine Elision in Container statt: C ++ 11 überlastet jedoch rvalue move insert-Operatoren, wodurch Kopien gespeichert werden.

struct whatever {
  std::string data;
  int count;
  whatever( std::string d, int c ):data(d), count(c) {}
};
std::vector<whatever> v;
v.push_back( whatever("some long string goes here", 3) );

In C ++ 03 wird ein Temporär whatevererstellt und dann in den Vektor kopiert v. Es werden 2 std::stringPuffer mit jeweils identischen Daten zugewiesen und einer verworfen.

In C ++ 11 wird eine temporäre whateverDatei erstellt. Die whatever&& push_backÜberlastung ist dann moveso temporär in den Vektor v. Ein std::stringPuffer wird zugewiesen und in den Vektor verschoben. Ein Leerzeichen std::stringwird verworfen.

Zuordnung

Gestohlen aus der Antwort von @ Jarod42 unten.

Elision kann nicht mit Zuweisung erfolgen, sondern mit Abzug.

std::set<int> some_function();

std::set<int> some_value;

// code

some_value = some_function();

Hier wird some_functionein Kandidat zurückgegeben, von dem entfernt werden soll. Da er jedoch nicht zum direkten Erstellen eines Objekts verwendet wird, kann er nicht entfernt werden. In C ++ 03 führt das Obige dazu, dass der Inhalt des Temporären kopiert wird some_value. In C ++ 11 wird es verschoben some_value, was grundsätzlich kostenlos ist.


Für die volle Wirkung des oben genannten benötigen Sie einen Compiler, der Verschiebungskonstruktoren und Zuweisungen für Sie synthetisiert.

MSVC 2013 implementiert Verschiebungskonstruktoren in stdContainern, synthetisiert jedoch keine Verschiebungskonstruktoren für Ihre Typen.

Typen, die std::vectors und ähnliches enthalten, erhalten solche Verbesserungen in MSVC2013 nicht, werden sie jedoch in MSVC2015 erhalten.

clang und gcc haben längst implizite Verschiebungskonstruktoren implementiert. Der 2013er Compiler von Intel unterstützt die implizite Generierung von Verschiebungskonstruktoren, wenn Sie übergeben -Qoption,cpp,--gen_move_operations(sie tun dies standardmäßig nicht, um mit MSVC2013 kompatibel zu sein).

Yakk - Adam Nevraumont
quelle
1
@alarge ja. Damit ein Verschiebungskonstruktor jedoch um ein Vielfaches effizienter ist als ein Kopierkonstruktor, muss er normalerweise Ressourcen verschieben, anstatt sie zu kopieren. Ohne eigene Verschiebungskonstruktoren zu schreiben (und nur ein C ++ 03-Programm neu zu kompilieren), stdwerden alle Bibliothekscontainer move"kostenlos" mit Konstruktoren aktualisiert und (wenn Sie sie nicht blockiert haben) Konstrukte, die diese Objekte verwenden ( und die genannten Objekte) werden in einer Reihe von Situationen mit dem Bau von freien Bewegungen beginnen. Viele dieser Situationen werden in C ++ 03 durch Elision abgedeckt: nicht alle.
Yakk - Adam Nevraumont
5
Das ist eine schlechte Optimierungsimplementierung. Da die zurückgegebenen Objekte mit unterschiedlichen Namen keine überlappende Lebensdauer haben, ist RVO theoretisch immer noch möglich.
Ben Voigt
2
@alarge Es gibt Orte, an denen die Elision fehlschlägt, z. B. wenn zwei Objekte mit überlappenden Lebensdauern in ein Drittel zerlegt werden können, aber nicht ineinander. Dann ist in C ++ 11 ein Verschieben und in C ++ 03 ein Kopieren erforderlich (als ob ignoriert). Elision ist in der Praxis oft fragil. Die Verwendung der stdoben genannten Container ist hauptsächlich darauf zurückzuführen, dass sie billig zu verschieben sind, um den Typ zu kopieren, den Sie in C ++ 11 beim Neukompilieren von C ++ 03 "kostenlos" erhalten. Das vector::resizeist eine Ausnahme: Es wird movein C ++ 11 verwendet.
Yakk - Adam Nevraumont
27
Ich sehe nur 1 allgemeine Kategorie, die Bewegungssemantik ist, und 5 Sonderfälle davon.
Johannes Schaub - litb
3
@sebro Ich verstehe, Sie halten es nicht für ausreichend, dass "Programme nicht viele 1000 von vielen Kilobyte-Zuweisungen zuweisen und stattdessen Zeiger verschieben". Sie möchten zeitgesteuerte Ergebnisse. Mikrobenchmarks sind kein Beweis für Leistungsverbesserungen mehr als der Beweis, dass Sie grundsätzlich weniger tun. Abgesehen von einigen 100 realen Anwendungen in einer Vielzahl von Branchen, die mit realen Aufgabenprofilen profiliert werden, ist dies kein wirklicher Beweis. Ich habe vage Behauptungen über "freie Leistung" aufgestellt und ihnen spezifische Fakten über Unterschiede im Programmverhalten unter C ++ 03 und C ++ 11 gemacht.
Yakk - Adam Nevraumont
46

wenn du so etwas hast wie:

std::vector<int> foo(); // function declaration.
std::vector<int> v;

// some code

v = foo();

Sie haben eine Kopie in C ++ 03 erhalten, während Sie in C ++ 11 eine Verschiebungszuweisung erhalten haben. In diesem Fall haben Sie also eine kostenlose Optimierung.

Jarod42
quelle
4
@Yakk: Wie erfolgt die Kopierentfernung bei der Zuweisung?
Jarod42
2
@ Jarod42 Ich glaube auch, dass eine Kopierelision in einer Zuweisung nicht möglich ist, da die linke Seite bereits erstellt ist und es für einen Compiler keine vernünftige Möglichkeit gibt, zu wissen, was mit den "alten" Daten zu tun ist, nachdem er die Ressourcen von rechts gestohlen hat Handseite. Aber vielleicht irre ich mich, ich würde gerne einmal und für immer die Antwort herausfinden. Die Kopierelision ist beim Kopieren von Konstrukten sinnvoll, da das Objekt "frisch" ist und es kein Problem gibt, zu entscheiden, was mit den alten Daten geschehen soll. Soweit ich weiß, ist die einzige Ausnahme folgende: "Zuweisungen können nur auf der Grundlage der Als-ob-Regel
beseitigt werden
4
Guter C ++ 03 Code hat in diesem Fall bereits einen Zug gemacht, viafoo().swap(v);
Ben Voigt
@BenVoigt sicher, aber nicht der gesamte Code ist optimiert, und nicht alle Stellen, an denen dies geschieht, sind leicht zu erreichen.
Yakk - Adam Nevraumont
Die Kopierellision kann in einer Aufgabe funktionieren, wie @BenVoigt sagt. Besserer Begriff ist RVO (Return Value Optimization) und funktioniert nur, wenn foo () so implementiert wurde.
DrumM