Ricevitore di valore vs ricevitore di puntatore


108

Non è molto chiaro per me, nel qual caso vorrei utilizzare un ricevitore di valore invece di usare sempre un ricevitore di puntatore.
Ricapitolando dai documenti:

type T struct {
    a int
}
func (tv  T) Mv(a int) int         { return 0 }  // value receiver
func (tp *T) Mp(f float32) float32 { return 1 }  // pointer receiver

La documentazione dice anche "Per i tipi come i tipi di base, le slice e le piccole strutture, un ricevitore di valore è molto economico, quindi a meno che la semantica del metodo non richieda un puntatore, un ricevitore di valore è efficiente e chiaro."

Primo punto dice che è "molto economico", ma la domanda è più è più economico del ricevitore del puntatore. Così ho fatto un piccolo benchmark (code on gist) che mi ha mostrato che il ricevitore del puntatore è più veloce anche per una struttura che ha un solo campo stringa. Questi sono i risultati:

// Struct one empty string property
BenchmarkChangePointerReceiver  2000000000               0.36 ns/op
BenchmarkChangeItValueReceiver  500000000                3.62 ns/op


// Struct one zero int property
BenchmarkChangePointerReceiver  2000000000               0.36 ns/op
BenchmarkChangeItValueReceiver  2000000000               0.36 ns/op

(Modifica: si prega di notare che il secondo punto non è più valido nelle versioni go più recenti, vedere i commenti) .
Secondo punto dice, è "efficiente e chiaro" che è più una questione di gusti, non è vero? Personalmente preferisco la coerenza usando ovunque allo stesso modo. Efficienza in che senso? dal punto di vista delle prestazioni sembra che i puntatori siano quasi sempre più efficienti. Poche prove con una proprietà int hanno mostrato un vantaggio minimo del ricevitore Value (intervallo di 0,01-0,1 ns / op)

Qualcuno può dirmi un caso in cui un ricevitore di valore ha chiaramente più senso di un ricevitore di puntatore? O sto facendo qualcosa di sbagliato nel benchmark, ho trascurato altri fattori?


3
Ho eseguito benchmark simili con un singolo campo stringa e anche con due campi: string e int. Ho ottenuto risultati più rapidi dal ricevitore del valore. BenchmarkChangePointerReceiver-4 10000000000 0,99 ns / op BenchmarkChangeItValueReceiver-4 10000000000 0,33 ns / op Questo utilizza Go 1.8. Mi chiedo se sono state apportate ottimizzazioni del compilatore dall'ultima volta che hai eseguito i benchmark. Vedi l' essenza per maggiori dettagli.
pbitty

2
Hai ragione. Eseguendo il mio benchmark originale utilizzando Go1.9, ora ottengo anche risultati diversi. Ricevitore puntatore 0,60 ns / op, valore ricevitore 0,38 ns / op
Chrisport

Risposte:


118

Nota che le FAQ menzionano la coerenza

Il prossimo è la coerenza. Se alcuni dei metodi del tipo devono avere ricevitori di puntatori, anche il resto dovrebbe esserlo, quindi il metodo impostato è coerente indipendentemente da come viene utilizzato il tipo. Vedere la sezione sui set di metodi per i dettagli.

Come accennato in questo thread :

La regola sui puntatori e sui valori per i ricevitori è che i metodi del valore possono essere invocati su puntatori e valori, ma i metodi del puntatore possono essere invocati solo sui puntatori

Adesso:

Qualcuno può dirmi un caso in cui un ricevitore di valore ha chiaramente più senso di un ricevitore di puntatore?

Il commento di revisione del codice può aiutare:

  • Se il ricevitore è una mappa, func o chan, non usare un puntatore ad esso.
  • Se il destinatario è una sezione e il metodo non ridimensiona o rialloca la sezione, non utilizzare un puntatore ad essa.
  • Se il metodo deve mutare il ricevitore, il ricevitore deve essere un puntatore.
  • Se il destinatario è una struttura che contiene un sync.Mutexcampo di sincronizzazione o simile, il destinatario deve essere un puntatore per evitare la copia.
  • Se il ricevitore è una struttura o un array di grandi dimensioni, un ricevitore puntatore è più efficiente. Quanto è grande? Supponiamo che sia equivalente a passare tutti i suoi elementi come argomenti al metodo. Se sembra troppo grande, è anche troppo grande per il ricevitore.
  • La funzione o i metodi, contemporaneamente o quando vengono chiamati da questo metodo, possono mutare il ricevitore? Un tipo di valore crea una copia del ricevitore quando il metodo viene richiamato, quindi gli aggiornamenti esterni non verranno applicati a questo ricevitore. Se le modifiche devono essere visibili nel ricevitore originale, il ricevitore deve essere un puntatore.
  • Se il ricevitore è una struttura, un array o uno slice e uno qualsiasi dei suoi elementi è un puntatore a qualcosa che potrebbe essere in mutazione, preferisci un ricevitore puntatore, poiché renderà l'intenzione più chiara al lettore.
  • Se il ricevitore è un piccolo array o una struttura che è naturalmente un tipo di valore (ad esempio, qualcosa di simile al time.Timetipo), senza campi modificabili e senza puntatori, o è solo un semplice tipo di base come int o stringa, un ricevitore di valore fa senso .
    Un ricevitore di valore può ridurre la quantità di spazzatura che può essere generata; se un valore viene passato a un metodo valore, è possibile utilizzare una copia sullo stack invece di allocare sull'heap.(Il compilatore cerca di evitare questa allocazione, ma non può sempre avere successo.) Non scegliere un tipo di ricevitore di valore per questo motivo senza prima eseguire il profilo.
  • Infine, in caso di dubbio, utilizzare un ricevitore puntatore.

La parte in grassetto si trova ad esempio in net/http/server.go#Write():

// Write writes the headers described in h to w.
//
// This method has a value receiver, despite the somewhat large size
// of h, because it prevents an allocation. The escape analysis isn't
// smart enough to realize this function doesn't mutate h.
func (h extraHeader) Write(w *bufio.Writer) {
...
}

16
The rule about pointers vs. values for receivers is that value methods can be invoked on pointers and values, but pointer methods can only be invoked on pointers Non è vero, in realtà. Entrambi i metodi ricevitore valore e ricevitore puntatore possono essere richiamati su un puntatore digitato correttamente o su un non puntatore. Indipendentemente da ciò su cui viene chiamato il metodo, all'interno del corpo del metodo l'identificatore del ricevitore si riferisce a un valore di copia quando viene utilizzato un ricevitore di valore e un puntatore quando viene utilizzato un ricevitore di puntatore: vedere play.golang.org/p / 3WHGaAbURM
Hart Simha

3
C'è una grande spiegazione qui "Se x è indirizzabile e il set di metodi di & x contiene m, xm () è un'abbreviazione per (& x) .m ()."
tera

@tera Sì: che viene discusso a stackoverflow.com/q/43953187/6309
VonC

4
Ottima risposta ma sono fortemente in disaccordo con questo punto: "poiché renderà l'intenzione più chiara", NOPE, un'API pulita, X come argomento e Y come valore di ritorno è una chiara intenzione. Passare uno Struct tramite il puntatore e dedicare tempo alla lettura attenta del codice per verificare cosa vengono modificati tutti gli attributi è tutt'altro che chiaro e gestibile.
Lukas Lukac

@HartSimha Penso che il post sopra indichi il fatto che i metodi del ricevitore del puntatore non sono in "metodo impostato" per i tipi di valore. Nel vostro parco giochi legati, aggiungendo seguente riga si tradurrà in errore di compilazione: Int(5).increment_by_one_ptr(). Allo stesso modo, un tratto che definisce il metodo increment_by_one_ptrnon sarà soddisfatto con un valore di tipo Int.
Gaurav Agarwal

16

Per aggiungere in aggiunta a @VonC un'ottima risposta informativa.

Sono sorpreso che nessuno abbia davvero menzionato il costo di manutenzione una volta che il progetto diventa più grande, i vecchi sviluppatori se ne vanno e ne arriva uno nuovo. Go è sicuramente una lingua giovane.

In generale, cerco di evitare i suggerimenti quando posso, ma hanno il loro posto e la loro bellezza.

Uso i puntatori quando:

  • lavorare con set di dati di grandi dimensioni
  • avere uno stato di mantenimento della struttura, ad esempio TokenCache,
    • Mi assicuro che TUTTI i campi siano PRIVATI, l'interazione è possibile solo tramite ricevitori di metodi definiti
    • Non passo questa funzione a nessuna goroutine

Per esempio:

type TokenCache struct {
    cache map[string]map[string]bool
}

func (c *TokenCache) Add(contract string, token string, authorized bool) {
    tokens := c.cache[contract]
    if tokens == nil {
        tokens = make(map[string]bool)
    }

    tokens[token] = authorized
    c.cache[contract] = tokens
}

Motivi per cui evito i puntatori:

  • i puntatori non sono contemporaneamente sicuri (l'intero punto di GoLang)
  • una volta ricevitore puntatore, sempre ricevitore puntatore (per tutti i metodi di Struct per coerenza)
  • i mutex sono sicuramente più costosi, più lenti e più difficili da mantenere rispetto al "costo della copia di valore"
  • parlando di "value copy cost", è davvero un problema? L'ottimizzazione prematura è alla radice di tutti i mali, puoi sempre aggiungere puntatori in seguito
  • direttamente, coscientemente mi costringe a progettare piccole strutture
  • i puntatori possono essere evitati principalmente progettando funzioni pure con una chiara intenzione e un I / O ovvio
  • la raccolta dei rifiuti è più difficile con i puntatori credo
  • più facile discutere di incapsulamento, responsabilità
  • mantienilo semplice, stupido (sì, i puntatori possono essere complicati perché non conosci mai lo sviluppo del prossimo progetto)
  • unit test è come camminare nel giardino rosa (solo espressione slovacca?), significa facile
  • no NIL se condizioni (NIL può essere passato dove era previsto un puntatore)

La mia regola pratica, scrivi il maggior numero possibile di metodi incapsulati come:

package rsa

// EncryptPKCS1v15 encrypts the given message with RSA and the padding scheme from PKCS#1 v1.5.
func EncryptPKCS1v15(rand io.Reader, pub *PublicKey, msg []byte) ([]byte, error) {
    return []byte("secret text"), nil
}

cipherText, err := rsa.EncryptPKCS1v15(rand, pub, keyBlock) 

AGGIORNARE:

Questa domanda mi ha ispirato a ricercare maggiormente l'argomento ea scrivere un post sul blog a riguardo https://medium.com/gophersland/gopher-vs-object-oriented-golang-4fa62b88c701


Mi piace il 99% di quello che dici qui e sono assolutamente d'accordo. Detto questo, mi chiedo se il tuo esempio sia il modo migliore per illustrare il tuo punto. TokenCache non è essenzialmente una mappa (da @VonC - "se il ricevitore è una mappa, func o chan, non usare un puntatore ad essa"). Dato che le mappe sono tipi di riferimento, cosa ottieni facendo di "Add ()" un ricevitore di puntatori? Qualsiasi copia di TokenCache farà riferimento alla stessa mappa. Guarda questo parco giochi Go - play.golang.com/p/Xda1rsGwvhq
Rich

Sono contento che siamo allineati. Ottimo punto. In realtà, penso di aver usato il puntatore in questo esempio perché l'ho copiato da un progetto in cui TokenCache gestisce più cose oltre a quella mappa. E se uso un puntatore in un metodo, lo uso in tutti. Suggerite di rimuovere il puntatore da questo particolare esempio SO?
Lukas Lukac

LOL, copia / incolla colpisce ancora! 😉 IMO puoi lasciarlo così com'è poiché illustra una trappola in cui è facile cadere, oppure potresti sostituire la mappa con qualcosa che dimostri lo stato e / o una grande struttura di dati.
Rich

Beh, sono sicuro che leggeranno i commenti ... PS: Ricco, i tuoi argomenti sembrano ragionevoli, aggiungimi su LinkedIn (link nel mio profilo) felice di connetterti.
Lukas Lukac
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.