Стоимость приемника и указателя приемника в Golang?


мне очень непонятно, в каком случае я хотел бы использовать приемник значений вместо того, чтобы всегда использовать приемник указателей.
Чтобы резюмировать из документов:

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

The docs говорит также: "для таких типов, как основные типы, срезы и небольшие структуры, приемник значений очень дешев, поэтому, если семантика метода не требует указателя, приемник значений эффективен и понятен."

первый пункт он говорит, что это "очень дешево", но вопрос больше это дешевле, чем приемник указателя. Поэтому я сделал небольшой тест (код по сути) который показал мне, что приемник указателя быстрее даже для структуры, которая имеет только одно строковое поле. Таковы результаты:

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

(Edit: обратите внимание, что второй пункт стал недействительным в новых версиях go, см. комментарии) .
второй пункт он говорит, что это" эффективно и ясно", что больше дело вкуса, не так ли? Сам Я предпочитаю последовательность, используя везде одинаково. Эффективность в каком смысле? с точки зрения производительности кажется, что указатель почти всегда более эффективен. Несколько тестовых запусков с одним свойством int показали минимальное преимущество приемника значений (диапазон 0,01-0,1 НС/ОП)

может кто-нибудь сказать мне случай, когда приемник значения явно имеет больше смысла, чем приемник указателя? Или я делаю что-то неправильно в бенчмарке, я упустил другие факторы?

2 55

2 ответа:

отметим, что в FAQ упоминается согласованность

следующей последовательности. Если некоторые методы типа должны иметь приемники указателей, остальные тоже должны, поэтому набор методов согласован независимо от того, как используется тип. Смотрите на метод setдля сведения.

как уже упоминалось в этой теме:

правило о указателях и значениях для приемников - это значение методы могут вызывается на указатели и значения, но методы указателя могут быть вызваны только по указателям

теперь:

может кто-нибудь сказать мне случай, когда приемник значения явно имеет больше смысла, чем приемник указателя?

The комментарий к обзору кода поможем:

  • если приемник является картой, func или chan, не используйте указатель на него.
  • если приемник является срезом и метод не перекрывает или перераспределяет срез, не использует указатель на него.
  • если метод должен мутировать приемник, приемник должен быть указателем.
  • если получатель является структурой, которая содержит sync.Mutex или аналогичное поле синхронизации, приемник должен быть указателем, чтобы избежать копирования.
  • если приемник является большой структурой или массивом, приемник указателя является более эффективным. Насколько велика эта величина? Предположим, что это эквивалентно передаче всех его элементы в качестве аргументов метода. Если это кажется слишком большим, это также слишком велико для приемника.
  • может ли функция или методы, либо одновременно, либо при вызове из этого метода, мутировать приемник? Тип значения создает копию приемника при вызове метода, поэтому внешние обновления не будут применяться к этому приемнику. Если изменения должны быть видны в исходном приемнике, приемник должен быть указателем.
  • если приемник является структурой, массивом или срез и любой из его элементов является указателем на что-то, что может мутировать, предпочтите приемник указателя, так как это сделает намерение более ясным для читателя.
  • если приемник представляет собой небольшой массив или структуру, которая, естественно, является типом значения (например, что-то вроде time.Time type), без изменяемых полей и указателей, или просто простой базовый тип, такой как int или string,приемник значения имеет смысл.
    значение receiver может уменьшить объем мусора, который может быть создан; если значение передается методу value, вместо выделения в куче можно использовать копию в стеке. (компилятор пытается быть умным, чтобы избежать этого выделения, но это не всегда удается.) По этой причине не выбирайте тип приемника значений без предварительного профилирования.
  • наконец, когда вы сомневаетесь, используйте приемник указателя.

часть, выделенная жирным шрифтом, находится, например, в 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 отличный, информативный ответ.

Я удивлен, что никто не упомянул о стоимости обслуживания, когда проект становится больше, старые разработчики уходят, а новый приходит. Иди, конечно, это молодой язык.

Я использую указатели, когда:

  • работа с большими наборами данных
  • есть структура поддержание состояния, например TokenCache,
    • Я удостоверяюсь, что все поля являются частными, взаимодействие возможно только через определенные приемники метода
    • Я не передаю эту функцию в любой goroutine

например:

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
}

причины, по которым я избегаю указателей:

  • указатели не являются одновременно безопасными (весь смысл GoLang)
  • один раз приемник указателя, всегда приемник указателя (для всех методов структуры для согласованности)
  • мьютексы, безусловно, дороже, медленнее и сложнее поддерживать по сравнению с "стоимостью копирования значения"
  • говоря о" стоимости копирования стоимости", это действительно проблема? Преждевременная оптимизация корень всех зол, вы всегда можете добавить указатели позже
  • это прямо, сознательно заставляет меня проектировать небольшие структуры
  • указатели могут быть в основном избегается путем разработки чистых функций с четким намерением и очевидным вводом / выводом
  • сбор мусора сложнее с указателями я считаю
  • проще спорить об инкапсуляции, ответственности
  • держите его простым, глупым (да, указатели могут быть сложными, потому что вы никогда не знаете, что следующий проект dev)
  • модульное тестирование похоже на прогулку по розовому саду (только словацкое выражение?), значит легко
  • нет нуля, если условия (ноль может быть передан там, где ожидался указатель)

мое эмпирическое правило, напишите как можно больше инкапсулированных методов, таких как:

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) 

обновление:

этот вопрос вдохновил меня исследовать эту тему больше и написать сообщение в блоге об этом https://medium.com/gophersland/gopher-vs-object-oriented-golang-4fa62b88c701