Zeiger gegen Werte in Parametern und Rückgabewerte

326

In Go gibt es verschiedene Möglichkeiten, einen structWert oder einen Slice davon zurückzugeben. Für einzelne habe ich gesehen:

type MyStruct struct {
    Val int
}

func myfunc() MyStruct {
    return MyStruct{Val: 1}
}

func myfunc() *MyStruct {
    return &MyStruct{}
}

func myfunc(s *MyStruct) {
    s.Val = 1
}

Ich verstehe die Unterschiede zwischen diesen. Der erste gibt eine Kopie der Struktur zurück, der zweite einen Zeiger auf den innerhalb der Funktion erstellten Strukturwert, der dritte erwartet, dass eine vorhandene Struktur übergeben wird, und überschreibt den Wert.

Ich habe gesehen, dass all diese Muster in verschiedenen Kontexten verwendet werden. Ich frage mich, welche Best Practices diesbezüglich gelten. Wann würden Sie welche verwenden? Zum Beispiel könnte die erste für kleine Strukturen in Ordnung sein (weil der Overhead minimal ist), die zweite für größere. Und die dritte, wenn Sie extrem speichereffizient sein möchten, da Sie eine einzelne Strukturinstanz zwischen Aufrufen problemlos wiederverwenden können. Gibt es Best Practices für die Verwendung von welchen?

Ebenso die gleiche Frage bezüglich Scheiben:

func myfunc() []MyStruct {
    return []MyStruct{ MyStruct{Val: 1} }
}

func myfunc() []*MyStruct {
    return []MyStruct{ &MyStruct{Val: 1} }
}

func myfunc(s *[]MyStruct) {
    *s = []MyStruct{ MyStruct{Val: 1} }
}

func myfunc(s *[]*MyStruct) {
    *s = []MyStruct{ &MyStruct{Val: 1} }
}

Nochmals: Was sind hier Best Practices? Ich weiß, dass Slices immer Zeiger sind, daher ist es nicht sinnvoll, einen Zeiger auf ein Slice zurückzugeben. Sollte ich jedoch ein Slice mit Strukturwerten zurückgeben, ein Slice mit Zeigern auf Strukturen, sollte ich einen Zeiger auf ein Slice als Argument übergeben (ein Muster, das in der Go App Engine-API verwendet wird )?

Zef Hemel
quelle
1
Wie Sie sagen, kommt es wirklich auf den Anwendungsfall an. Alle sind je nach Situation gültig - ist dies ein veränderliches Objekt? Wollen wir eine Kopie oder einen Zeiger? usw. Übrigens, Sie haben die Verwendung nicht erwähnt new(MyStruct):) Aber es gibt keinen Unterschied zwischen den verschiedenen Methoden zum Zuweisen und Zurückgeben von Zeigern.
Not_a_Golfer
15
Das ist buchstäblich über Engineering. Strukturen müssen ziemlich groß sein, damit die Rückgabe eines Zeigers Ihr Programm beschleunigt. Nur nicht stören, Code, Profil, reparieren, wenn nützlich.
Volker
1
Es gibt nur eine Möglichkeit, einen Wert oder einen Zeiger zurückzugeben, nämlich einen Wert oder einen Zeiger zurückzugeben. Wie Sie sie zuweisen, ist ein separates Problem. Verwenden Sie, was für Ihre Situation funktioniert, und schreiben Sie Code, bevor Sie sich darüber Gedanken machen.
JimB
3
Übrigens, nur aus Neugier habe ich das überprüft. Das Zurückgeben von Strukturen im Vergleich zu Zeigern scheint ungefähr gleich schnell zu sein, aber das Übergeben von Zeigern an Funktionen in den Zeilen ist erheblich schneller. Obwohl nicht auf einem Level wäre es wichtig
Not_a_Golfer
1
@Not_a_Golfer: Ich würde annehmen, dass die BC-Zuweisung nur außerhalb der Funktion erfolgt. Auch das Benchmarking von Werten gegenüber Zeigern hängt von der Größe der Struktur und den Speicherzugriffsmustern nachträglich ab. Das Kopieren von Dingen in Cache-Zeilengröße ist so schnell wie möglich, und die Geschwindigkeit der Dereferenzierung von Zeigern aus dem CPU-Cache unterscheidet sich erheblich von der Dereferenzierung aus dem Hauptspeicher.
JimB

Antworten:

392

tl; dr :

  • Methoden, die Empfängerzeiger verwenden, sind üblich. Die Faustregel für Empfänger lautet : "Verwenden Sie im Zweifelsfall einen Zeiger."
  • Slices, Maps, Kanäle, Strings, Funktionswerte und Schnittstellenwerte werden intern mit Zeigern implementiert, und ein Zeiger darauf ist häufig redundant.
  • Verwenden Sie an anderer Stelle Zeiger für große Strukturen oder Strukturen, die Sie ändern müssen, und übergeben Sie andernfalls Werte , da es verwirrend ist, Dinge über einen Zeiger überraschend zu ändern .

Ein Fall, in dem Sie häufig einen Zeiger verwenden sollten:

  • Empfänger sind häufiger Zeiger als andere Argumente. Es ist nicht ungewöhnlich, dass Methoden das aufgerufene Objekt ändern oder dass benannte Typen große Strukturen sind. Daher wird standardmäßig auf Zeiger verwiesen, außer in seltenen Fällen.
    • Das Copyfighter- Tool von Jeff Hodges sucht automatisch nach nicht winzigen Empfängern, die als Wert übergeben wurden.

Einige Situationen, in denen Sie keine Zeiger benötigen:

  • Richtlinien zur Codeüberprüfung schlagen vor, kleine Strukturen wie type Point struct { latitude, longitude float64 }und möglicherweise sogar etwas größere Werte als Werte zu übergeben, es sei denn, die von Ihnen aufgerufene Funktion muss in der Lage sein, sie an Ort und Stelle zu ändern.

    • Wertesemantik vermeidet Aliasing-Situationen, in denen eine Zuweisung hier einen Wert dort überraschend ändert.
    • Es ist nicht einfach, eine saubere Semantik für ein wenig Geschwindigkeit zu opfern, und manchmal ist es effizienter, kleine Strukturen nach Wert zu übergeben, da dadurch Cache-Fehlschläge oder Heap-Zuweisungen vermieden werden .
    • Auf der Kommentarseite zur Codeüberprüfung von Go Wiki wird daher vorgeschlagen, den Wert zu übergeben, wenn die Strukturen klein sind und dies wahrscheinlich auch bleiben.
    • Wenn der "große" Cutoff vage erscheint, ist es; Möglicherweise befinden sich viele Strukturen in einem Bereich, in dem entweder ein Zeiger oder ein Wert in Ordnung ist. Als Untergrenze legen die Kommentare zur Codeüberprüfung nahe, dass Slices (drei Maschinenwörter) als Wertempfänger sinnvoll sind. Als etwas, das näher an einer Obergrenze liegt, werden bytes.ReplaceArgumente im Wert von 10 Wörtern (drei Scheiben und eine int) benötigt.
  • Für Slices müssen Sie keinen Zeiger übergeben, um Elemente des Arrays zu ändern. io.Reader.Read(p []byte)ändert pzum Beispiel die Bytes von . Es ist wohl ein Sonderfall, "kleine Strukturen wie Werte zu behandeln", da Sie intern eine kleine Struktur herumgeben, die als Slice-Header bezeichnet wird (siehe Erklärung von Russ Cox (rsc) ). Ebenso benötigen Sie keinen Zeiger, um eine Karte zu ändern oder auf einem Kanal zu kommunizieren .

  • Für Slices werden Sie neu schneiden (ändern Sie den Start / die Länge / die Kapazität von), integrierte Funktionen wie das appendAkzeptieren eines Slice-Werts und das Zurückgeben eines neuen. Ich würde das nachahmen; Es vermeidet Aliasing. Wenn Sie ein neues Slice zurückgeben, wird die Aufmerksamkeit auf die Tatsache gelenkt, dass möglicherweise ein neues Array zugewiesen wird, und es ist den Anrufern vertraut.

    • Es ist nicht immer praktisch, diesem Muster zu folgen. Einige Tools wie Datenbankschnittstellen oder Serializer müssen an ein Slice angehängt werden, dessen Typ zum Zeitpunkt der Kompilierung nicht bekannt ist. Sie akzeptieren manchmal einen Zeiger auf ein Slice in einem interface{}Parameter.
  • Zuordnungen, Kanäle, Zeichenfolgen sowie Funktions- und Schnittstellenwerte sind wie Slices interne Referenzen oder Strukturen, die bereits Referenzen enthalten. Wenn Sie also nur vermeiden möchten, dass die zugrunde liegenden Daten kopiert werden, müssen Sie keine Zeiger an sie übergeben . (rsc hat einen separaten Beitrag darüber geschrieben, wie Schnittstellenwerte gespeichert werden ).

    • Sie müssen noch können Zeiger im selteneren Fall passieren , dass Sie zu wollen , ändern den Anrufer Struktur: flag.StringVarnehmen eine *stringaus diesem Grunde, zum Beispiel.

Wo Sie Zeiger verwenden:

  • Überlegen Sie, ob Ihre Funktion eine Methode für die Struktur sein soll, auf die Sie einen Zeiger benötigen. Die Menschen erwarten , auf eine Menge von Methoden xzu modifizieren x, so dass die modifizierte Struktur macht der Empfänger Überraschung minimieren kann helfen. Es gibt Richtlinien, wann Empfänger Zeiger sein sollten.

  • Funktionen, die Auswirkungen auf ihre Nicht-Empfänger-Parameter haben, sollten dies im Godoc oder besser noch im Godoc und im Namen (wie reader.WriteTo(writer)) verdeutlichen .

  • Sie erwähnen das Akzeptieren eines Zeigers, um Zuordnungen zu vermeiden, indem Sie die Wiederverwendung zulassen. Das Ändern von APIs zum Zwecke der Wiederverwendung von Speicher ist eine Optimierung, die ich verzögern würde, bis klar ist, dass die Zuweisungen nicht triviale Kosten verursachen, und dann nach einer Möglichkeit suchen würde, die nicht allen Benutzern die schwierigere API aufzwingt:

    1. Um Zuweisungen zu vermeiden, ist Go's Fluchtanalyse Ihr Freund. Sie können manchmal dazu beitragen, Heap-Zuweisungen zu vermeiden, indem Sie Typen erstellen, die mit einem einfachen Konstruktor, einem einfachen Literal oder einem nützlichen Nullwert wie initialisiert werden können bytes.Buffer.
    2. Reset()Stellen Sie sich eine Methode vor, um ein Objekt wieder in einen leeren Zustand zu versetzen, wie es einige stdlib-Typen anbieten. Benutzer, die eine Zuordnung nicht interessieren oder nicht speichern können, müssen sie nicht aufrufen.
    3. Betrachten wir das Schreiben Modifizier-in-Place - Verfahren und Erstellen von Grund auf Funktionen wie passenden Paare, für die Bequemlichkeit: existingUser.LoadFromJSON(json []byte) errordurch umwickelnden könnte NewUserFromJSON(json []byte) (*User, error). Auch hier wird die Wahl zwischen Faulheit und Kneifzuweisungen an den einzelnen Anrufer getroffen.
    4. Anrufer, die Speicher recyceln möchten, können sync.Pooleinige Details verarbeiten lassen. Wenn eine bestimmte Zuordnung viel Speicherdruck erzeugt, können Sie sicher sein, dass Sie wissen, wann die Zuordnung nicht mehr verwendet wird und keine bessere Optimierung verfügbar sync.Poolist. (CloudFlare hat einen nützlichen (Vor- sync.Pool) Blog-Beitrag zum Thema Recycling veröffentlicht.)

Schließlich, ob Ihre Slices Zeiger sein sollen: Slices von Werten können nützlich sein und Ihnen Zuordnungen und Cache-Fehler ersparen. Es kann Blocker geben:

  • Die API zum Erstellen Ihrer Elemente erzwingt möglicherweise Zeiger auf Sie, z. B. müssen Sie aufrufen, NewFoo() *Fooanstatt Go mit dem Wert Null initialisieren zu lassen .
  • Die gewünschten Lebensdauern der Artikel sind möglicherweise nicht alle gleich. Die ganze Scheibe wird auf einmal befreit; Wenn 99% der Elemente nicht mehr nützlich sind, Sie jedoch Zeiger auf die anderen 1% haben, bleibt das gesamte Array zugewiesen.
  • Das Verschieben von Objekten kann zu Problemen führen. Kopiert insbesondere appendElemente, wenn das zugrunde liegende Array vergrößert wird . Zeiger, die Sie vor dem appendPunkt an der falschen Stelle erhalten haben, können für große Strukturen langsamer kopiert werden, und zum Beispiel ist das sync.MutexKopieren nicht zulässig. Einfügen / Löschen in der Mitte und Sortieren verschieben Elemente auf ähnliche Weise.

Im Allgemeinen können Wertscheiben sinnvoll sein, wenn Sie entweder alle Ihre Elemente im Voraus platzieren und sie nicht verschieben (z. B. keine appends nach der Ersteinrichtung mehr) oder wenn Sie sie weiter verschieben, aber Sie sind sicher, dass dies der Fall ist OK (keine / sorgfältige Verwendung von Zeigern auf Elemente, Elemente sind klein genug, um effizient zu kopieren usw.). Manchmal müssen Sie über die Besonderheiten Ihrer Situation nachdenken oder diese messen, aber das ist eine grobe Richtlinie.

zwei
quelle
12
Was bedeutet große Strukturen? Gibt es ein Beispiel für eine große und eine kleine Struktur?
Der Benutzer ohne Hut
1
Wie erkennt man Bytes.Replace benötigt auf amd64 Argumente im Wert von 80 Bytes?
Tim Wu
2
Die Unterschrift ist Replace(s, old, new []byte, n int) []byte; s, alt und neu sind jeweils drei Wörter ( Slice-Header sind(ptr, len, cap) ) und n intsind ein Wort, also 10 Wörter, was bei acht Bytes / Wort 80 Bytes entspricht.
Zwei am
6
Wie definieren Sie große Strukturen? Wie groß ist groß?
Andy Aldo
3
@AndyAldo Keine meiner Quellen (Codeüberprüfungskommentare usw.) definiert einen Schwellenwert, daher habe ich beschlossen, zu sagen, dass es sich um einen Urteilsaufruf handelt, anstatt einen Schwellenwert festzulegen. Drei Wörter (wie ein Slice) werden ziemlich konsequent als berechtigt behandelt, ein Wert in der stdlib zu sein. Ich habe gerade eine Instanz eines Fünf-Wort-Wertempfängers gefunden (Text / Scanner.Position), aber ich würde nicht viel darüber lesen (es wird auch als Zeiger übergeben!). Ohne Benchmarks usw. würde ich einfach das tun, was für die Lesbarkeit am bequemsten erscheint.
Zwei
10

Drei Hauptgründe, warum Sie Methodenempfänger als Zeiger verwenden möchten:

  1. "Erstens und vor allem muss die Methode den Empfänger modifizieren? Wenn dies der Fall ist, muss der Empfänger ein Zeiger sein."

  2. "Zweitens geht es um die Effizienz. Wenn der Empfänger groß ist, beispielsweise eine große Struktur, ist die Verwendung eines Zeigerempfängers viel billiger."

  3. "Als nächstes kommt die Konsistenz. Wenn einige Methoden des Typs Zeigerempfänger haben müssen, sollte dies auch der Rest tun, damit der Methodensatz unabhängig von der Verwendung des Typs konsistent ist."

Referenz: https://golang.org/doc/faq#methods_on_values_or_pointers

Bearbeiten: Eine weitere wichtige Sache ist es, den tatsächlichen "Typ" zu kennen, den Sie an die Funktion senden. Der Typ kann entweder ein 'Werttyp' oder ein 'Referenztyp' sein.

Auch wenn Slices und Maps als Referenzen fungieren, möchten wir sie möglicherweise als Zeiger in Szenarien wie dem Ändern der Länge des Slice in der Funktion übergeben.

Santosh Pillai
quelle
1
Was ist für 2 der Cutoff? Woher weiß ich, ob meine Struktur groß oder klein ist? Außerdem gibt es eine Struktur , die klein genug ist, so ist , dass es mehr effizient einen Wert zu verwenden , anstatt Zeiger (so dass sie nicht aus dem Heap verwiesen werden muss)?
Zlotnika
Ich würde sagen, je mehr Felder und / oder verschachtelte Strukturen darin enthalten sind, desto größer ist die Struktur. Ich bin nicht sicher, ob es einen bestimmten Cutoff oder eine Standardmethode gibt, um zu wissen, wann eine Struktur als "groß" oder "groß" bezeichnet werden kann. Wenn ich eine Struktur verwende oder erstelle, würde ich wissen, ob sie groß oder klein ist, basierend auf dem, was ich oben gesagt habe. Aber das bin nur ich!
Santosh Pillai
2

Wenn Sie können (z. B. eine nicht freigegebene Ressource, die nicht als Referenz übergeben werden muss), verwenden Sie einen Wert. Aus folgenden Gründen:

  1. Ihr Code wird besser und besser lesbar sein und Zeigeroperatoren und Nullprüfungen vermeiden.
  2. Ihr Code ist sicherer gegen Null-Zeiger-Panik.
  3. Ihr Code wird oft schneller sein: Ja, schneller! Warum?

Grund 1 : Sie werden weniger Elemente im Stapel zuweisen. Die Zuweisung / Freigabe vom Stapel erfolgt sofort, die Zuweisung / Freigabe auf dem Heap kann jedoch sehr teuer sein (Zuweisungszeit + Speicherbereinigung). Einige grundlegende Zahlen finden Sie hier: http://www.macias.info/entry/201802102230_go_values_vs_references.md

Grund 2 : Insbesondere wenn Sie zurückgegebene Werte in Slices speichern, werden Ihre Speicherobjekte im Speicher kompakter: Das Schleifen eines Slice, in dem alle Elemente zusammenhängend sind, ist viel schneller als das Iterieren eines Slice, in dem alle Elemente Zeiger auf andere Teile des Speichers sind . Nicht für den Indirektionsschritt, sondern für die Erhöhung von Cache-Fehlern.

Mythosbrecher : Eine typische x86-Cache-Zeile besteht aus 64 Bytes. Die meisten Strukturen sind kleiner als das. Der Zeitpunkt des Kopierens einer Cache-Zeile im Speicher ähnelt dem Kopieren eines Zeigers.

Nur wenn ein kritischer Teil Ihres Codes langsam ist, würde ich eine Mikrooptimierung versuchen und prüfen, ob die Verwendung von Zeigern die Geschwindigkeit etwas verbessert, was die Lesbarkeit und Wartbarkeit beeinträchtigt.

Mario
quelle
1

Ein Fall, in dem Sie im Allgemeinen einen Zeiger zurückgeben müssen, ist das Erstellen einer Instanz einer zustandsbehafteten oder gemeinsam nutzbaren Ressource . Dies geschieht häufig durch Funktionen, denen ein Präfix vorangestellt ist New.

Da sie eine bestimmte Instanz von etwas darstellen und möglicherweise eine Aktivität koordinieren müssen, ist es nicht sehr sinnvoll, duplizierte / kopierte Strukturen zu generieren, die dieselbe Ressource darstellen. Der zurückgegebene Zeiger fungiert daher als Handle für die Ressource selbst .

Einige Beispiele:

In anderen Fällen werden Zeiger zurückgegeben, nur weil die Struktur möglicherweise zu groß ist, um standardmäßig kopiert zu werden:


Alternativ könnte die direkte Rückgabe von Zeigern vermieden werden, indem stattdessen eine Kopie einer Struktur zurückgegeben wird, die den Zeiger intern enthält. Dies wird jedoch möglicherweise nicht als idiomatisch angesehen:

kein Balken
quelle
Diese Analyse impliziert, dass Strukturen standardmäßig nach Wert kopiert werden (aber nicht unbedingt nach ihren indirekten Mitgliedern).
Nobar