Was ist effizienter: Geben Sie einen Wert gegen Referenzübergabe zurück?

76

Ich studiere gerade, wie man effizienten C ++ - Code schreibt, und bei Funktionsaufrufen kommt mir eine Frage in den Sinn. Vergleich dieser Pseudocode-Funktion:

not-void function-name () {
    do-something
    return value;
}
int main () {
    ...
    arg = function-name();
    ...
}

mit dieser ansonsten identischen Pseudocode-Funktion:

void function-name (not-void& arg) {
    do-something
    arg = value;
}
int main () {
    ...
    function-name(arg);
    ...
}

Welche Version ist effizienter und in welcher Hinsicht (Zeit, Speicher usw.)? Wenn es darauf ankommt, wann wäre der erste effizienter und wann wäre der zweite effizienter?

Bearbeiten : Für den Kontext beschränkt sich diese Frage auf Unterschiede zwischen Hardwareplattformen und größtenteils auch auf Software. Gibt es maschinenunabhängige Leistungsunterschiede?

Bearbeiten : Ich sehe nicht, wie dies ein Duplikat ist. Die andere Frage besteht darin, die Übergabe als Referenz (vorheriger Code) mit der Übergabe als Wert (unten) zu vergleichen:

not-void function-name (not-void arg)

Welches ist nicht das gleiche wie meine Frage. Ich konzentriere mich nicht darauf, wie ich ein Argument besser an eine Funktion übergeben kann. Mein Fokus liegt darauf, wie ich ein Ergebnis besser an eine Variable von außerhalb des Bereichs weitergeben kann.

thegreatjedi
quelle
12
Warum probierst du es nicht einfach aus? Vermutlich hängt es von Ihrer Plattform und Ihrem Compiler ab. Mach es millionenfach und profiliere es. Schreiben Sie den Code im Allgemeinen so, wie es am klarsten ist, und sorgen Sie sich nur dann um Optimierungen, wenn Sie die Leistung steigern müssen.
Xaxxon
1
Probieren Sie beide Versionen einige Millionen Mal aus, während Sie die Anrufe zeitlich festlegen. Tun Sie dies sowohl ohne als auch mit aktivierten Optimierungen. In Anbetracht der Rückgabewertoptimierungen und der Kopierentfernung bezweifle ich, dass Sie in beiden Fällen große Unterschiede feststellen.
Einige Programmierer Typ
3
@Pedro: Dank der Kopierelision und der Verschiebungssemantik gibt es viele Fälle, in denen das Übergeben / Zurückgeben nach Wert tatsächlich effizienter ist.
MikeMB
7
Ihre Aufgabe besteht darin, Code zu schreiben, und Sie haben gerade das Profilieren gelernt? Lernen Sie, wie man ein Profil erstellt. Das wird Ihnen viel mehr als alles andere in dieser Frage helfen. Und wenn Sie Hardware verwenden, die eingeschränkt ist, wird ohne hier spezifische Informationen für dieses Gerät nichts als wahr bekannt sein.
Xaxxon

Antworten:

27

Berücksichtigen Sie zunächst, dass die Rückgabe eines Objekts immer lesbarer (und in der Leistung sehr ähnlich) ist als die Übergabe als Referenz. Daher könnte es für Ihr Projekt interessanter sein, das Objekt zurückzugeben und die Lesbarkeit zu verbessern, ohne wichtige Leistungsunterschiede zu haben . Wenn Sie wissen möchten, wie Sie die niedrigsten Kosten erzielen können, müssen Sie Folgendes zurückgeben:

  1. Wenn Sie ein einfaches oder einfaches Objekt zurückgeben müssen, ist die Leistung in beiden Fällen ähnlich.

  2. Wenn das Objekt so groß und komplex ist, würde für die Rückgabe eine Kopie benötigt, und es könnte langsamer sein, als es als referenzierten Parameter zu haben, aber ich denke, es würde weniger Speicherplatz verbrauchen.

Man muss sowieso denken, dass Compiler viele Optimierungen vornehmen, die beide Leistungen sehr ähnlich machen. Siehe Kopieren von Elision .

Arodriguezdonaire
quelle
2
Was ist mit Copy Elision?
Juanchopanza
8
Unter x86 - und ohne Compiler-Optimierungen - würden beide denselben Assembly-Code erstellen, da Rückgabewerte größer als 1 oder 2 sind. Register werden über einen Speicherbereich übergeben, der vom Aufrufer zugewiesen und über einen impliziten Zeigerparameter an den Angerufenen übergeben wird.
MikeMB
9

Nun, man muss verstehen, dass das Zusammenstellen kein einfaches Geschäft ist. Es werden viele Überlegungen angestellt, wenn der Compiler Ihren Code kompiliert.

Man kann diese Frage nicht einfach beantworten, da der C ++ - Standard kein Standard-ABI (Abstract Binary Interface) bietet, sodass jeder Compiler den Code nach Belieben kompilieren kann und Sie bei jeder Kompilierung unterschiedliche Ergebnisse erzielen können.

Beispielsweise wird in einigen Projekten C ++ als verwaltete Erweiterung von Microsoft CLR (C ++ / CX) kompiliert. Da alles bereits einen Verweis auf ein Objekt auf dem Heap enthält, gibt es wohl keinen Unterschied.

Die Antwort ist für nicht verwaltete Kompilierungen nicht einfacher. Einige Fragen kommen mir in den Sinn, wenn ich an "Wird XXX schneller laufen als JJJ?" denke, zum Beispiel:

  • Ist Ihr Objekt taub-konstruierbar?
  • Unterstützt Ihr Compiler die Rückgabewertoptimierung?
  • Unterstützt Ihr Objekt die Nur-Kopieren-Semantik oder sowohl Kopieren als auch Verschieben?
  • Ist das Objekt zusammenhängend gepackt (z. B. std::array) oder hat es einen Zeiger auf etwas auf dem Haufen? (zB std::vector)?

Wenn ich ein konkretes Beispiel gebe, gehe ich davon aus, dass bei MSVC ++ und GCC die Rückgabe std::vectornach Wert aufgrund der R-Wert-Optimierung als Referenzübergabe erfolgt und etwas (um einige Nanosekunden) schneller ist als die Rückgabe des Vektors durch bewegen. Dies kann beispielsweise bei Clang völlig anders sein.

Letztendlich ist die Profilerstellung hier die einzig wahre Antwort.

David Haim
quelle
8

Zurückführen des Objekts sollte wegen eines optimsation genannt in den meisten Fällen verwendet werden Kopie elision .

Abhängig davon, wie Ihre Funktion verwendet werden soll, ist es möglicherweise besser, das Objekt als Referenz zu übergeben.

Schauen Sie sich std::getlinezum Beispiel an, welches als std::stringReferenz dient. Diese Funktion soll als Schleifenbedingung verwendet werden und füllt a so lange, std::stringbis EOF erreicht ist. Durch die Verwendung derselben std::stringkann der Speicherplatz des std::stringin jeder Schleifeniteration wiederverwendet werden, wodurch die Anzahl der durchzuführenden Speicherzuweisungen drastisch reduziert wird.

Einfach
quelle
5

Einige der Antworten haben dies berührt, aber ich möchte im Lichte der Bearbeitung hervorheben

Für den Kontext beschränkt sich diese Frage auf Hardwareplattform-unabhängige Unterschiede und größtenteils auch auf Software. Gibt es maschinenunabhängige Leistungsunterschiede?

Wenn dies die Grenzen der Frage sind, lautet die Antwort, dass es keine Antwort gibt. Die c ++ - Spezifikation legt nicht fest, wie entweder die Rückgabe eines Objekts oder die Übergabe einer Referenz leistungsmäßig implementiert wird, sondern nur die Semantik dessen, was beide in Bezug auf Code tun.

Es steht einem Compiler daher frei, einen auf identischen Code wie den anderen zu optimieren, vorausgesetzt, dies erzeugt keinen wahrnehmbaren Unterschied zum Programmierer.

In Anbetracht dessen denke ich, dass es am besten ist, das zu verwenden, was für die Situation am intuitivsten ist. Wenn die Funktion tatsächlich ein Objekt als Ergebnis einer Aufgabe oder Abfrage "zurückgibt", geben Sie es zurück, während Sie, wenn die Funktion eine Operation für ein Objekt ausführt, das dem externen Code gehört, als Referenz übergeben.

Sie können die Leistung hierfür nicht verallgemeinern. Machen Sie zunächst alles Intuitive und sehen Sie, wie gut Ihr Zielsystem und Ihr Compiler es optimieren. Wenn Sie nach der Profilerstellung ein Problem feststellen, ändern Sie es bei Bedarf.

Vality
quelle
4

Wir können nicht 100% allgemein sein, da verschiedene Plattformen unterschiedliche ABIs haben, aber ich denke, wir können einige ziemlich allgemeine Aussagen treffen, die für die meisten Implementierungen gelten, mit der Einschränkung, dass diese Dinge hauptsächlich für Funktionen gelten, die nicht inline sind.

Betrachten wir zunächst primitive Typen. Auf einer niedrigen Ebene wird eine Parameterübergabe als Referenz unter Verwendung eines Zeigers implementiert, während primitive Rückgabewerte typischerweise buchstäblich in Registern übergeben werden. Rückgabewerte erzielen daher wahrscheinlich eine bessere Leistung. Bei einigen Architekturen gilt das Gleiche für kleine Strukturen. Das Kopieren eines Wertes, der klein genug ist, um in ein oder zwei Register zu passen, ist sehr billig.

Betrachten wir nun größere, aber immer noch einfache (keine Standardkonstruktoren, Kopierkonstruktoren usw.) Rückgabewerte. In der Regel werden größere Rückgabewerte behandelt, indem der Funktion ein Zeiger auf die Stelle übergeben wird, an der der Rückgabewert platziert werden soll. Mit Copy Elision können die von der Funktion zurückgegebene Variable, die für die Rückgabe verwendete temporäre Variable und die Variable im Aufrufer, in die das Ergebnis gestellt wird, zu einer zusammengeführt werden. Die Grundlagen der Übergabe wären also für die Übergabe als Referenz und den Rückgabewert ähnlich.

Insgesamt würde ich für primitive Typen erwarten, dass die Rückgabewerte geringfügig besser sind, und für größere, aber immer noch einfache Typen würde ich erwarten, dass sie gleich oder besser sind, es sei denn, Ihr Compiler ist bei der Kopierelision sehr schlecht.

Bei Typen, die Standardkonstruktoren, Kopierkonstruktoren usw. verwenden, werden die Dinge komplexer. Wenn die Funktion mehrmals aufgerufen wird, erzwingen Rückgabewerte, dass das Objekt jedes Mal neu erstellt wird, während Referenzparameter möglicherweise die Wiederverwendung der Datenstruktur ohne Rekonstruktion ermöglichen. Andererseits erzwingen Referenzparameter eine (möglicherweise unnötige) Konstruktion, bevor die Funktion aufgerufen wird.

Plugwash
quelle
2

Diese Pseudocode-Funktion:

not-void function-name () {
    do-something
    return value;
}

wäre besser zu verwenden, wenn der zurückgegebene Wert keine weiteren Änderungen erfordert. Der übergebene Parameter wird nur in der geändert function-name. Es sind keine weiteren Verweise darauf erforderlich.


ansonsten identische Pseudocode-Funktion:

void function-name (not-void& arg) {
    do-something
    arg = value;
}

Dies wäre nützlich, wenn wir eine andere Methode haben, die den Wert derselben Variablen wie moderiert, und die Änderungen, die durch einen der Aufrufe an der Variablen vorgenommen wurden, beibehalten müssen.

void another-function-name (not-void& arg) {
    do-something
    arg = value;
}
Naman
quelle
1

In Bezug auf die Leistung sind Kopien im Allgemeinen teurer, obwohl der Unterschied für kleine Objekte vernachlässigbar sein kann. Außerdem kann Ihr Compiler eine Rückgabekopie für eine Verschiebung optimieren, was der Übergabe einer Referenz entspricht.

Ich würde empfehlen, keine Nichtreferenzen weiterzugeben, es constsei denn, Sie haben einen guten Grund dazu. Verwenden Sie den Rückgabewert (zB Funktionen der tryGet()Sortierung).

Wenn Sie möchten, können Sie den Unterschied selbst messen, wie andere bereits gesagt haben. Führen Sie den Testcode für beide Versionen einige Millionen Mal aus und sehen Sie den Unterschied.

elnigno
quelle
Ich möchte darauf hinweisen, dass Nichtreferenzen const aufgrund des impliziten Status einer Referenz möglicherweise zu weiteren Problemen führen. Wenn Sie eine Referenz ändern, müssen wir sicherstellen, dass die Zustände anderer Argumente auch nicht durch die Referenz geändert werden.
CinchBlue
Zusätzlich zu den Aussagen von Vermillion optimiert der Compiler eine Rückgabekopie nicht für eine Verschiebung: Die Rückgabe wird als Verschiebung definiert (Kopie ist Fallback). Es könnte jedoch die Bewegung vollständig ausschließen.
MikeMB