Pointer vs. nilai dalam parameter dan mengembalikan nilai

329

Di Go ada berbagai cara untuk mengembalikan structnilai atau potongannya. Untuk individu yang pernah saya lihat:

type MyStruct struct {
    Val int
}

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

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

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

Saya mengerti perbedaan di antara ini. Yang pertama mengembalikan salinan struct, yang kedua pointer ke nilai struct yang dibuat dalam fungsi, yang ketiga mengharapkan struct yang ada untuk diteruskan dan menimpa nilainya.

Saya telah melihat semua pola ini digunakan dalam berbagai konteks, saya bertanya-tanya apa praktik terbaik mengenai ini. Kapan Anda akan menggunakan yang mana? Misalnya, yang pertama bisa ok untuk struct kecil (karena overhead minimal), yang kedua untuk yang lebih besar. Dan yang ketiga jika Anda ingin menjadi sangat efisien dalam memori, karena Anda dapat dengan mudah menggunakan kembali instance inst tunggal di antara panggilan. Apakah ada praktik terbaik untuk kapan menggunakannya?

Demikian pula, pertanyaan yang sama tentang irisan:

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

Sekali lagi: apa praktik terbaik di sini. Saya tahu irisan selalu pointer, jadi mengembalikan pointer ke irisan tidak berguna. Namun, haruskah saya mengembalikan sepotong nilai struct, sepotong pointer ke struct, haruskah saya meneruskan sebuah pointer ke sebuah slice sebagai argumen (pola yang digunakan dalam Go App Engine API )?

Zef Hemel
sumber
1
Seperti yang Anda katakan, itu benar-benar tergantung pada use case. Semua valid tergantung pada situasinya - apakah ini objek yang bisa berubah? apakah kita ingin salinan atau penunjuk? dll. BTW Anda tidak menyebutkan menggunakan new(MyStruct):) Tapi tidak ada perbedaan antara metode yang berbeda untuk mengalokasikan pointer dan mengembalikannya.
Not_a_Golfer
15
Itu benar-benar over engineering. Structs harus cukup besar sehingga mengembalikan pointer membuat program Anda lebih cepat. Hanya saja, jangan repot-repot, kode, profil, perbaiki jika berguna.
Volker
1
Hanya ada satu cara untuk mengembalikan nilai atau pointer, dan itu adalah untuk mengembalikan nilai, atau pointer. Bagaimana Anda mengalokasikannya adalah masalah terpisah. Gunakan apa yang sesuai dengan situasi Anda, dan tulis beberapa kode sebelum Anda khawatir.
JimB
3
Btw hanya karena penasaran saya bangku ini. Mengembalikan struct vs pointer tampaknya kira-kira kecepatan yang sama, tetapi melewati pointer ke fungsi di telepon secara signifikan lebih cepat. Meskipun tidak pada tingkat itu akan masalah
Not_a_Golfer
1
@Not_a_Golfer: Saya akan menganggap itu hanya alokasi bc dilakukan di luar fungsi. Juga nilai benchmark vs pointer tergantung pada ukuran struct dan pola akses memori setelah fakta. Menyalin ukuran cache-line secepat Anda bisa dapatkan, dan kecepatan pointer dereferencing dari cache CPU jauh berbeda dari dereferencing mereka dari memori utama.
JimB

Jawaban:

392

tl; dr :

  • Metode yang menggunakan pointer penerima adalah umum; aturan praktis untuk penerima adalah , "Jika ragu, gunakan pointer."
  • Irisan, peta, saluran, string, nilai fungsi, dan nilai antarmuka diimplementasikan dengan pointer secara internal, dan sebuah pointer ke mereka seringkali berlebihan.
  • Di tempat lain, gunakan pointer untuk struct besar atau struct Anda harus berubah, dan jika tidak memberikan nilai , karena mengubah hal-hal dengan kejutan melalui pointer membingungkan.

Satu kasus di mana Anda harus sering menggunakan pointer:

  • Penerima adalah penunjuk lebih sering daripada argumen lain. Ini tidak biasa untuk metode untuk memodifikasi hal yang mereka panggil, atau untuk tipe bernama menjadi struct besar, jadi pedomannya adalah default ke pointer kecuali dalam kasus yang jarang terjadi.
    • Alat copyfighter Jeff Hodges ' secara otomatis mencari penerima yang tidak kecil yang dilewati oleh nilai.

Beberapa situasi di mana Anda tidak perlu petunjuk:

  • Pedoman tinjauan kode menyarankan untuk meneruskan struct kecil seperti type Point struct { latitude, longitude float64 }, dan mungkin bahkan hal-hal yang sedikit lebih besar, sebagai nilai, kecuali fungsi yang Anda panggil harus dapat memodifikasinya.

    • Semantik nilai menghindari situasi aliasing di mana tugas di sini mengubah nilai di sana secara mengejutkan.
    • Bukanlah Go-y untuk mengorbankan semantik yang bersih untuk sedikit kecepatan, dan kadang-kadang melewati struct kecil dengan nilai sebenarnya lebih efisien, karena menghindari kesalahan cache atau menumpuk alokasi.
    • Jadi, halaman komentar ulasan kode Go Wiki menyarankan lewat nilai ketika struct kecil dan cenderung tetap seperti itu.
    • Jika cutoff "besar" tampak kabur, itu adalah; bisa dibilang banyak struct berada dalam kisaran di mana pointer atau nilai OK. Sebagai batas bawah, komentar ulasan kode menyarankan irisan (tiga kata mesin) masuk akal untuk digunakan sebagai penerima nilai. Sebagai sesuatu yang mendekati batas atas, bytes.Replacedibutuhkan args senilai 10 kata (tiga potong dan satu int).
  • Untuk irisan , Anda tidak perlu meneruskan pointer untuk mengubah elemen array. io.Reader.Read(p []byte)mengubah byte p, misalnya. Ini bisa dibilang kasus khusus "perlakukan struct kecil seperti nilai," karena secara internal Anda melewati struktur kecil yang disebut header slice (lihat penjelasan Russ Cox (rsc) ). Demikian pula, Anda tidak perlu pointer untuk mengubah peta atau berkomunikasi di saluran .

  • Untuk irisan, Anda akan memilih ulang (mengubah start / panjang / kapasitas), fungsi bawaan seperti appendmenerima nilai irisan dan mengembalikan yang baru. Saya akan meniru itu; itu menghindari alias, mengembalikan sepotong baru membantu menarik perhatian pada fakta bahwa array baru mungkin dialokasikan, dan itu akrab bagi penelepon.

    • Tidak selalu praktis mengikuti pola itu. Beberapa alat seperti antarmuka basis data atau serializers perlu ditambahkan ke slice yang tipenya tidak diketahui pada waktu kompilasi. Mereka terkadang menerima pointer ke slice di interface{}parameter.
  • Peta, saluran, string, dan nilai fungsi dan antarmuka , seperti irisan, sudah merupakan referensi atau struktur internal yang sudah berisi referensi, jadi jika Anda hanya berusaha menghindari penyalinan data yang mendasarinya, Anda tidak perlu memberikan petunjuk kepada mereka . (rsc menulis posting terpisah tentang bagaimana nilai antarmuka disimpan ).

    • Anda mungkin masih harus meneruskan pointer dalam kasus yang lebih jarang yang ingin Anda ubah struct penelepon: flag.StringVarambil *stringkarena alasan itu, misalnya.

Di mana Anda menggunakan pointer:

  • Pertimbangkan apakah fungsi Anda harus menjadi metode pada struct mana pun yang Anda perlukan pointer. Orang-orang berharap banyak metode xuntuk memodifikasi x, sehingga membuat struct yang dimodifikasi penerima dapat membantu meminimalkan kejutan. Ada pedoman kapan penerima harus menjadi petunjuk.

  • Fungsi-fungsi yang memiliki efek pada params non-penerima mereka harus memperjelas di godoc, atau lebih baik lagi, godoc dan namanya (seperti reader.WriteTo(writer)).

  • Anda menyebutkan menerima pointer untuk menghindari alokasi dengan mengizinkan penggunaan kembali; mengubah API demi penggunaan kembali memori adalah pengoptimalan yang akan saya tunda hingga jelas bahwa alokasi memiliki biaya nontrivial, dan kemudian saya akan mencari cara yang tidak memaksa API yang lebih rumit pada semua pengguna:

    1. Untuk menghindari alokasi, analisis pelarian Go adalah teman Anda. Anda kadang-kadang dapat membantu menghindari alokasi tumpukan dengan membuat jenis yang dapat diinisialisasi dengan konstruktor sepele, literal biasa, atau nilai nol yang bermanfaat seperti bytes.Buffer.
    2. Pertimbangkan Reset()metode untuk mengembalikan objek ke kondisi kosong, seperti beberapa jenis stdlib. Pengguna yang tidak peduli atau tidak dapat menyimpan alokasi tidak harus menyebutnya.
    3. Pertimbangkan menulis metode ubah-di-tempat dan fungsi-fungsi buat-dari-awal sebagai pasangan yang serasi, untuk kenyamanan: existingUser.LoadFromJSON(json []byte) errorbisa dibungkus dengan NewUserFromJSON(json []byte) (*User, error). Sekali lagi, ini mendorong pilihan antara kemalasan dan menjepit alokasi untuk penelepon individu.
    4. Penelepon yang ingin mendaur ulang memori dapat sync.Poolmenangani beberapa detail. Jika alokasi tertentu menciptakan banyak tekanan memori, Anda yakin Anda tahu kapan alokasi tersebut tidak lagi digunakan, dan Anda tidak memiliki optimasi yang lebih baik, sync.Pooldapat membantu. (CloudFlare menerbitkan posting blog bermanfaat (pra- sync.Pool) tentang daur ulang.)

Akhirnya, pada apakah irisan Anda harus dari pointer: irisan nilai dapat bermanfaat, dan menghemat alokasi dan cache Anda. Mungkin ada pemblokir:

  • API untuk membuat item Anda mungkin memaksa pointer pada Anda, misalnya Anda harus menelepon NewFoo() *Foodaripada membiarkan Go menginisialisasi dengan nilai nol .
  • Umur item yang diinginkan mungkin tidak semuanya sama. Seluruh irisan dibebaskan sekaligus; jika 99% item tidak lagi berguna tetapi Anda memiliki pointer ke 1% lainnya, semua array tetap dialokasikan.
  • Memindahkan item di sekitar dapat menyebabkan masalah. Khususnya, appendmenyalin item ketika menumbuhkan array yang mendasarinya . Pointer yang Anda dapatkan sebelum appendtitik ke tempat yang salah setelah itu, penyalinan bisa lebih lambat untuk struct besar, dan untuk misalnya sync.Mutexmenyalin tidak diperbolehkan. Sisipkan / hapus di tengah dan mengurutkan item yang sama.

Secara umum, nilai irisan dapat masuk akal jika Anda mendapatkan semua item di tempat di depan dan tidak memindahkannya (misalnya, tidak ada lagi appendsetelah pengaturan awal), atau jika Anda terus memindahkannya, tetapi Anda yakin itu OK (tidak / hati-hati menggunakan pointer ke item, item cukup kecil untuk menyalin secara efisien, dll.). Kadang-kadang Anda harus memikirkan atau mengukur spesifik situasi Anda, tetapi itu adalah panduan kasar.

twotwotwo
sumber
12
Apa artinya struct besar? Apakah ada contoh struct besar dan struct kecil?
Pengguna tanpa topi
1
Bagaimana Anda tahu bytes.Replace membutuhkan args senilai 80 byte pada amd64?
Tim Wu
2
Tanda tangan adalah Replace(s, old, new []byte, n int) []byte; s, lama, dan baru adalah tiga kata masing-masing ( header slice(ptr, len, cap) ) dan n intmerupakan satu kata, jadi 10 kata, yang pada delapan byte / kata adalah 80 byte.
twotwotwo
6
Bagaimana Anda mendefinisikan struct besar? Seberapa besar?
Andy Aldo
3
@AndyAldo Tak satu pun dari sumber saya (komentar ulasan kode, dll.) Menentukan ambang, jadi saya memutuskan untuk mengatakan itu panggilan penilaian alih-alih menaikkan ambang. Tiga kata (seperti sepotong) cukup konsisten diperlakukan sebagai memenuhi syarat untuk menjadi nilai di stdlib. Saya menemukan contoh dari penerima nilai lima kata tadi (teks / scanner.Posisi) tapi saya tidak akan membaca banyak ke dalamnya (itu juga dilewatkan sebagai pointer!). Tidak ada tolok ukur, dll., Saya hanya akan melakukan apa pun yang tampaknya paling nyaman untuk dibaca.
twotwotwo
10

Tiga alasan utama ketika Anda ingin menggunakan penerima metode sebagai petunjuk:

  1. "Pertama, dan yang paling penting, apakah metode perlu memodifikasi penerima? Jika ya, penerima harus menjadi penunjuk."

  2. "Kedua adalah pertimbangan efisiensi. Jika penerima besar, struct besar misalnya, akan jauh lebih murah menggunakan pointer pointer."

  3. "Berikutnya adalah konsistensi. Jika beberapa metode dari jenis harus memiliki penerima pointer, sisanya harus juga, jadi set metode konsisten terlepas dari bagaimana jenis digunakan"

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

Sunting: Hal penting lainnya adalah mengetahui "tipe" aktual yang Anda kirim berfungsi. Jenisnya dapat berupa 'tipe nilai' atau 'tipe referensi'.

Bahkan ketika irisan dan peta bertindak sebagai referensi, kita mungkin ingin meneruskannya sebagai petunjuk dalam skenario seperti mengubah panjang irisan dalam fungsi.

Santosh Pillai
sumber
1
Untuk 2, apa cutoffnya? Bagaimana saya tahu jika struct saya besar atau kecil? Juga, apakah ada struct yang cukup kecil sehingga lebih efisien untuk menggunakan nilai daripada pointer (sehingga tidak harus dirujuk dari heap)?
zlotnika
Saya akan mengatakan semakin banyak jumlah bidang dan / atau struct bersarang di dalam, semakin besar struct itu. Saya tidak yakin apakah ada cutoff khusus atau cara standar untuk mengetahui kapan struct dapat disebut "besar" atau "besar". Jika saya menggunakan atau membuat struct, saya akan tahu apakah itu besar atau kecil berdasarkan apa yang saya katakan di atas. Tapi itu hanya aku !.
Santosh Pillai
2

Sebuah kasus di mana Anda biasanya perlu mengembalikan pointer adalah ketika membangun sebuah instance dari beberapa sumber daya stateful atau dibagikan . Ini sering dilakukan oleh fungsi yang diawali dengan New.

Karena mereka mewakili contoh spesifik dari sesuatu dan mereka mungkin perlu mengkoordinasikan beberapa kegiatan, itu tidak masuk akal untuk menghasilkan duplikat / menyalin struktur yang mewakili sumber daya yang sama - jadi penunjuk yang kembali bertindak sebagai pegangan untuk sumber daya itu sendiri .

Beberapa contoh:

Dalam kasus lain, pointer dikembalikan hanya karena struktur mungkin terlalu besar untuk disalin secara default:


Atau, mengembalikan pointer secara langsung dapat dihindari dengan mengembalikan salinan struktur yang berisi pointer secara internal, tetapi mungkin ini tidak dianggap idiomatis:

bangsawan
sumber
Tersirat dalam analisis ini adalah bahwa, secara default, struct disalin oleh nilai (tetapi tidak harus anggota tidak langsung mereka).
Nobar
2

Jika Anda bisa (mis. Sumber daya yang tidak dibagi-pakai yang tidak perlu diteruskan sebagai referensi), gunakan nilai. Dengan alasan berikut:

  1. Kode Anda akan lebih bagus dan lebih mudah dibaca, menghindari operator penunjuk dan cek nol.
  2. Kode Anda akan lebih aman dari kepanikan Null Pointer.
  3. Kode Anda akan lebih cepat: ya, lebih cepat! Mengapa?

Alasan 1 : Anda akan mengalokasikan lebih sedikit item dalam tumpukan. Mengalokasikan / membatalkan alokasi dari tumpukan adalah langsung, tetapi mengalokasikan / membatalkan alokasi pada Heap mungkin sangat mahal (waktu alokasi + pengumpulan sampah). Anda dapat melihat beberapa angka dasar di sini: http://www.macias.info/entry/201802102230_go_values_vs_references.md

Alasan 2 : terutama jika Anda menyimpan nilai yang dikembalikan dalam irisan, objek memori Anda akan lebih dipadatkan dalam memori: perulangan irisan di mana semua item bersebelahan jauh lebih cepat daripada mengulangi irisan di mana semua item mengarah ke bagian lain dari memori . Bukan untuk langkah tipuan tetapi untuk peningkatan cache yang meleset.

Mitos breaker : garis cache x86 khas adalah 64 byte. Kebanyakan struct lebih kecil dari itu. Waktu menyalin garis cache di memori mirip dengan menyalin pointer.

Hanya jika bagian penting dari kode Anda lambat, saya akan mencoba beberapa optimasi mikro dan memeriksa apakah menggunakan pointer meningkatkan kecepatan, dengan biaya lebih sedikit keterbacaan dan mantainabilitas.

Mario
sumber