İşaretçiler ve parametrelerdeki değerler ve dönüş değerleri


328

Go'da bir structdeğeri veya dilimini döndürmenin çeşitli yolları vardır . Bireysel olanlar için gördüm:

type MyStruct struct {
    Val int
}

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

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

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

Bunlar arasındaki farkları anlıyorum. Birincisi yapının bir kopyasını döndürür, ikincisi işlev içinde oluşturulan yapı değerine bir işaretçi, üçüncüsü var olan bir yapının aktarılmasını bekler ve değeri geçersiz kılar.

Tüm bu modellerin çeşitli bağlamlarda kullanıldığını gördüm, bunlarla ilgili en iyi uygulamaların ne olduğunu merak ediyorum. Hangisini ne zaman kullanırsın? Örneğin, birincisi küçük yapılar için iyi olabilir (çünkü havai minimum), ikincisi daha büyük olanlar için. Ve üçüncüsü, bellekte son derece verimli olmak istiyorsanız, çağrılar arasında tek bir yapı örneğini kolayca yeniden kullanabilirsiniz. Hangisini kullanacağınız için en iyi uygulamalar var mı?

Benzer şekilde, dilimlerle ilgili aynı soru:

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

Tekrar: burada en iyi uygulamalar nelerdir. Dilimleri her zaman işaretçiler olduğunu biliyorum, bu yüzden bir dilime bir işaretçi döndürmek yararlı değildir. Ancak, bir dilim yapı değerleri, bir dilim işaretçi yapıları döndürmek gerekir, bir işaretçiyi bir dilim argüman olarak ( Go App Engine API kullanılan desen) iletmek gerekir ?


1
Dediğiniz gibi, bu gerçekten kullanım durumuna bağlıdır. Her şey duruma bağlı olarak geçerlidir - bu değiştirilebilir bir nesne midir? bir kopya ya da işaretçi istiyor muyuz? vb BTW kullanarak bahsetmedim new(MyStruct):) Ama gerçekten işaretçi ayırma ve onları döndürme farklı yöntemler arasında hiçbir fark yoktur.
Not_a_Golfer

15
Bu tam anlamıyla mühendislik üzerindedir. Yapılar bir işaretçi döndürmenin programınızı daha hızlı hale getirmesi için oldukça büyük olmalıdır. Sadece rahatsız etmeyin, kod, profil, yararlı ise düzeltmeyin.
Volker

1
Bir değeri veya bir işaretçiyi döndürmenin yalnızca bir yolu vardır ve bu bir değeri veya işaretçiyi döndürmektir. Bunları nasıl ayırdığınız ayrı bir konudur. Durumunuz için uygun olanı kullanın ve endişelenmeden önce bir kod yazın.
JimB

3
BTW sadece meraktan benchamrked bunu. Dönen yapıların ve işaretçilerin karşılaştırması hemen hemen aynı hızda gibi gözükmektedir, ancak işaretçilerin çizgilerin altından işlevlere geçirilmesi çok daha hızlıdır. Bir düzeyde olmasa da önemli olacaktır
Not_a_Golfer

1
@ Not_a_Golfer: Bunun bc tahsisinin fonksiyonun dışında yapıldığını varsayarım. Ayrıca değerleri işaretçilerle karşılaştırmak da yapının boyutuna ve bundan sonraki bellek erişim modellerine bağlıdır. Önbellek boyutundaki şeyleri kopyalamak, alabileceğiniz kadar hızlıdır ve CPU önbelleğindeki kayıt silme işaretçilerin hızı, onları ana bellekten kayıttan çıkarmadan çok farklıdır.
JimB

Yanıtlar:


392

tl; dr :

  • Alıcı işaretçileri kullanan yöntemler yaygındır; alıcılar için temel kural şudur : "Şüpheniz varsa, bir işaretçi kullanın."
  • Dilimler, haritalar, kanallar, dizeler, işlev değerleri ve arabirim değerleri dahili işaretçilerle uygulanır ve bunlara bir işaretçi genellikle gereksizdir.
  • Başka yerlerde, değiştirmek zorunda kalacağınız büyük yapılar veya yapılar için işaretçiler kullanın ve aksi takdirde değerleri iletin , çünkü şeyleri bir işaretçi aracılığıyla sürprizle değiştirmek kafa karıştırıcıdır.

Sıklıkla bir işaretçi kullanmanız gereken bir durum:

  • Alıcılar diğer argümanlardan daha sık işaretçilerdir. Yöntemlerin çağrıldıkları şeyi değiştirme veya adlandırılmış türlerin büyük yapılar olması alışılmadık bir durum değildir, bu nedenle rehberlik, nadir durumlar dışında varsayılan olarak işaretçiler içindir.
    • Jeff Hodges'in copyfighter aracı, değere göre aktarılan küçük olmayan alıcıları otomatik olarak arar.

İşaretçilere ihtiyacınız olmayan bazı durumlar:

  • Kod inceleme yönergeleri, aradığınız işlevin yerinde değiştirilebilmesi gerekmedikçe, değerler gibi küçük yapılarıntype Point struct { latitude, longitude float64 } ve hatta belki de biraz daha büyük şeylerin geçirilmesini önerir .

    • Değer semantiği, buradaki bir ödevin orada bir değeri sürpriz bir şekilde değiştirdiği örtüşme durumlarından kaçınır.
    • Temiz anlambilimi biraz hızdan feda etmek Go-y değildir ve bazen küçük yapıları değere göre geçirmek aslında daha verimlidir, çünkü önbellek hatalarını veya yığın ayırmalarını önler .
    • Bu nedenle, Go Wiki'nin kod inceleme yorumları sayfası, yapılar küçük ve bu şekilde kalma olasılığı yüksek olduğunda değere göre geçmeyi önerir.
    • Eğer "büyük" kesme belirsiz görünüyorsa; tartışmasız birçok yapı, bir işaretçinin veya değerin uygun olduğu bir aralıktadır. Alt sınır olarak, kod inceleme yorumları dilimlerin (üç makine kelimesi) değer alıcıları olarak kullanılmasının makul olduğunu göstermektedir. Bir üst sınıra daha yakın bir şey olarak, bytes.Replace10 kelime değerinde argüman alır (üç dilim ve bir int).
  • İçin dilimleri , sen dizinin değişim elemanlarına bir işaretçi geçmesi gerekmez. örneğin io.Reader.Read(p []byte)baytlarını değiştirir p. Tartışmalı bir şekilde, "değerler gibi küçük yapıları tedavi et" özel bir durumudur, çünkü dahili olarak bir dilim başlığı adı verilen küçük bir yapıdan geçiyorsunuz (bkz. Russ Cox (rsc) 'nin açıklaması ). Benzer şekilde, bir haritayı değiştirmek veya bir kanalda iletişim kurmak için bir işaretçiye ihtiyacınız yoktur .

  • Dilimler için yeniden dilimleyeceksiniz (başlangıç ​​/ uzunluk / kapasitesini değiştirin), appendbir dilim değerini kabul etme ve yeni bir değer döndürme gibi yerleşik işlevler . Bunu taklit ederdim; takma addan kaçınır, yeni bir dilim döndürmek, yeni bir dizinin tahsis edilebileceğine dikkat çekmeye yardımcı olur ve arayanlar için tanıdıktır.

    • Bu modeli takip etmek her zaman pratik değildir. Veritabanı arabirimleri veya serileştiriciler gibi bazı araçların türü derleme zamanında bilinmeyen bir dilime eklenmelidir. Bazen bir interface{}parametredeki bir dilim için bir işaretçi kabul ederler .
  • Haritalar, kanallar, dizeler ve dilimler gibi işlev ve arayüz değerleri dahili referanslar veya zaten referanslar içeren yapılardır, bu nedenle yalnızca temel verilerin kopyalanmasını önlemek istiyorsanız, bunlara işaretçiler iletmeniz gerekmez. . (rsc arayüz değerlerinin nasıl saklandığına dair ayrı bir yazı yazdı ).

    • Hâlâ istediğiniz nadir durumda işaretçileri geçmesi gerekebilir değiştirmek arayanın yapı: flag.StringVarBir sürer *stringörneğin, o nedenle.

İşaretçileri kullandığınız yer:

  • İşlevinizin, işaretçiye ihtiyacınız olan yapı üzerinde bir yöntem olup olmadığını düşünün. İnsanlar xdeğişiklik xyapmak için birçok yöntem beklemektedir , bu nedenle değiştirilmiş yapıyı alıcının yapmak sürprizleri en aza indirmeye yardımcı olabilir. Alıcıların ne zaman işaretçi olması gerektiğine dair yönergeler vardır .

  • Alıcı olmayan paramleri üzerinde etkisi olan işlevler, godoc'ta ya da daha iyisi, godoc ve adında (örneğin reader.WriteTo(writer)) bunu netleştirmelidir .

  • Yeniden kullanıma izin vererek ayırmalardan kaçınmak için bir işaretçi kabul ettiğinizden bahsediyorsunuz; bellek yeniden kullanımı uğruna API'ları değiştirmek, ayırmaların önemsiz bir maliyete sahip olduğu anlaşılana kadar geciktireceğim bir optimizasyon ve sonra tüm kullanıcılarda daha zor API'yı zorlamayan bir yol ararım:

    1. Tahsislerden kaçınmak için Go'nun kaçış analizi arkadaşınızdır. Bazen önemsiz bir yapıcı, düz bir hazır bilgi veya benzer bir sıfır değeri ile başlatılabilen türler yaparak yığın ayırmalarını önlemeye yardımcı olabilirsiniz bytes.Buffer.
    2. Reset()Bazı stdlib türlerinin sunduğu gibi, bir nesneyi boş duruma geri koymak için bir yöntem düşünün . Bir ayırmayı umursamayan veya kaydedemeyen kullanıcıların onu çağırması gerekmez.
    3. Yöntem yerinde-değiştirme oluşturmak ve baştan itibaren kolaylık uygun çiftleri gibi işlevleri, yazma göz önünde bulundurun: existingUser.LoadFromJSON(json []byte) errorile sanlabilir NewUserFromJSON(json []byte) (*User, error). Yine, bireysel arayana tembellik ve pinching tahsisleri arasında seçim yapar.
    4. Belleği geri dönüştürmek isteyen arayanlar sync.Poolbazı ayrıntıları ele alabilir . Belirli bir ayırma işlemi çok fazla bellek basıncı yaratırsa, ayırmanın artık kullanılmadığını bildiğinizden ve daha iyi bir optimizasyonunuz olmadığından emin sync.Poololabilirsiniz. (CloudFlare, geri dönüşüm hakkında yararlı (ön sync.Pool) bir blog yazısı yayınladı .)

Son olarak, dilimlerinizin işaretçi olup olmayacağı konusunda: değer dilimleri yararlı olabilir ve size ayırmaları ve önbellek özlediklerini kaydedebilir. Engelleyiciler olabilir:

  • Öğelerinizi oluşturmak için kullanılan API sizi işaretçileri zorlayabilir; örneğin, NewFoo() *FooGo'nun sıfır değeriyle başlatılmasına izin vermek yerine aramak zorundasınız .
  • Maddelerin istenen ömürleri aynı olmayabilir. Bütün dilim bir kerede serbest bırakılır; öğelerin% 99'u artık kullanışlı değilse, ancak% 1'ine işaretçileriniz varsa, dizinin tamamı ayrılmış olarak kalır.
  • Öğeleri hareket ettirmek sorunlara neden olabilir. Özellikle, temel diziyi büyüttüğünde appendöğeleri kopyalar . Daha sonra yanlış yeri işaret etmeden önce aldığınız işaretçiler , büyük yapılar için kopyalama daha yavaş olabilir ve örneğin kopyalamaya izin verilmez. Ortaya ekleme / silme ve sıralama, öğeleri benzer şekilde hareket ettirir.appendsync.Mutex

Genel olarak, değer öğeleriniz ya tüm öğelerinizi öne yerleştirir ve onları hareket ettirmezseniz (örn. appendİlk kurulumdan sonra artık s yok ) ya da onları hareket ettirmeye devam ederseniz, ancak bunun Tamam (öğelere işaretçi yok / dikkatli kullanılmıyor, öğeler verimli kopyalanacak kadar küçük, vb.). Bazen durumunuzun özelliklerini düşünmeniz veya ölçmeniz gerekir, ancak bu kaba bir kılavuzdur.


12
Büyük yapılar ne demektir? Büyük bir yapı ve küçük bir yapı örneği var mı?
Şapkası olmayan kullanıcı

1
Amd64'te yer 80 bayt değerinde argüman alır mı?
Tim Wu

2
İmza Replace(s, old, new []byte, n int) []byte; s, old ve new, her biri üç sözcüktür ( dilim başlıkları vardır(ptr, len, cap) ) ve n intbir kelimedir, yani sekiz bayt / sözcükte 80 bayt olan 10 kelimedir.
twotwotwo

6
Büyük yapıları nasıl tanımlarsınız? Ne kadar büyük?
Andy Aldo

3
@AndyAldo Kaynaklarımdan hiçbiri (kod inceleme yorumları vb.) Bir eşik tanımladı, bu yüzden bir eşik yapmak yerine bir karar çağrısı olduğunu söylemeye karar verdim. Üç kelime (bir dilim gibi) oldukça tutarlı bir şekilde stdlib'de bir değer olarak kabul edilir. Şu anda beş kelimelik bir değer alıcısının bir örneğini buldum (metin / tarayıcı.Konum), ancak çok fazla okumazdım (ayrıca bir işaretçi olarak da geçti!). Karşılaştırma ölçütleri yok, sadece okunabilirlik için en uygun olanı yapardım.
twotwotwo

10

Yöntem alıcılarını işaretçi olarak kullanmak istemenizin üç ana nedeni:

  1. "Öncelikle ve en önemlisi, yöntemin alıcıyı değiştirmesi gerekiyor mu? Öyleyse, alıcı bir işaretçi olmalıdır."

  2. "İkincisi verimlilik konusudur. Eğer alıcı büyükse, örneğin büyük bir yapı, bir işaretçi alıcısı kullanmak çok daha ucuz olacaktır."

  3. "Sonraki tutarlılıktır. Tür yöntemlerinden bazılarının işaretçi alıcıları olması gerekiyorsa, geri kalanı da olmalıdır, bu nedenle yöntem kümesi türün nasıl kullanıldığına bakılmaksızın tutarlıdır"

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

Düzenleme: Bir başka önemli şey, işlev için gönderdiğiniz gerçek "türü" bilmek. Tür, bir 'değer türü' veya 'referans türü' olabilir.

Dilimler ve haritalar referans olarak hareket etse bile, bunları işlevdeki dilimin uzunluğunu değiştirmek gibi senaryolarda işaretçi olarak geçirmek isteyebiliriz.


1
2 için, kesme nedir? Yapımın büyük mü küçük mü olduğunu nasıl bilebilirim? Ayrıca, işaretçi yerine bir değer kullanmanın daha verimli olacağı kadar küçük bir yapı var mı (öbekten referans alınması gerekmiyor)?
zlotnika

İçerideki alan ve / veya iç içe yapıların sayısı ne kadar fazlaysa, yapı o kadar büyük olur. Bir yapının ne zaman "büyük" veya "büyük" olarak adlandırılabileceğini bilmenin belirli bir kesimi veya standart bir yolu olup olmadığından emin değilim. Eğer bir yapı kullanırsam veya yaratırsam, büyük ya da küçük olup olmadığını yukarıda söylediklerime dayanarak bilirim. Ama bu sadece benim !.
Santosh Pillai

2

Genel olarak bir işaretçi döndürmeniz gereken bir durum , bazı durum bilgisi olan veya paylaşılabilir kaynakların bir örneğini oluştururken ortaya çıkar . Bu genellikle ön ekli işlevlerle yapılır New.

Bir şeyin belirli bir örneğini temsil ettikleri ve bazı etkinlikleri koordine etmeleri gerekebileceğinden, aynı kaynağı temsil eden kopyalanmış / kopyalanmış yapılar oluşturmak pek mantıklı değildir - bu nedenle döndürülen işaretçi kaynağın kendisinin işleyicisi olarak hareket eder .

Bazı örnekler:

Diğer durumlarda, yalnızca varsayılan olarak yapı kopyalanamayacak kadar büyük olabileceğinden işaretçiler döndürülür:


Alternatif olarak, işaretçileri dahili olarak içeren bir yapının bir kopyasını döndürmek yerine işaretçilerin doğrudan döndürülmesinden kaçınılabilir, ancak belki de deyimsel olarak kabul edilmez:


Bu analizde örtük olan, varsayılan olarak yapıların değere göre kopyalanmasıdır (ancak mutlaka dolaylı üyeleri değil).
nobar

2

Yapabiliyorsanız (örn. Referans olarak geçirilmesi gerekmeyen paylaşılmayan bir kaynak) bir değer kullanın. Aşağıdaki nedenlerle:

  1. Kodunuz daha güzel ve daha okunabilir olacak, işaretçi operatörlerinden ve boş denetimlerden kaçınacaktır.
  2. Kodunuz Boş İşaretçi paniklerine karşı daha güvenli olacaktır.
  3. Kodunuz genellikle daha hızlı olacaktır: evet, daha hızlı! Neden?

Sebep 1 : Yığına daha az öğe dağıtacaksınız. Yığından ayırma / ayırma hemen gerçekleşir, ancak Yığın üzerinde ayırma / ayırma çok pahalı olabilir (ayırma süresi + çöp toplama). Bazı temel sayıları burada görebilirsiniz: http://www.macias.info/entry/201802102230_go_values_vs_references.md

Sebep 2 : özellikle döndürülen değerleri dilimler halinde saklarsanız, bellek nesneleriniz bellekte daha fazla sıkıştırılır: tüm öğelerin bitişik olduğu bir dilimi döngüye almak, tüm öğelerin belleğin diğer bölümlerine işaret ettiği bir dilimi yinelemekten çok daha hızlıdır . Dolaylı adım için değil, önbellek hatalarının artması için.

Efsane kırıcı : Tipik bir x86 önbellek satırı 64 bayttır. Çoğu yapı bundan daha küçüktür. Bellekte bir önbellek satırını kopyalama süresi, işaretçiyi kopyalamaya benzer.

Yalnızca kodunuzun kritik bir kısmı yavaşsa, bazı mikro optimizasyonları dener ve daha az okunabilirlik ve manevra kabiliyeti pahasına, işaretçi kullanmanın hızı biraz artırıp iyileştirmediğini kontrol ederim.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.