Puntatori vs. valori nei parametri e valori restituiti


329

In Vai ci sono vari modi per restituire un structvalore o una sua porzione. Per quelli individuali che ho visto:

type MyStruct struct {
    Val int
}

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

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

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

Capisco le differenze tra questi. Il primo restituisce una copia della struttura, il secondo un puntatore al valore della struttura creato all'interno della funzione, il terzo si aspetta che una struttura esistente venga passata e sovrascrive il valore.

Ho visto tutti questi schemi essere usati in vari contesti, mi chiedo quali siano le migliori pratiche in merito. Quando useresti quale? Ad esempio, il primo potrebbe andare bene per le piccole strutture (perché il sovraccarico è minimo), il secondo per quelle più grandi. E il terzo se si desidera essere estremamente efficienti in termini di memoria, perché è possibile riutilizzare facilmente una singola istanza di struttura tra le chiamate. Ci sono delle migliori pratiche per quando usare quali?

Allo stesso modo, la stessa domanda per quanto riguarda le sezioni:

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} }
}

Ancora: quali sono le migliori pratiche qui. So che le sezioni sono sempre dei puntatori, quindi restituire un puntatore a una fetta non è utile. Tuttavia, dovrei restituire una porzione di valori di struttura, una fetta di puntatori a strutture, dovrei passare un puntatore a una fetta come argomento (un modello utilizzato nell'API del motore dell'app Go )?


1
Come dici tu, dipende davvero dal caso d'uso. Tutti sono validi a seconda della situazione - è un oggetto mutabile? vogliamo una copia o un puntatore? ecc. A proposito, non hai menzionato l'uso di new(MyStruct):) Ma in realtà non vi è alcuna differenza tra i diversi metodi di allocazione dei puntatori e di restituzione.
Not_a_Golfer

15
Questo è letteralmente oltre l'ingegneria. Le strutture devono essere piuttosto grandi che la restituzione di un puntatore rende il programma più veloce. Non preoccuparti, codifica, profilo, correggi se utile.
Volker,

1
Esiste un solo modo per restituire un valore o un puntatore, ovvero restituire un valore o un puntatore. Il modo in cui li assegni è un problema separato. Usa ciò che funziona per la tua situazione e vai a scrivere del codice prima di preoccupartene.
JimB,

3
A proposito, solo per curiosità, ho scritto questo. Restituire le strutture rispetto ai puntatori sembra avere approssimativamente la stessa velocità, ma passare i puntatori alle funzioni lungo le linee è notevolmente più veloce. Anche se non a un livello, importerebbe
Not_a_Golfer

1
@Not_a_Golfer: suppongo che sia solo l'allocazione bc fatta fuori dalla funzione. Anche il benchmarking dei valori rispetto ai puntatori dipende dalla dimensione della struttura e dai modelli di accesso alla memoria dopo il fatto. La copia di oggetti di dimensioni di una linea di cache è la più veloce possibile e la velocità di dereferenziazione dei puntatori dalla cache della CPU è molto diversa rispetto alla loro dereferenziazione dalla memoria principale.
JimB,

Risposte:


392

tl; dr :

  • I metodi che utilizzano i puntatori del ricevitore sono comuni; la regola empirica per i ricevitori è "In caso di dubbio, utilizzare un puntatore".
  • Fette, mappe, canali, stringhe, valori di funzione e valori di interfaccia sono implementati internamente con puntatori e un puntatore ad essi è spesso ridondante.
  • Altrove, usa i puntatori per grandi strutture o strutture che dovrai cambiare, e altrimenti passa valori , perché far cambiare le cose di sorpresa tramite un puntatore è confuso.

Un caso in cui dovresti usare spesso un puntatore:

  • I ricevitori sono puntatori più spesso di altri argomenti. Non è insolito che i metodi modifichino la cosa su cui sono chiamati, o che i tipi nominati siano strutture di grandi dimensioni, quindi la guida è quella di default sui puntatori, tranne in rari casi.
    • Lo strumento copyfighter di Jeff Hodges cerca automaticamente i ricevitori non piccoli passati per valore.

Alcune situazioni in cui non hai bisogno di puntatori:

  • Le linee guida per la revisione del codice suggeriscono il passaggio di piccole strutture come type Point struct { latitude, longitude float64 }, e forse anche cose un po 'più grandi, come valori, a meno che la funzione che stai chiamando non sia in grado di modificarle sul posto.

    • La semantica del valore evita situazioni di aliasing in cui un'assegnazione qui cambia di sorpresa un valore laggiù.
    • Non è Go-y sacrificare la semantica pulita per un po 'di velocità, e talvolta passare piccole strutture per valore è in realtà più efficiente, perché evita mancate cache o allocazioni di heap.
    • Quindi, la pagina dei commenti sulla revisione del codice di Go Wiki suggerisce di passare per valore quando le strutture sono piccole e probabilmente rimarranno tali.
    • Se il "grande" taglio sembra vago, lo è; probabilmente molte strutture sono in un intervallo in cui un puntatore o un valore sono OK. Come limite inferiore, i commenti di revisione del codice suggeriscono che le sezioni (tre parole macchina) sono ragionevoli da usare come destinatari del valore. Man mano che qualcosa si avvicina a un limite superiore, bytes.Replaceimpiega 10 parole di args (tre sezioni e una int).
  • Per le sezioni , non è necessario passare un puntatore per modificare gli elementi dell'array. io.Reader.Read(p []byte)cambia i byte di p, per esempio. È senza dubbio un caso speciale di "tratta le piccole strutture come valori", poiché internamente stai passando intorno a una piccola struttura chiamata header di sezione (vedi la spiegazione di Russ Cox (rsc) ). Allo stesso modo, non è necessario un puntatore per modificare una mappa o comunicare su un canale .

  • Per le sezioni dovrai ridimensionare (modificare l'inizio / lunghezza / capacità di), le funzioni integrate come appendaccettare un valore di sezione e restituirne uno nuovo. Lo imiterei; evita l'aliasing, restituendo una nuova porzione aiuta a richiamare l'attenzione sul fatto che un nuovo array potrebbe essere allocato ed è familiare ai chiamanti.

    • Non è sempre pratico seguire quel modello. Alcuni strumenti come interfacce di database o serializzatori devono essere aggiunti a una sezione il cui tipo non è noto al momento della compilazione. A volte accettano un puntatore a una sezione in un interface{}parametro.
  • Mappe, canali, stringhe e valori di funzioni e interfaccia , come sezioni, sono riferimenti interni o strutture che contengono già riferimenti, quindi se stai solo cercando di evitare di copiare i dati sottostanti, non è necessario passare loro dei puntatori . (rsc ha scritto un post separato su come sono memorizzati i valori dell'interfaccia ).

    • Potrebbe essere necessario passare i puntatori nel caso più raro in cui si desidera modificare la struttura del chiamante: ad esempio, flag.StringVarprende a *stringper questo motivo.

Dove usi i puntatori:

  • Valuta se la tua funzione dovrebbe essere un metodo su qualunque struttura tu abbia bisogno di un puntatore. Le persone si aspettano molti metodi xper modificare x, quindi rendendo la struttura modificata il ricevitore può aiutare a minimizzare la sorpresa. Ci sono linee guida su quando i ricevitori dovrebbero essere puntatori.

  • Le funzioni che hanno effetti sui loro parametri non riceventi dovrebbero renderlo chiaro nel godoc, o meglio ancora, nel godoc e nel nome (come reader.WriteTo(writer)).

  • Accetti di accettare un puntatore per evitare allocazioni consentendo il riutilizzo; la modifica delle API per il riutilizzo della memoria è un'ottimizzazione che ritarderei fino a quando è chiaro che le allocazioni hanno un costo non banale, quindi cercherò un modo che non imponga l'API più complicata su tutti gli utenti:

    1. Per evitare allocazioni, l' analisi di escape di Go è tua amica. A volte puoi aiutare a evitare le allocazioni di heap creando tipi che possono essere inizializzati con un costruttore banale, un semplice valore letterale o un valore zero utile come bytes.Buffer.
    2. Considera un Reset()metodo per riportare un oggetto in uno stato vuoto, come offrono alcuni tipi stdlib. Gli utenti che non si preoccupano o non possono salvare un'allocazione non devono chiamarla.
    3. Prendi in considerazione la possibilità di scrivere metodi di modifica sul posto e funzioni di creazione da zero come coppie corrispondenti, per comodità: existingUser.LoadFromJSON(json []byte) errorpotrebbero essere racchiusi da NewUserFromJSON(json []byte) (*User, error). Ancora una volta, spinge la scelta tra pigrizia e pizzicare le allocazioni al singolo chiamante.
    4. I chiamanti che desiderano riciclare la memoria possono sync.Poolgestire alcuni dettagli. Se una determinata allocazione crea molta pressione nella memoria, sei sicuro di sapere quando l'allocazione non viene più utilizzata e non hai una migliore ottimizzazione disponibile, sync.Poolpuò aiutarti. (CloudFlare ha pubblicato un utile sync.Poolpost (pre ) sul blog sul riciclaggio.)

Infine, se le sezioni devono essere puntatori: sezioni di valori possono essere utili e salvare allocazioni e mancate cache. Possono esserci blocchi:

  • L'API per creare i tuoi articoli potrebbe imporre puntatori su di te, ad esempio devi chiamare NewFoo() *Fooanziché lasciare inizializzare Go con il valore zero .
  • Le durate desiderate degli articoli potrebbero non essere tutte uguali. L'intera sezione viene liberata in una sola volta; se il 99% degli elementi non è più utile ma si hanno puntatori sull'altro 1%, tutto l'array rimane allocato.
  • Spostare gli oggetti potrebbe causare problemi. In particolare, appendcopia gli elementi quando cresce l'array sottostante . Puntatori che hai ottenuto prima del appendpunto in un posto sbagliato dopo, la copia può essere più lenta per enormi strutture e, ad esempio, la sync.Mutexcopia non è consentita. Inserisci / elimina al centro e ordina in modo simile per spostare gli oggetti.

In linea di massima, le sezioni di valore possono avere senso se si mettono tutti gli oggetti in posizione frontale e non li si sposta (ad es. Non più appends dopo la configurazione iniziale) o se si continua a spostarli ma si è sicuri che OK (no / uso attento dei puntatori agli oggetti, gli oggetti sono abbastanza piccoli da essere copiati in modo efficiente, ecc.). A volte devi pensare o misurare i dettagli della tua situazione, ma questa è una guida approssimativa.


12
Cosa significa grandi strutture? C'è un esempio di una grande struttura e una piccola struttura?
L'utente senza cappello

1
Come si fa a dire byte. Sostituisci prende 80 byte di args su amd64?
Tim Wu,

2
La firma è Replace(s, old, new []byte, n int) []byte; s, vecchio e nuovo sono tre parole ciascuno (le intestazioni di sezione sono(ptr, len, cap) ) ed n intè una parola, quindi 10 parole, che a otto byte / parola è 80 byte.
twotwotwo

6
Come definisci le grandi strutture? Quanto è grande?
Andy Aldo,

3
@AndyAldo Nessuna delle mie fonti (commenti di revisione del codice, ecc.) Definisce una soglia, quindi ho deciso di dire che è una chiamata di giudizio invece di aumentare la soglia. Tre parole (come una sezione) sono abbastanza coerentemente trattate come idonee per essere un valore nello stdlib. Ho trovato un'istanza di un ricevitore di valori di cinque parole in questo momento (text / scanner.Position) ma non avrei letto molto in questo (è anche passato come un puntatore!). Assenti parametri di riferimento, ecc., Farei semplicemente tutto ciò che sembra più conveniente per la leggibilità.
due

10

Tre motivi principali per utilizzare i ricevitori del metodo come puntatori:

  1. "In primo luogo, e soprattutto, il metodo deve modificare il ricevitore? In tal caso, il ricevitore deve essere un puntatore."

  2. "La seconda è la considerazione dell'efficienza. Se il ricevitore è grande, ad esempio una grande struttura, sarà molto più economico usare un ricevitore puntatore."

  3. "Il prossimo è la coerenza. Se alcuni dei metodi del tipo devono avere ricevitori puntatore, anche il resto dovrebbe, quindi il set di metodi è coerente indipendentemente da come viene utilizzato il tipo"

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

Modifica: un'altra cosa importante è conoscere il vero "tipo" che stai inviando alla funzione. Il tipo può essere un "tipo di valore" o un "tipo di riferimento".

Anche se sezioni e mappe fungono da riferimenti, potremmo volerle passare come puntatori in scenari come la modifica della lunghezza della sezione nella funzione.


1
Per 2, qual è il limite? Come faccio a sapere se la mia struttura è grande o piccola? Inoltre, esiste una struttura abbastanza piccola da rendere più efficiente l'uso di un valore anziché di un puntatore (in modo che non debba essere referenziato dall'heap)?
zlotnika,

Direi più il numero di campi e / o strutture nidificate all'interno, maggiore è la struttura. Non sono sicuro che esista un taglio specifico o un modo standard per sapere quando una struttura può essere definita "grande" o "grande". Se sto usando o creando una struttura, vorrei sapere se è grande o piccolo in base a ciò che ho detto sopra. Ma sono solo io !.
Santosh Pillai,

2

Un caso in cui generalmente è necessario restituire un puntatore è quando si costruisce un'istanza di una risorsa stateful o condivisibile . Questo è spesso fatto da funzioni con prefisso New.

Poiché rappresentano un'istanza specifica di qualcosa e potrebbero dover coordinare alcune attività, non ha molto senso generare strutture duplicate / copiate che rappresentano la stessa risorsa, quindi il puntatore restituito funge da handle per la risorsa stessa .

Qualche esempio:

In altri casi, i puntatori vengono restituiti solo perché la struttura potrebbe essere troppo grande per essere copiata per impostazione predefinita:


In alternativa, è possibile evitare di restituire direttamente i puntatori restituendo invece una copia di una struttura che contiene il puntatore internamente, ma forse questo non è considerato idiomatico:


In questa analisi è implicito che, per impostazione predefinita, le strutture sono copiate per valore (ma non necessariamente per i loro membri indiretti).
nobar,

2

Se è possibile (ad esempio una risorsa non condivisa che non deve essere passata come riferimento), utilizzare un valore. Con i seguenti motivi:

  1. Il tuo codice sarà più gradevole e leggibile, evitando operatori di puntatori e controlli nulli.
  2. Il tuo codice sarà più sicuro contro il panico del puntatore null.
  3. Il tuo codice sarà spesso più veloce: sì, più veloce! Perché?

Motivo 1 : assegnerai meno oggetti nello stack. L'allocazione / deallocazione dallo stack è immediata, ma l'allocazione / deallocazione su Heap può essere molto costosa (tempo di allocazione + garbage collection). Puoi vedere alcuni numeri di base qui: http://www.macias.info/entry/201802102230_go_values_vs_references.md

Motivo 2 : specialmente se si memorizzano i valori restituiti in sezioni, gli oggetti di memoria saranno più compatti in memoria: eseguire il ciclo di una sezione in cui tutti gli elementi sono contigui è molto più veloce rispetto all'iterazione di una sezione in cui tutti gli elementi sono puntatori ad altre parti della memoria . Non per il passaggio indiretto ma per l'aumento dei mancati cache.

Myth breaker : una tipica linea cache x86 ha 64 byte. La maggior parte delle strutture sono più piccole di così. Il tempo di copia di una riga della cache in memoria è simile alla copia di un puntatore.

Solo se una parte critica del tuo codice è lenta, proverei un po 'di micro-ottimizzazione e verificherei se l'uso dei puntatori migliora un po' la velocità, a costo di minore leggibilità e manutenibilità.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.