Wskaźniki a wartości parametrów i zwracane wartości

327

W Go istnieją różne sposoby zwracania structwartości lub jej części. Dla indywidualnych widziałem:

type MyStruct struct {
    Val int
}

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

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

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

Rozumiem różnice między nimi. Pierwszy zwraca kopię struktury, drugi wskaźnik do wartości struktury utworzonej w funkcji, trzeci oczekuje, że istniejąca struktura zostanie przekazana i zastępuje wartość.

Widziałem, że wszystkie te wzorce są używane w różnych kontekstach, zastanawiam się, jakie są najlepsze praktyki w odniesieniu do nich. Kiedy użyjesz które? Na przykład pierwszy może być odpowiedni dla małych struktur (ponieważ narzut jest minimalny), drugi dla większych. I trzeci, jeśli chcesz być wyjątkowo wydajnym pod względem pamięci, ponieważ możesz łatwo ponownie użyć jednej instancji struktury między rozmowami. Czy są jakieś najlepsze praktyki dotyczące tego, kiedy stosować?

Podobnie to samo pytanie dotyczące plasterków:

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

Ponownie: jakie są tutaj najlepsze praktyki. Wiem, że plasterki są zawsze wskaźnikami, więc zwrócenie wskaźnika do plasterka nie jest przydatne. Jednak czy powinienem zwrócić kawałek wartości struct, kawałek wskaźników do struktur, czy powinienem przekazać wskaźnik do plasterka jako argument (wzorzec używany w API App Engine )?

Zef Hemel
źródło
1
Jak mówisz, tak naprawdę zależy to od przypadku użycia. Wszystkie są ważne w zależności od sytuacji - czy jest to obiekt zmienny? czy chcemy kopię lub wskaźnik? itp. BTW, o którym nie wspominałeś o użyciu new(MyStruct):) Ale tak naprawdę nie ma różnicy między różnymi metodami przydzielania wskaźników i zwracania ich.
Not_a_Golfer
15
To dosłownie ponad inżynieria. Struktury muszą być dość duże, aby zwracanie wskaźnika przyspieszyło program. Tylko nie zawracaj sobie głowy kodem, profilem, napraw, jeśli jest to przydatne.
Volker
1
Jest tylko jeden sposób na zwrócenie wartości lub wskaźnika, a mianowicie na zwrócenie wartości lub wskaźnika. Sposób ich przydzielania to osobny problem. Użyj tego, co działa w twojej sytuacji, i napisz trochę kodu, zanim się o to martwisz.
JimB
3
BTW właśnie z ciekawości sprawdziłem to. Zwracanie struktur względem wskaźników wydaje się być mniej więcej taką samą prędkością, ale przekazywanie wskaźników do funkcji wzdłuż linii jest znacznie szybsze. Chociaż nie na poziomie, miałoby to znaczenie
Not_a_Golfer
1
@Not_a_Golfer: Zakładam, że po prostu przydzielanie bc odbywa się poza funkcją. Również wartości porównawcze względem wskaźników zależą od wielkości struktur i wzorców dostępu do pamięci po fakcie. Kopiowanie rzeczy o wielkości linii bufora jest tak szybkie, jak to tylko możliwe, a szybkość wskazywania dereferencji z pamięci podręcznej procesora znacznie różni się od usuwania dereferencji z pamięci głównej.
JimB

Odpowiedzi:

392

tl; dr :

  • Metody wykorzystujące wskaźniki odbiornika są powszechne; ogólna zasada dla odbiorników brzmi : „W razie wątpliwości użyj wskaźnika”.
  • Plasterki, mapy, kanały, łańcuchy, wartości funkcji i wartości interfejsu są implementowane wewnętrznie ze wskaźnikami, a wskaźnik do nich jest często nadmiarowy.
  • Gdzie indziej używaj wskaźników dla dużych struktur lub struktur, które musisz zmienić, a w przeciwnym razie przekaż wartości , ponieważ wprowadzanie zmian niespodziewanie za pomocą wskaźnika jest mylące.

Jeden przypadek, w którym powinieneś często używać wskaźnika:

  • Odbiorcy są wskaźnikami częściej niż inne argumenty. Nie jest niczym niezwykłym, że metody modyfikują rzecz, do której są wywoływane, lub że nazwane typy są dużymi strukturami, więc wskazówki są domyślne dla wskaźników, z wyjątkiem rzadkich przypadków.
    • Narzędzie kopiujące Jeffa Hodgesa automatycznie wyszukuje małe odbiorniki przekazywane według wartości.

Niektóre sytuacje, w których nie potrzebujesz wskaźników:

  • Wytyczne przeglądu kodu sugerują przekazywanie małych struktur, takich jak type Point struct { latitude, longitude float64 }, a może nawet nieco większych, wartości, chyba że wywoływana funkcja musi mieć możliwość ich modyfikacji.

    • Semantyka wartości pozwala uniknąć sytuacji aliasingu, w których przypisanie tutaj zmienia wartość z zaskoczenia.
    • Go-y nie poświęca czystej semantyki dla małej prędkości, a czasami przekazywanie małych struktur przez wartość jest w rzeczywistości bardziej wydajne, ponieważ pozwala uniknąć błędów pamięci podręcznej lub alokacji sterty.
    • Tak więc strona komentarzy do recenzji kodu Go Wiki sugeruje przekazywanie wartości, gdy struktury są małe i prawdopodobnie tak pozostaną.
    • Jeśli „duża” granica wydaje się niejasna, to jest; prawdopodobnie wiele struktur znajduje się w zakresie, w którym wskaźnik lub wartość są w porządku. W dolnej części komentarze do przeglądu kodu sugerują, że wycinki (trzy słowa maszynowe) są rozsądne do wykorzystania jako odbiorniki wartości. Gdy coś jest bliżej górnej granicy, bytes.Replacewymaga 10 słów argumentów (trzy plasterki i an int).
  • W przypadku plasterków nie trzeba przekazywać wskaźnika, aby zmieniać elementy tablicy. io.Reader.Read(p []byte)zmienia na przykład bajty p. Prawdopodobnie jest to szczególny przypadek „traktowania małych struktur jak wartości”, ponieważ wewnętrznie przekazujesz małą strukturę zwaną nagłówkiem wycinka (zobacz wyjaśnienie Russa Coxa (rsc) ). Podobnie nie potrzebujesz wskaźnika, aby modyfikować mapę lub komunikować się na kanale .

  • W przypadku plasterków nastąpi ponowne odbarwienie (zmiana początku / długości / pojemności), wbudowane funkcje, takie jak appendakceptowanie wartości plasterka i zwracanie nowej. Naśladowałbym to; unika aliasingu, zwracanie nowego wycinka pomaga zwrócić uwagę na fakt, że nowa tablica może być przydzielona i jest znana dzwoniącym.

    • Podążanie za tym wzorem nie zawsze jest praktyczne. Niektóre narzędzia, takie jak interfejsy baz danych lub serializatory, muszą dołączyć do wycinka, którego typ nie jest znany w czasie kompilacji. Czasami akceptują wskaźnik do wycinka interface{}parametru.
  • Mapy, kanały, ciągi oraz wartości funkcji i interfejsów , takie jak wycinki, są wewnętrznymi odniesieniami lub strukturami, które już zawierają odwołania, więc jeśli próbujesz tylko uniknąć kopiowania podstawowych danych, nie musisz przekazywać do nich wskaźników . (rsc napisał osobny post na temat przechowywania wartości interfejsów ).

    • Nadal konieczne może być przekazanie wskaźników w rzadszym przypadku, gdy chcesz zmodyfikować strukturę dzwoniącego: na przykład flag.StringVarbierze to *stringz tego powodu.

Gdzie używasz wskaźników:

  • Zastanów się, czy twoja funkcja powinna być metodą na dowolnej strukturze, do której potrzebujesz wskaźnika. Ludzie oczekują wielu metod xmodyfikacji x, więc zmodyfikowanie struktury odbiornika może pomóc zminimalizować niespodziankę. Istnieją wytyczne dotyczące tego, kiedy odbiorniki powinny być wskaźnikami.

  • Funkcje, które mają wpływ na ich nie-odbiorcze parametry, powinny to wyjaśnić w godoc, lub jeszcze lepiej, godoc i nazwa (jak reader.WriteTo(writer)).

  • Wspominasz o zaakceptowaniu wskaźnika, aby uniknąć przydziału, umożliwiając ponowne użycie; zmiana interfejsów API w celu ponownego wykorzystania pamięci to optymalizacja, którą opóźnię, dopóki nie okaże się, że alokacje wiążą się z nietrywialnymi kosztami, a następnie poszukałbym sposobu, który nie wymuszałby bardziej skomplikowanego interfejsu API dla wszystkich użytkowników:

    1. Aby uniknąć alokacji, analiza ucieczki Go jest Twoim przyjacielem. Czasami możesz pomóc uniknąć przydziałów sterty, tworząc typy, które można zainicjować za pomocą trywialnego konstruktora, zwykłego literału lub użytecznej wartości zerowej, takiej jak bytes.Buffer.
    2. Rozważ Reset()metodę przywrócenia obiektu do stanu pustego, tak jak oferują niektóre typy stdlib. Użytkownicy, którym nie zależy lub nie mogą zapisać przydziału, nie muszą do niego dzwonić.
    3. Rozważ pisanie metod modyfikacji na miejscu i funkcji tworzenia od zera jako pasujących par, dla wygody: existingUser.LoadFromJSON(json []byte) errormoże być zawinięty NewUserFromJSON(json []byte) (*User, error). Ponownie przesuwa wybór między lenistwem a szczypaniem przydziałów dla poszczególnych rozmówców.
    4. Dzwoniący, którzy chcą odzyskać pamięć, mogą podać sync.Poolpewne szczegóły. Jeśli dana alokacja powoduje duże obciążenie pamięci, masz pewność, że wiesz, kiedy alokacja nie jest już używana i nie masz lepszej optymalizacji, sync.Poolmoże pomóc. (CloudFlare opublikował przydatny (wstępny sync.Pool) post na blogu o recyklingu.)

Na koniec, czy wycinki powinny zawierać wskaźniki: wycinki wartości mogą być przydatne, oszczędzając przydziały i pominięcia pamięci podręcznej. Mogą istnieć blokery:

  • Interfejs API do tworzenia twoich przedmiotów może wymusić na tobie wskaźniki, np. Musisz zadzwonić, NewFoo() *Fooa nie pozwolić, aby Go zainicjował wartość zerową .
  • Pożądane czasy życia przedmiotów mogą nie być takie same. Cały kawałek jest natychmiast uwalniany; jeśli 99% elementów nie jest już użytecznych, ale masz wskaźniki do pozostałych 1%, cała tablica pozostaje przydzielona.
  • Przenoszenie przedmiotów może powodować problemy. W szczególności appendkopiuje elementy, gdy rośnie podstawowa tablica . Wskaźniki, które dostałeś przed appendwskazaniem niewłaściwego miejsca po, kopiowanie może być wolniejsze w przypadku dużych struktur, a na przykład sync.Mutexkopiowanie nie jest dozwolone. Wstaw / usuń na środku i sortuj podobnie przesuwaj elementy.

Ogólnie rzecz biorąc, wycinki wartości mogą mieć sens, jeśli albo dostaniesz wszystkie swoje przedmioty z góry i nie przeniesiesz ich (np. Nie będzie więcej appendpo pierwszej konfiguracji), lub jeśli będziesz je ciągle przenosić, ale jesteś pewien, że to OK (brak / ostrożne użycie wskaźników do elementów, elementy są wystarczająco małe, aby skutecznie kopiować itp.). Czasami musisz przemyśleć lub zmierzyć specyfikę swojej sytuacji, ale jest to przybliżony przewodnik.

twotwotwo
źródło
12
Co oznacza duże struktury? Czy istnieje przykład dużej struktury i małej struktury?
Użytkownik bez kapelusza
1
Jak rozpoznać bajty. Wymiana zajmuje 80 bajtów argumentów na amd64?
Tim Wu
2
Podpis jest Replace(s, old, new []byte, n int) []byte; s, stary i nowy to po trzy słowa ( nagłówki wycinka(ptr, len, cap) ) i n intjedno słowo, więc 10 słów, które przy ośmiu bajtach / słowo to 80 bajtów.
twotwotwo
6
Jak definiujesz duże struktury? Jak duży jest duży?
Andy Aldo,
3
@AndyAldo Żadne z moich źródeł (komentarze recenzji kodu itp.) Nie definiuje progu, więc postanowiłem powiedzieć, że jest to wywołanie osądu zamiast progu. Trzy słowa (jak plasterek) są dość konsekwentnie traktowane jako kwalifikujące się do bycia wartością w stdlib. Znalazłem właśnie wystąpienie odbiornika wartości pięciu słów (text / scanner.Position), ale nie czytałem w nim dużo (jest również przekazywany jako wskaźnik!). Brak testów porównawczych itp. Po prostu zrobiłbym wszystko, co wydaje się najbardziej dogodne dla czytelności.
twotwotwo
10

Trzy główne powody, dla których chcesz używać odbiorników metod jako wskaźników:

  1. „Po pierwsze i najważniejsze, czy metoda wymaga modyfikacji odbiornika? Jeśli tak, odbiornik musi być wskaźnikiem”.

  2. „Drugą kwestią jest rozważenie wydajności. Jeśli odbiornik jest duży, na przykład duża konstrukcja, korzystanie ze wskaźnika odbiornika będzie znacznie tańsze”.

  3. „Następna jest spójność. Jeśli niektóre metody tego typu muszą mieć odbiorniki wskaźnikowe, pozostałe też powinny, więc zestaw metod jest spójny bez względu na sposób użycia typu”

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

Edycja: Inną ważną rzeczą jest poznanie rzeczywistego „typu”, który wysyłasz do funkcji. Typ może być „typem wartości” lub „typem odniesienia”.

Nawet gdy wycinki i mapy działają jako odniesienia, możemy chcieć przekazać je jako wskaźniki w scenariuszach, takich jak zmiana długości wycinka w funkcji.

Santosh Pillai
źródło
1
Dla 2, jaka jest granica? Skąd mam wiedzieć, czy moja struktura jest duża czy mała? Ponadto, czy istnieje struktura, która jest na tyle mała, że bardziej efektywne jest użycie wartości niż wskaźnika (aby nie trzeba było do niej odwoływać się ze sterty)?
zlotnika
Powiedziałbym, że im więcej pól i / lub zagnieżdżonych struktur wewnątrz, tym większa jest struktura. Nie jestem pewien, czy istnieje określony punkt odcięcia lub standardowy sposób, aby wiedzieć, kiedy strukturę można nazwać „dużą” lub „dużą”. Jeśli używam lub tworzę strukturę, wiedziałbym, czy jest ona duża czy mała w oparciu o to, co powiedziałem powyżej. Ale to tylko ja !.
Santosh Pillai
2

Przypadek, w którym zwykle musisz zwrócić wskaźnik, dotyczy budowy instancji jakiegoś zasobowego lub współdzielonego zasobu . Często odbywa się to za pomocą funkcji z prefiksem New.

Ponieważ reprezentują określoną instancję czegoś i mogą potrzebować skoordynować pewne działanie, nie ma sensu generowanie zduplikowanych / kopiowanych struktur reprezentujących ten sam zasób - więc zwrócony wskaźnik działa jak uchwyt do samego zasobu .

Kilka przykładów:

W innych przypadkach zwracane są wskaźniki tylko dlatego, że struktura może być zbyt duża, aby domyślnie ją skopiować:


Alternatywnie, można uniknąć bezpośredniego zwracania wskaźników, zamiast tego zwracając kopię struktury zawierającą wskaźnik wewnętrznie, ale być może nie jest to uważane za idiomatyczne:

nobar
źródło
Z tej analizy wynika, że ​​struktury są domyślnie kopiowane według wartości (ale niekoniecznie ich pośrednich członków).
nobar
2

Jeśli możesz (np. Nieudostępniony zasób, który nie musi być przekazywany jako odniesienie), użyj wartości. Z następujących powodów:

  1. Twój kod będzie ładniejszy i bardziej czytelny, unikając operatorów wskaźników i sprawdzania wartości zerowej.
  2. Twój kod będzie bezpieczniejszy przed paniką Null Pointer.
  3. Twój kod będzie często szybszy: tak, szybciej! Dlaczego?

Powód 1 : przydzielisz mniej przedmiotów na stosie. Przydzielanie / zwalnianie ze stosu jest natychmiastowe, ale przydzielanie / zwalnianie na stercie może być bardzo kosztowne (czas przydzielania + odśmiecanie). Możesz zobaczyć kilka podstawowych liczb tutaj: http://www.macias.info/entry/201802102230_go_values_vs_references.md

Powód 2 : zwłaszcza jeśli przechowujesz zwrócone wartości w plasterkach, twoje obiekty pamięci będą bardziej zwarte w pamięci: zapętlenie plasterka, w którym wszystkie elementy są ciągłe, jest znacznie szybsze niż iteracja plasterka, w którym wszystkie elementy są wskaźnikami do innych części pamięci . Nie dla kroku pośredniego, ale dla zwiększenia braków pamięci podręcznej.

Łamacz mitów : typowa linia pamięci podręcznej x86 ma 64 bajty. Większość struktur jest mniejsza. Czas kopiowania linii pamięci podręcznej jest podobny do kopiowania wskaźnika.

Tylko jeśli krytyczna część twojego kodu jest powolna, spróbowałbym mikrooptymalizacji i sprawdziłbym, czy użycie wskaźników poprawia nieco szybkość, kosztem mniejszej czytelności i łatwości zarządzania.

Mario
źródło