Récepteur de valeur vs récepteur de pointeur

108

Il est très peu clair pour moi dans quel cas je voudrais utiliser un récepteur de valeur au lieu de toujours utiliser un récepteur de pointeur.
Pour récapituler à partir des documents:

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 documentation dit également "Pour les types tels que les types de base, les tranches et les petites structures, un récepteur de valeur est très bon marché donc à moins que la sémantique de la méthode ne nécessite un pointeur, un récepteur de valeur est efficace et clair."

Premier point, il dit qu'il est "très bon marché", mais la question est plus est-il moins cher que le récepteur de pointeur. J'ai donc fait un petit benchmark (code sur l'essentiel) qui m'a montré que le récepteur de pointeur est plus rapide même pour une structure qui n'a qu'un seul champ de chaîne. Voici les résultats:

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

(Modifier: veuillez noter que le deuxième point est devenu invalide dans les nouvelles versions de go, voir les commentaires) .
Deuxième point dit-il, c'est "efficace et clair", ce qui est plus une question de goût, n'est-ce pas? Personnellement, je préfère la cohérence en utilisant partout de la même manière. Efficacité dans quel sens? en termes de performances, il semble que les pointeurs soient presque toujours plus efficaces. Peu de tests avec une propriété int ont montré un avantage minimal du récepteur Value (plage de 0,01-0,1 ns / op)

Quelqu'un peut-il me dire un cas où un récepteur de valeur a clairement plus de sens qu'un récepteur de pointeur? Ou est-ce que je fais quelque chose de mal dans le benchmark, ai-je négligé d'autres facteurs?

Chrisport
la source
3
J'ai exécuté des tests similaires avec un seul champ de chaîne et également avec deux champs: les champs string et int. J'ai obtenu des résultats plus rapides du récepteur de valeur. BenchmarkChangePointerReceiver-4 10000000000 0,99 ns / op BenchmarkChangeItValueReceiver-4 10000000000 0,33 ns / op Ceci utilise Go 1.8. Je me demande s'il y a eu des optimisations du compilateur depuis la dernière exécution des benchmarks. Voir l' essentiel pour plus de détails.
pbitty
2
Vous avez raison. En exécutant mon benchmark d'origine en utilisant Go1.9, j'obtiens également des résultats différents. Récepteur de pointeur 0,60 ns / op, récepteur de valeur 0,38 ns / op
Chrisport

Réponses:

118

Notez que la FAQ mentionne la cohérence

Vient ensuite la cohérence. Si certaines des méthodes du type doivent avoir des récepteurs de pointeur, les autres devraient l'être également, de sorte que l'ensemble de méthodes est cohérent quelle que soit la façon dont le type est utilisé. Voir la section sur les ensembles de méthodes pour plus de détails.

Comme mentionné dans ce fil :

La règle concernant les pointeurs et les valeurs pour les récepteurs est que les méthodes de valeur peuvent être appelées sur des pointeurs et des valeurs, mais les méthodes de pointeur ne peuvent être appelées que sur des pointeurs

Maintenant:

Quelqu'un peut-il me dire un cas où un récepteur de valeur a clairement plus de sens qu'un récepteur de pointeur?

Le commentaire de révision de code peut aider:

  • Si le récepteur est une map, func ou chan, n'utilisez pas de pointeur vers celui-ci.
  • Si le récepteur est une tranche et que la méthode ne redistribue pas ou ne réaffecte pas la tranche, n'utilisez pas de pointeur vers elle.
  • Si la méthode doit muter le récepteur, le récepteur doit être un pointeur.
  • Si le receveur est une structure qui contient un sync.Mutex champ de synchronisation ou similaire, le récepteur doit être un pointeur pour éviter la copie.
  • Si le récepteur est une grande structure ou un tableau, un récepteur pointeur est plus efficace. Quelle est la taille? Supposons que cela équivaut à passer tous ses éléments comme arguments à la méthode. Si cela semble trop grand, c'est aussi trop grand pour le récepteur.
  • Une fonction ou des méthodes, simultanément ou appelées à partir de cette méthode, peuvent-elles muter le récepteur? Un type valeur crée une copie du récepteur lorsque la méthode est appelée, de sorte que les mises à jour extérieures ne seront pas appliquées à ce récepteur. Si les modifications doivent être visibles dans le récepteur d'origine, le récepteur doit être un pointeur.
  • Si le récepteur est une structure, un tableau ou une tranche et que l'un de ses éléments est un pointeur vers quelque chose qui pourrait être en mutation, préférez un récepteur pointeur, car il rendra l'intention plus claire pour le lecteur.
  • Si le récepteur est un petit tableau ou une structure qui est naturellement un type valeur (par exemple, quelque chose comme le time.Timetype), sans champs mutables et sans pointeurs, ou simplement un type de base simple tel que int ou string, un récepteur de valeur fait sens .
    Un récepteur de valeur peut réduire la quantité de déchets qui peut être générée; si une valeur est transmise à une méthode de valeur, une copie sur pile peut être utilisée au lieu d'allouer sur le tas. (Le compilateur essaie d'être intelligent pour éviter cette allocation, mais cela ne peut pas toujours réussir.) Ne choisissez pas un type de récepteur de valeur pour cette raison sans profilage préalable.
  • Enfin, en cas de doute, utilisez un récepteur pointeur.

La partie en gras se trouve par exemple dans 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) {
...
}
VonC
la source
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 Pas vrai, en fait. Les méthodes de récepteur de valeur et de récepteur de pointeur peuvent être appelées sur un pointeur ou un non-pointeur correctement typé. Indépendamment de ce sur quoi la méthode est appelée, dans le corps de la méthode, l'identifiant du récepteur fait référence à une valeur par copie lorsqu'un récepteur de valeur est utilisé, et à un pointeur lorsqu'un récepteur de pointeur est utilisé: Voir play.golang.org/p / 3WHGaAbURM
Hart Simha
3
Il y a une grande explication ici "Si x est adressable et que l'ensemble de méthodes de & x contient m, xm () est un raccourci pour (& x) .m ()."
tera
@tera Oui: cela est discuté sur stackoverflow.com/q/43953187/6309
VonC
4
Excellente réponse mais je ne suis pas du tout d'accord avec ce point: "car cela rendra l'intention plus claire", NOPE, une API propre, X comme argument et Y comme valeur de retour est une intention claire. Passer un Struct par pointeur et passer du temps à lire attentivement le code pour vérifier ce que tous les attributs sont modifiés est loin d'être clair, maintenable.
Lukas Lukac
@HartSimha Je pense que le post ci-dessus indique que les méthodes de récepteur de pointeur ne sont pas dans "ensemble de méthodes" pour les types valeur. Dans votre terrain de jeu lié, en ajoutant la ligne suivante se traduira par erreur de compilation: Int(5).increment_by_one_ptr(). De même, un trait qui définit la méthode increment_by_one_ptrne sera pas satisfait d'une valeur de type Int.
Gaurav Agarwal le
16

Pour ajouter en plus à @VonC une excellente réponse informative.

Je suis surpris que personne n'ait vraiment mentionné le coût de maintenance une fois que le projet s'agrandit, que les anciens développeurs partent et que le nouveau arrive. Go est sûrement une langue jeune.

De manière générale, j'essaie d'éviter les pointeurs quand je le peux, mais ils ont leur place et leur beauté.

J'utilise des pointeurs lorsque:

  • travailler avec de grands ensembles de données
  • avoir un état de maintien de structure, par exemple TokenCache,
    • Je m'assure que TOUS les champs sont PRIVÉS, l'interaction n'est possible que via des récepteurs de méthode définis
    • Je ne passe cette fonction à aucun goroutine

Par exemple:

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
}

Raisons pour lesquelles j'évite les pointeurs:

  • les pointeurs ne sont pas simultanément sûrs (tout l'intérêt de GoLang)
  • une fois récepteur de pointeur, toujours récepteur de pointeur (pour toutes les méthodes de Struct pour la cohérence)
  • les mutex sont sûrement plus chers, plus lents et plus difficiles à entretenir par rapport au "coût de copie de valeur"
  • en parlant de «coût de copie de valeur», est-ce vraiment un problème? L'optimisation prématurée est la racine de tous les maux, vous pouvez toujours ajouter des pointeurs plus tard
  • ça m'oblige directement, consciemment à concevoir de petits Structs
  • les pointeurs peuvent être évités en concevant des fonctions pures avec une intention claire et des E / S évidentes
  • le ramassage des ordures est plus difficile avec des pointeurs, je crois
  • plus facile d'argumenter sur l'encapsulation, les responsabilités
  • restez simple, stupide (oui, les pointeurs peuvent être délicats car vous ne connaissez jamais le développement du prochain projet)
  • les tests unitaires, c'est comme marcher dans un jardin rose (expression slovaque uniquement?), c'est facile
  • no NIL si conditions (NIL peut être passé là où un pointeur était attendu)

Ma règle d'or, écrivez autant de méthodes encapsulées que possible, telles que:

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) 

METTRE À JOUR:

Cette question m'a inspiré à approfondir mes recherches sur le sujet et à écrire un article de blog à ce sujet https://medium.com/gophersland/gopher-vs-object-oriented-golang-4fa62b88c701

Lukas Lukac
la source
J'aime 99% de ce que vous dites ici et je suis tout à fait d'accord avec lui. Cela dit, je me demande si votre exemple est la meilleure façon d'illustrer votre point de vue. TokenCache n'est-il pas essentiellement une carte (de @VonC - "si le récepteur est une carte, une func ou un chan, n'utilisez pas de pointeur vers elle"). Puisque les cartes sont des types de référence, que gagnez-vous en faisant de "Add ()" un récepteur de pointeur? Toutes les copies de TokenCache feront référence à la même carte. Voir ce terrain de jeu Go - play.golang.com/p/Xda1rsGwvhq
Rich
Heureux que nous soyons alignés. Excellent point. En fait, je pense avoir utilisé le pointeur dans cet exemple parce que je l'ai copié à partir d'un projet où le TokenCache gère plus de choses que simplement cette carte. Et si j'utilise un pointeur dans une méthode, je l'utilise dans toutes. Proposez-vous de supprimer le pointeur de cet exemple particulier de SO?
Lukas Lukac
LOL, copier / coller grève à nouveau! 😉 OMI, vous pouvez le laisser tel quel car il illustre un piège dans lequel il est facile de tomber, ou vous pouvez remplacer la carte par quelque chose qui démontre l'état et / ou une grande structure de données.
Riche
Bon, je suis sûr qu'ils liront les commentaires ... PS: Riche, vos arguments semblent raisonnables, ajoutez-moi sur LinkedIn (lien dans mon profil) heureux de vous connecter.
Lukas Lukac