Kann die Verwendung von C ++ 11 'Auto' die Leistung verbessern?

229

Ich kann sehen, warum der autoTyp in C ++ 11 die Korrektheit und Wartbarkeit verbessert. Ich habe gelesen, dass es auch die Leistung verbessern kann ( Fast immer automatisch von Herb Sutter), aber ich vermisse eine gute Erklärung.

  • Wie kann autodie Leistung verbessert werden?
  • Kann jemand ein Beispiel geben?
DaBrain
quelle
5
Siehe herbsutter.com/2013/06/13/…, in dem es darum geht, versehentliche implizite Konvertierungen zu vermeiden, z. B. von Gadget zu Widget. Es ist kein häufiges Problem.
Jonathan Wakely
42
Akzeptieren Sie "macht es weniger wahrscheinlich, dass es ungewollt pessimiert" als Leistungsverbesserung?
5gon12eder
1
Leistung der Code-Bereinigung vielleicht nur in der Zukunft
Croll
Wir brauchen eine kurze Antwort: Nein, wenn Sie gut sind. Es kann "noobische" Fehler verhindern. C ++ hat eine Lernkurve, die diejenigen tötet, die es doch nicht schaffen.
Alec Teal

Antworten:

308

autokann die Leistung verbessern, indem stille implizite Konvertierungen vermieden werden . Ein Beispiel, das mich überzeugt, ist das folgende.

std::map<Key, Val> m;
// ...

for (std::pair<Key, Val> const& item : m) {
    // do stuff
}

Sehen Sie den Fehler? Hier denken wir, dass wir jedes Element in der Karte elegant als Konstantenreferenz nehmen und den neuen Bereich-für-Ausdruck verwenden, um unsere Absicht klar zu machen, aber tatsächlich kopieren wir jedes Element. Dies liegt daran std::map<Key, Val>::value_typeist std::pair<const Key, Val>, nicht std::pair<Key, Val>. Wenn wir also (implizit) haben:

std::pair<Key, Val> const& item = *iter;

Anstatt einen Verweis auf ein vorhandenes Objekt zu nehmen und es dabei zu belassen, müssen wir eine Typkonvertierung durchführen. Sie dürfen einen konstanten Verweis auf ein Objekt (oder ein temporäres Objekt) eines anderen Typs erstellen, solange eine implizite Konvertierung verfügbar ist, z.

int const& i = 2.0; // perfectly OK

Die Typkonvertierung ist eine zulässige implizite Konvertierung aus demselben Grund, aus dem Sie a const Keyin a konvertieren können. KeyWir müssen jedoch eine temporäre Konvertierung des neuen Typs erstellen, um dies zu ermöglichen. Somit macht unsere Schleife effektiv:

std::pair<Key, Val> __tmp = *iter;       // construct a temporary of the correct type
std::pair<Key, Val> const& item = __tmp; // then, take a reference to it

(Natürlich gibt es eigentlich kein __tmpObjekt, es dient nur zur Veranschaulichung, in Wirklichkeit ist das unbenannte Temporär nur itemfür seine Lebensdauer gebunden ).

Einfach ändern zu:

for (auto const& item : m) {
    // do stuff
}

Wir haben gerade eine Menge Kopien gespeichert - jetzt stimmt der Typ, auf den verwiesen wird, mit dem Initialisierungstyp überein, sodass keine temporäre oder Konvertierung erforderlich ist. Wir können nur eine direkte Referenz erstellen.

Barry
quelle
18
@Barry Können Sie erklären, warum der Compiler gerne Kopien erstellt, anstatt sich über den Versuch zu beschweren, eine std::pair<const Key, Val> const &als zu behandeln std::pair<Key, Val> const &? Neu in C ++ 11, nicht sicher, wie weitreichend und autospielt dies.
Agop
@ Barry Danke für die Erklärung. Das ist das Stück, das mir gefehlt hat - aus irgendeinem Grund dachte ich, dass man keinen ständigen Bezug zu einem temporären haben könnte. Aber natürlich können Sie - es wird am Ende seines Umfangs einfach aufhören zu existieren.
Agop
@barry Ich verstehe, aber das Problem ist, dass es dann keine Antwort gibt, die alle Gründe für die Verwendung abdeckt auto, die die Leistung steigern. Also schreibe ich es unten in meinen eigenen Worten auf.
Yakk - Adam Nevraumont
37
Ich denke immer noch nicht, dass dies ein Beweis dafür ist, dass " autodie Leistung verbessert" wird. Dies ist nur ein Beispiel dafür, dass " autoProgrammiererfehler vermieden werden, die die Leistung beeinträchtigen". Ich behaupte, dass es einen subtilen, aber wichtigen Unterschied zwischen den beiden gibt. Trotzdem +1.
Leichtigkeitsrennen im Orbit
69

Da autoder Typ des initialisierenden Ausdrucks abgeleitet wird, ist keine Typkonvertierung erforderlich. In Kombination mit Vorlagenalgorithmen bedeutet dies, dass Sie eine direktere Berechnung erhalten können, als wenn Sie selbst einen Typ erstellen würden - insbesondere, wenn Sie mit Ausdrücken arbeiten, deren Typ Sie nicht benennen können!

Ein typisches Beispiel stammt von (ab) mit std::function:

std::function<bool(T, T)> cmp1 = std::bind(f, _2, 10, _1);  // bad
auto cmp2 = std::bind(f, _2, 10, _1);                       // good
auto cmp3 = [](T a, T b){ return f(b, 10, a); };            // also good

std::stable_partition(begin(x), end(x), cmp?);

Mit cmp2und cmp3kann der gesamte Algorithmus den Vergleichsaufruf inline. Wenn Sie jedoch ein std::functionObjekt erstellen, kann der Aufruf nicht nur nicht inline eingefügt werden, sondern Sie müssen auch die polymorphe Suche im typgelöschten Inneren des Funktionsumschlags durchlaufen.

Eine andere Variante dieses Themas ist, dass Sie sagen können:

auto && f = MakeAThing();

Dies ist immer eine Referenz, die an den Wert des Funktionsaufrufausdrucks gebunden ist und niemals zusätzliche Objekte erstellt. Wenn Sie den Typ des zurückgegebenen Werts nicht kennen, müssen Sie möglicherweise ein neues Objekt (möglicherweise als temporäres Objekt) über so etwas wie erstellen T && f = MakeAThing(). (Funktioniert auto &&auch dann, wenn der Rückgabetyp nicht beweglich ist und der Rückgabewert ein Wert ist.)

Kerrek SB
quelle
Dies ist also der Grund für die Verwendung des Typlöschvorgangs auto. Ihre andere Variante lautet "Vermeiden Sie versehentliche Kopien", muss jedoch verschönert werden. Warum gibt autoes Ihnen die Möglichkeit, den Typ dort einfach einzugeben? (Ich denke, die Antwort lautet "Sie verstehen den Typ falsch und er konvertiert stillschweigend") Was macht ihn zu einem weniger gut erklärten Beispiel für Barrys Antwort, nein? Das heißt, es gibt zwei grundlegende Fälle: Auto, um das Löschen von Typen zu vermeiden, und Auto, um unbeabsichtigte Typfehler zu vermeiden, die versehentlich konvertiert werden. Beide haben Laufzeitkosten.
Yakk - Adam Nevraumont
2
"Der Anruf kann nicht nur nicht inline gestellt werden" - warum ist das dann so? Wollen Sie damit sagen , dass grundsätzlich etwas verhindert wird der Ruf nach devirtualized Datenanalyse , wenn die entsprechenden Spezialisierungen fließen std::bind, std::functionund std::stable_partitionalle haben inlined worden? Oder nur, dass in der Praxis kein C ++ - Compiler aggressiv genug inline wird, um das Chaos zu beseitigen?
Steve Jessop
@SteveJessop: Meistens letzteres - nachdem Sie den std::functionKonstruktor durchlaufen haben , wird es sehr komplex sein, den tatsächlichen Aufruf zu durchschauen, insbesondere bei Optimierungen mit kleinen Funktionen (sodass Sie eigentlich keine Devirtualisierung wünschen). Natürlich ist im Prinzip alles so, als ob ...
Kerrek SB
41

Es gibt zwei Kategorien.

autokann das Löschen des Typs vermeiden. Es gibt unbenennbare Typen (wie Lambdas) und fast unbenennbare Typen (wie das Ergebnis von std::bindoder andere Ausdrucksvorlagen-ähnliche Dinge).

Ohne automüssen Sie am Ende die Daten löschen, um sie zu löschen std::function. Das Löschen von Typen hat Kosten.

std::function<void()> task1 = []{std::cout << "hello";};
auto task2 = []{std::cout << " world\n";};

task1Overhead beim Löschen von Typen - eine mögliche Heap-Zuordnung, Schwierigkeiten beim Inlining und Overhead beim Aufrufen virtueller Funktionstabellen. task2hat keine. Lambdas benötigen automatische oder andere Formen des Typabzugs , um ohne Typlöschung zu speichern. andere Typen können so komplex sein, dass sie nur in der Praxis benötigt werden.

Zweitens können Sie Typen falsch verstehen. In einigen Fällen funktioniert der falsche Typ scheinbar perfekt, verursacht jedoch eine Kopie.

Foo const& f = expression();

wird kompiliert, wenn expression()return Bar const&oder Baroder sogar Bar&, woher Fookann konstruiert werden Bar. Ein Temporär Foowird erstellt, dann gebunden fund seine Lebensdauer verlängert, bis es fverschwindet.

Der Programmierer hat möglicherweise beabsichtigt Bar const& fund nicht beabsichtigt, dort eine Kopie zu erstellen, aber eine Kopie wird trotzdem erstellt.

Das häufigste Beispiel ist die Art von *std::map<A,B>::const_iterator, was std::pair<A const, B> const&nicht std::pair<A,B> const&der Fall ist, aber der Fehler ist eine Kategorie von Fehlern, die stillschweigend die Leistung kosten. Sie können ein std::pair<A, B>aus einem konstruieren std::pair<const A, B>. (Der Schlüssel auf einer Karte ist const, da das Bearbeiten eine schlechte Idee ist.)

Sowohl @Barry als auch @KerrekSB haben diese beiden Prinzipien zuerst in ihren Antworten veranschaulicht. Dies ist lediglich ein Versuch, die beiden Themen in einer Antwort hervorzuheben, wobei der Wortlaut eher auf das Problem abzielt als auf Beispiele.

Yakk - Adam Nevraumont
quelle
8

Die vorhandenen drei Antworten geben Beispiele, bei denen die Verwendung von autoHilfen "die Wahrscheinlichkeit einer unbeabsichtigten Pessimierung verringert" und die Leistung "verbessert".

Die Münze hat eine Kehrseite. Die Verwendung automit Objekten mit Operatoren, die das Basisobjekt nicht zurückgeben, kann zu falschem (noch kompilierbarem und ausführbarem) Code führen. In dieser Frage wird beispielsweise gefragt , wie die Verwendung der autoEigenbibliothek zu unterschiedlichen (falschen) Ergebnissen geführt hat, dh in den folgenden Zeilen

const auto    resAuto    = Ha + Vector3(0.,0.,j * 2.567);
const Vector3 resVector3 = Ha + Vector3(0.,0.,j * 2.567);

std::cout << "resAuto = " << resAuto <<std::endl;
std::cout << "resVector3 = " << resVector3 <<std::endl;

führte zu unterschiedlichen Ausgaben. Dies ist zwar hauptsächlich auf die verzögerte Auswertung durch Eigens zurückzuführen, aber dieser Code ist / sollte für den (Bibliotheks-) Benutzer transparent sein.

Während die Leistung hier nicht stark beeinträchtigt wurde, kann die Verwendung autozur Vermeidung einer unbeabsichtigten Pessimierung als vorzeitige Optimierung oder zumindest als falsch eingestuft werden;).

Avi Ginsburg
quelle
1
Die entgegengesetzte Frage wurde hinzugefügt
Leon