Nilai penerima vs. penerima penunjuk

108

Sangat tidak jelas bagi saya dalam hal ini saya ingin menggunakan penerima nilai daripada selalu menggunakan penerima penunjuk.
Ringkasan dari dokumen:

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 mengatakan juga "Untuk jenis seperti tipe dasar, iris, dan struct kecil, penerima nilai sangat murah jadi kecuali semantik metode membutuhkan pointer, penerima nilai efisien dan jelas."

Poin pertama dikatakan "sangat murah", tetapi pertanyaannya adalah apakah lebih murah dari pada penerima pointer. Jadi saya membuat patokan kecil (kode pada intinya) yang menunjukkan kepada saya, penerima penunjuk itu lebih cepat bahkan untuk struct yang hanya memiliki satu bidang string. Inilah hasilnya:

// 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: Harap dicatat bahwa poin kedua menjadi tidak valid di versi go yang lebih baru, lihat komentar) .
Poin kedua mengatakan, "efisien dan jelas" yang lebih merupakan masalah selera, bukan? Secara pribadi saya lebih suka konsistensi dengan menggunakan cara yang sama di mana-mana. Efisiensi dalam arti apa? kinerja bijaksana tampaknya penunjuk hampir selalu lebih efisien. Beberapa uji coba dengan satu properti int menunjukkan keuntungan minimal penerima Nilai (kisaran 0,01-0,1 ns / op)

Dapatkah seseorang memberi tahu saya kasus di mana penerima nilai jelas lebih masuk akal daripada penerima penunjuk? Atau apakah saya melakukan sesuatu yang salah dalam benchmark, apakah saya mengabaikan faktor-faktor lain?

Chrisport
sumber
3
Saya menjalankan benchmark serupa dengan bidang string tunggal dan juga dengan dua bidang: bidang string dan int. Saya mendapatkan hasil yang lebih cepat dari penerima nilai. BenchmarkChangePointerReceiver-4 10000000000 0,99 ns / op BenchmarkChangeItValueReceiver-4 10000000000 0,33 ns / op Ini menggunakan Go 1.8. Saya ingin tahu apakah ada pengoptimalan compiler yang dibuat sejak terakhir kali Anda menjalankan benchmark. Lihat inti untuk lebih jelasnya.
pbitty
2
Kamu benar. Menjalankan benchmark asli saya menggunakan Go1.9, saya juga mendapatkan hasil yang berbeda sekarang. Pointer Receiver 0,60 ns / op, Nilai penerima 0,38 ns / op
Chrisport

Jawaban:

118

Perhatikan bahwa FAQ memang menyebutkan konsistensi

Berikutnya adalah konsistensi. Jika beberapa metode tipe harus memiliki penerima pointer, sisanya juga harus, sehingga kumpulan metode konsisten terlepas dari bagaimana tipe digunakan. Lihat bagian tentang kumpulan metode untuk detailnya.

Seperti yang disebutkan di utas ini :

Aturan tentang pointer vs. nilai untuk penerima adalah bahwa metode nilai dapat dipanggil pada pointer dan nilai, tetapi metode pointer hanya dapat dipanggil pada pointer

Sekarang:

Dapatkah seseorang memberi tahu saya kasus di mana penerima nilai jelas lebih masuk akal daripada penerima penunjuk?

The Kode Ulasan komentar dapat membantu:

  • Jika penerima adalah peta, func atau chan, jangan gunakan penunjuk ke sana.
  • Jika penerima adalah potongan dan metode tidak menggeser atau mengalokasikan kembali potongan tersebut, jangan gunakan penunjuk ke sana.
  • Jika metode perlu mengubah penerima, penerima harus menjadi penunjuk.
  • Jika penerima adalah sebuah struct yang berisi sync.Mutexbidang sinkronisasi serupa, penerima harus menjadi penunjuk untuk menghindari penyalinan.
  • Jika penerima adalah struct atau array yang besar, penerima pointer lebih efisien. Seberapa besar? Asumsikan itu setara dengan meneruskan semua elemennya sebagai argumen ke metode. Jika dirasa terlalu besar, itu juga terlalu besar untuk receiver.
  • Dapatkah fungsi atau metode, baik secara bersamaan atau ketika dipanggil dari metode ini, mengubah penerima? Tipe nilai membuat salinan penerima saat metode dipanggil, jadi pembaruan luar tidak akan diterapkan ke penerima ini. Jika perubahan harus terlihat di penerima asli, penerima harus menjadi penunjuk.
  • Jika penerima adalah struct, array atau slice dan salah satu elemennya adalah penunjuk ke sesuatu yang mungkin bermutasi, pilih penerima penunjuk, karena akan membuat maksud lebih jelas bagi pembaca.
  • Jika penerima adalah larik kecil atau struct yang secara alami merupakan tipe nilai (misalnya, sesuatu seperti time.Timetipe), tanpa bidang yang bisa berubah dan tanpa petunjuk, atau hanya tipe dasar sederhana seperti int atau string, penerima nilai membuat rasa .
    Penerima nilai dapat mengurangi jumlah sampah yang dapat dihasilkan; jika nilai diteruskan ke metode nilai, salinan on-stack dapat digunakan daripada mengalokasikan di heap. (Kompilator mencoba untuk berhati-hati dalam menghindari alokasi ini, tetapi tidak selalu berhasil.) Jangan memilih tipe penerima nilai karena alasan ini tanpa membuat profil terlebih dahulu.
  • Terakhir, jika ragu, gunakan penerima penunjuk.

Bagian yang dicetak tebal ditemukan misalnya dalam 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
sumber
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 Sebenarnya tidak benar. Metode penerima nilai dan penerima pointer dapat dipanggil pada pointer atau non-pointer yang diketik dengan benar. Terlepas dari apa metode dipanggil, di dalam tubuh metode, pengenal penerima merujuk ke nilai by-copy ketika penerima nilai digunakan, dan penunjuk ketika penerima pointer digunakan: Lihat play.golang.org/p / 3WHGaAbURM
Hart Simha
3
Ada penjelasan bagus di sini "Jika x dapat dialamatkan dan set metode & x berisi m, xm () adalah singkatan dari (& x) .m ()."
tera
@tera Ya: itu dibahas di stackoverflow.com/q/43953187/6309
VonC
4
Jawaban yang bagus tetapi saya sangat tidak setuju dengan poin ini: "karena akan membuat niat lebih jelas", TIDAK, API yang bersih, X sebagai argumen dan Y sebagai nilai balik adalah niat yang jelas. Meneruskan Struct demi pointer dan menghabiskan waktu membaca kode dengan cermat untuk memeriksa semua atribut yang dimodifikasi masih jauh dari jelas, dapat dipelihara.
Lukas Lukac
@HartSimha Saya pikir posting di atas menunjukkan fakta bahwa metode penerima pointer tidak dalam "set metode" untuk jenis nilai. Dalam bermain Anda terkait, menambahkan baris berikut akan menghasilkan error kompilasi: Int(5).increment_by_one_ptr(). Demikian pula, sifat yang mendefinisikan metode increment_by_one_ptrtidak akan puas dengan nilai tipe Int.
Gaurav Agarwal
16

Untuk menambahkan tambahan ke @VonC jawaban yang bagus dan informatif.

Saya terkejut tidak ada yang benar-benar menyebutkan biaya pemeliharaan setelah proyek menjadi lebih besar, pengembang lama pergi dan yang baru datang. Go pasti adalah bahasa yang masih muda.

Secara umum, saya mencoba menghindari petunjuk ketika saya bisa tetapi mereka memiliki tempat dan keindahannya sendiri.

Saya menggunakan pointer ketika:

  • bekerja dengan kumpulan data besar
  • memiliki status pemeliharaan struct, misalnya TokenCache,
    • Saya memastikan SEMUA bidang PRIVATE, interaksi hanya mungkin melalui penerima metode yang ditentukan
    • Saya tidak meneruskan fungsi ini ke goroutine mana pun

Misalnya:

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
}

Alasan mengapa saya menghindari petunjuk:

  • pointer tidak aman secara bersamaan (inti dari GoLang)
  • sekali penerima penunjuk, selalu penunjuk penerima (untuk semua metode Struct untuk konsistensi)
  • mutex pasti lebih mahal, lebih lambat dan lebih sulit untuk dipertahankan dibandingkan dengan "biaya copy nilai"
  • berbicara tentang "nilai copy cost", apakah itu benar-benar menjadi masalah? Pengoptimalan prematur adalah akar dari semua kejahatan, Anda selalu dapat menambahkan petunjuk nanti
  • secara langsung, secara sadar memaksa saya untuk mendesain Struktur kecil
  • pointer sebagian besar dapat dihindari dengan merancang fungsi murni dengan maksud yang jelas dan I / O yang jelas
  • pengumpulan sampah lebih sulit dengan petunjuk yang saya percaya
  • lebih mudah untuk berdebat tentang enkapsulasi, tanggung jawab
  • tetap sederhana, bodoh (ya, petunjuk bisa jadi rumit karena Anda tidak pernah tahu pengembang proyek berikutnya)
  • pengujian unit seperti berjalan melalui taman merah muda (hanya ekspresi slovak?), berarti mudah
  • tidak ada NIL jika kondisi (NIL dapat dilalui di mana penunjuk diharapkan)

Aturan praktis saya, tulis sebanyak mungkin metode yang dienkapsulasi seperti:

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) 

MEMPERBARUI:

Pertanyaan ini menginspirasi saya untuk meneliti topik lebih lanjut dan menulis posting blog tentangnya https://medium.com/gophersland/gopher-vs-object-oriented-golang-4fa62b88c701

Lukas Lukac
sumber
Saya suka 99% dari apa yang Anda katakan di sini dan sangat setuju dengannya. Yang mengatakan saya ingin tahu apakah contoh Anda adalah cara terbaik untuk mengilustrasikan maksud Anda. Bukankah TokenCache pada dasarnya adalah sebuah peta (dari @VonC - "jika penerima adalah peta, func atau chan, jangan gunakan penunjuk ke sana"). Karena peta adalah tipe referensi, apa yang Anda peroleh dengan membuat "Add ()" sebagai penerima pointer? Setiap salinan TokenCache akan mereferensikan peta yang sama. Lihat taman bermain Go ini - play.golang.com/p/Xda1rsGwvhq
Kaya
Senang kita sejajar. Poin yang bagus. Sebenarnya, saya pikir saya telah menggunakan pointer dalam contoh ini karena saya menyalinnya dari proyek di mana TokenCache menangani lebih banyak barang daripada hanya peta itu. Dan jika saya menggunakan penunjuk dalam satu metode, saya menggunakannya di semua metode. Apakah Anda menyarankan untuk menghapus penunjuk dari contoh SO khusus ini?
Lukas Lukac
LOL, salin / tempel teguran lagi! 😉 IMO Anda dapat membiarkannya apa adanya karena ini menggambarkan jebakan yang mudah jatuh, atau Anda dapat mengganti peta dengan sesuatu yang menunjukkan status dan / atau struktur data yang besar.
Rich
Yah, saya yakin mereka akan membaca komentar ... NB: Kaya, argumen Anda sepertinya masuk akal, tambahkan saya di LinkedIn (tautan di profil saya) senang terhubung.
Lukas Lukac