Покажчики проти значень параметрів та повернених значень


328

У Go є різні способи повернути structзначення або його фрагмент. Для окремих я бачив:

type MyStruct struct {
    Val int
}

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

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

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

Я розумію відмінності між ними. Перший повертає копію структури, другий - вказівник на значення структури, створене в рамках функції, третій очікує, що існуюча структура буде передана і перекриє значення.

Я бачив, як усі ці зразки використовуються в різних контекстах, мені цікаво, які найкращі практики стосуються цього. Коли ви б використовували яку? Наприклад, перший може бути нормальним для невеликих конструкцій (оскільки накладні витрати мінімальні), а другий для більших. І третє, якщо ви хочете бути надзвичайно ефективними в пам'яті, оскільки ви можете легко використовувати один екземпляр структури між викликами. Чи є найкращі практики, коли використовувати?

Аналогічно те саме питання щодо скибочок:

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

Знову: які найкращі практики тут. Я знаю, що фрагменти завжди є вказівниками, тому повернення вказівника на фрагмент не корисне. Однак, якщо я повинен повернути фрагмент структурних значень, фрагмент покажчиків на структури, чи повинен я передати вказівник на фрагмент як аргумент (шаблон, який використовується в API API App Engine )?


1
Як ви кажете, це дійсно залежить від випадку використання. Усі дійсні в залежності від ситуації - це об'єкт, що змінюється? ми хочемо копію чи покажчик? тощо. BTW ви не згадали про використання new(MyStruct):) Але насправді немає різниці між різними методами розподілу покажчиків та їх повернення.
Not_a_Golfer

15
Це буквально над технікою. Структури повинні бути досить великими, що повернення покажчика робить вашу програму швидшою. Просто не турбуйте, код, профіль, виправити, якщо це корисно.
Волкер

1
Є лише один спосіб повернути значення або вказівник, а це повернути значення або вказівник. Як ви їх розподіляєте - це окреме питання. Використовуйте те, що підходить для вашої ситуації, і перейдіть, напишіть якийсь код, перш ніж перейматися цим.
JimB

3
До речі, просто з цікавості я поставив це на сполох. Здається, що повертаються структури проти покажчиків приблизно однаковою швидкістю, але передача покажчиків на функції вниз по лініях суттєво швидша. Хоча це не на рівні, це мало б значення
Not_a_Golfer

1
@Not_a_Golfer: Я б припустив, що просто розподіл bc відбувається поза функцією. Також значення порівняльного оцінювання та покажчиків залежать від розміру структури та структури доступу до пам'яті після факту. Копіювання речей розміром із кеш-пам'яті настільки ж швидко, як ви можете отримати, і швидкість перенаправлення покажчиків з кешу процесора значно відрізняється, ніж перенаправлення їх з основної пам'яті.
JimB

Відповіді:


392

tl; dr :

  • Методи, що використовують вказівники приймача, є загальними; правило для приймачів : "Якщо ви сумніваєтесь, використовуйте вказівник".
  • Фрагменти, карти, канали, рядки, значення функцій та значення інтерфейсу реалізуються за допомогою покажчиків всередині, і вказівник на них часто є зайвим.
  • В іншому випадку використовуйте покажчики на великі структури або структури, які вам доведеться змінити, інакше передайте значення , тому що змінити речі несподівано за допомогою вказівника заплутано.

Один випадок, коли вам часто слід використовувати вказівник:

  • Одержувачі - покажчики частіше, ніж інші аргументи. Незвичайно для методів зміни речі, на яку вони покликані, або для названих типів, які є великими структурами, тому вказівки полягають у замовчуванні покажчиків, за винятком рідкісних випадків.
    • Інструмент копіювання файлів Jeff Hodges автоматично здійснює пошук неглибоких приймачів, переданих за значенням.

Деякі ситуації, коли вам не потрібні вказівники:

  • Вказівки щодо перегляду коду пропонують як невеликі структури, як type Point struct { latitude, longitude float64 }, а може бути, і дещо більші, як значення, якщо тільки функція, яку ви викликаєте, не має змоги змінювати їх на місці.

    • Семантична цінність дозволяє уникнути ситуацій зі споживання, коли призначення тут змінюється.
    • Це не Go-y жертвувати чистою семантикою за невелику швидкість, а іноді передача невеликих структур за значенням насправді є більш ефективною, оскільки це дозволяє уникнути пропусків кешу або купівлі.
    • Отже, сторінка коментарів з перегляду коду Go Wiki пропонує проходження за значенням, коли структури невеликі та, ймовірно, залишаться таким.
    • Якщо «велике» обрізання здається розпливчастим, воно є; Можливо, багато структур знаходяться в діапазоні, де або вказівник, або значення. У нижній межі коментарі з перегляду коду дозволяють використовувати фрагменти (три машинні слова) як приймачі значень. Як щось ближче до верхньої межі, bytes.Replaceприймає аргументи вартістю 10 слів (три шматочки і ан int).
  • Для фрагментів вам не потрібно передавати вказівник на зміну елементів масиву. Наприклад, io.Reader.Read(p []byte)змінює байти p. Це, мабуть, особливий випадок "трактувати маленькі структури як значення", оскільки внутрішньо ви обходите невелику структуру, яка називається заголовком фрагмента (див . Пояснення Russ Cox (rsc) ). Так само вам не потрібен вказівник для зміни карти або спілкування на каналі .

  • Для фрагментів ви будете перерозподіляти (змінити старт / довжину / ємність), вбудовані функції, такі як appendприйняття значення зрізу та повернення нового. Я би наслідував це; це дозволяє уникнути псевдоніму, повернення нового фрагмента допомагає звернути увагу на те, що може бути виділений новий масив, і він знайомий абонентам.

    • Це не завжди практично, керуючись цією схемою. Деякі інструменти, такі як інтерфейси бази даних або серіалізатори, повинні додавати фрагмент, тип якого не відомий під час компіляції. Вони іноді приймають вказівник на фрагмент у interface{}параметрі.
  • Карти, канали, рядки та значення функцій та інтерфейсу , як фрагменти, є внутрішніми посиланнями або структурами, які вже містять посилання, тому якщо ви просто намагаєтесь не скопіювати базові дані, вам не потрібно передавати покажчики на них . (rsc написав окремий пост про те, як зберігаються значення інтерфейсу ).

    • Вам все ж може знадобитися передати покажчики в тому випадку, коли ви бажаєте змінити структуру абонента: flag.StringVarбере, наприклад, з *stringцієї причини.

Де ви використовуєте вказівники:

  • Поміркуйте, чи повинна ваша функція бути методом на тій структурі, на яку вам потрібен вказівник. Люди очікують, що багато способів xзмінити x, тому зробити модифіковану структуру приймачем може допомогти мінімізувати здивування. Існують вказівки щодо того, коли приймачі повинні бути покажчиками.

  • Функції, які впливають на їх парами, що не приймають, повинні робити це чітким у знаку godoc, а ще краще - у godoc та в назві (як reader.WriteTo(writer)).

  • Ви згадуєте прийняття покажчика, щоб уникнути виділень, дозволяючи повторне використання; зміна API для повторного використання пам’яті - це оптимізація, яку я затримаю, поки не стане зрозумілим, що в асигнуваннях є нетривіальна вартість, і тоді я б шукав спосіб, який не примушує API більш складного для всіх користувачів:

    1. Щоб уникнути асигнувань, ваш друг знайомий аналіз Go . Іноді ви можете допомогти уникнути розподілу купи, створивши типи, які можна ініціалізувати тривіальним конструктором, простим літералом або корисним нульовим значенням bytes.Buffer.
    2. Розглянемо Reset()метод повернення об'єкта в порожній стан, як це пропонують деякі типи stdlib. Користувачам, які не хвилюються або не можуть зберегти розподіл, не потрібно його викликати.
    3. Розгляньте можливість написання змін на місці та функцій створення з нуля як сумісних пар для зручності: existingUser.LoadFromJSON(json []byte) errorможе бути обгорнуто NewUserFromJSON(json []byte) (*User, error). Знову ж таки, це підштовхує вибір між лінню та прищипуючими виділеннями індивідуального абоненту.
    4. Абоненти, які прагнуть переробити пам'ять, можуть дозволити sync.Poolобробляти деякі деталі. Якщо певний розподіл створює великий тиск пам'яті, ви впевнені, що знаєте, коли аллока більше не використовується, і у вас немає кращої оптимізації, яка sync.Poolможе допомогти. (CloudFlare опублікував корисну (до sync.Pool) публікацію в блозі про переробку.)

Нарешті, про те, чи повинні ваші фрагменти бути покажчиками: фрагменти значень можуть бути корисними та заощадити розподіли та пропуски кешу. Можуть бути блокатори:

  • API для створення ваших елементів може змусити покажчики на вас, наприклад, вам потрібно зателефонувати, NewFoo() *Fooа не дозволити Go ініціалізуватися з нульовим значенням .
  • Бажаний термін служби елементів може бути не однаковим. Весь зріз звільняється відразу; якщо 99% елементів більше не корисні, але у вас є вказівники на інші 1%, весь масив залишається виділеним.
  • Переміщення предметів може спричинити проблеми. Зокрема, appendкопіює елементи, коли вона зростає базовий масив . Покажчики, які ви отримали перед appendточкою на неправильне місце після, копіювання може бути повільнішим для величезних структур, а наприклад, sync.Mutexкопіювання заборонено. Вставте / видаліть посередині і сортуючи аналогічно переміщення елементів.

Загалом, зрізи значень можуть мати сенс, якщо або ви поставите всі свої предмети на передній план, і не переміщуйте їх (наприклад, більше append, ніж після початкового налаштування), або якщо ви продовжуєте їх переміщувати, але ви впевнені, що це Гаразд (відсутність / обережне використання покажчиків на елементи, елементи досить малі, щоб ефективно копіювати тощо). Іноді доводиться думати або вимірювати специфіку своєї ситуації, але це орієнтовний посібник.


12
Що означає великі структури? Чи є приклад великої структури та малої структури?
Користувач без шапки

1
Як ви можете сказати байти. Замініть аргументи на 80 байт на amd64?
Тім Ву

2
Підпис є Replace(s, old, new []byte, n int) []byte; s, old і new - це три слова кожен ( заголовки фрагментів є(ptr, len, cap) ) і n intце одне слово, тож 10 слів, що у восьми байтах / слово становить 80 байт.
twotwotwo

6
Як ви визначаєте великі структури? Наскільки великий великий?
Енді Альдо

3
@AndyAldo Жодне з моїх джерел (коментарі щодо перегляду коду тощо) не визначає поріг, тому я вирішив сказати, що це виклик рішення, а не підвищення порогового рівня. Три слова (як фрагмент) досить послідовно трактуються як такі, що мають значення в stdlib. Я просто знайшов екземпляр п'ятисловного приймача значення (текст / сканер.Позиція), але я не дуже читав це (він також передається як вказівник!). Відсутність орієнтирів тощо, я б просто робив все, що здається найзручнішим для читання.
twotwotwo

10

Три основні причини, коли ви хочете використовувати приймачі методів як покажчики:

  1. "По-перше, і найголовніше, чи потрібен метод модифікувати приймач? Якщо це так, приймач повинен бути вказівником."

  2. "По-друге, це врахування ефективності. Якщо приймач великий, наприклад, велика структура, буде набагато дешевше використовувати приймач вказівника."

  3. "Далі - послідовність. Якщо деякі з методів типу мають мати приймачі покажчиків, решта також повинна бути, тому набір методів є послідовним незалежно від того, яким типом використовується"

Довідка: https://golang.org/doc/faq#methods_on_values_or_pointers

Редагувати: Ще одна важлива річ - це знати власне "тип", який ви надсилаєте до функції. Тип може бути "типом значення" або "типом посилання".

Навіть тому, що фрагменти та карти діють як посилання, ми можемо захотіти передати їх як покажчики в таких сценаріях, як зміна довжини фрагмента у функції.


1
Для 2, що таке обрізання? Як я можу знати, чи моя структура велика чи мала? Крім того, чи існує така структура, яка є достатньо малою, щоб було більш ефективно використовувати значення, а не покажчик (так що на нього не потрібно посилатися з купи)?
zlotnika

Я б сказав, що чим більше кількість полів та / або вкладених структур всередині, тим більша структура. Я не впевнений, чи є конкретний відрізний або стандартний спосіб дізнатися, коли структуру можна назвати "великою" чи "великою". Якщо я використовую або створюю структуру, я б знав, чи є її великою чи малою залежно від того, що я сказав вище. Але це тільки я !.
Сантош

2

Випадок, коли вам, як правило, потрібно повернути вказівник, це коли створюється екземпляр якогось загальнодоступного або доступного ресурсу . Це часто робиться за допомогою функцій з префіксом New.

Оскільки вони представляють певний екземпляр чогось і їм може знадобитися координувати деяку діяльність, не має великого сенсу створювати дублювані / скопійовані структури, що представляють один і той же ресурс - тому повернутий вказівник виконує функцію обробки самого ресурсу .

Деякі приклади:

В інших випадках покажчики повертаються лише тому, що структура може бути занадто великою для копіювання за замовчуванням:


Крім того, повернення покажчиків безпосередньо можна уникнути, замість того, щоб повернути копію структури, яка містить покажчик внутрішньо, але, можливо, це не вважається ідіоматичним:


Імпліцит цього аналізу полягає в тому, що структури за замовчуванням копіюються за значенням (але не обов'язково їх непрямими членами).
nobar

2

Якщо ви можете (наприклад, загальнодоступний ресурс, який не потрібно передавати як посилання), використовуйте значення. З наступних причин:

  1. Ваш код буде приємнішим і легшим для читання, уникаючи операторів вказівника та нульових перевірок.
  2. Ваш код буде безпечнішим проти паніки Null Pointer.
  3. Ваш код буде часто швидше: так, швидше! Чому?

Причина 1 : ви виділите менше елементів у стеку. Розподіл / розміщення з стека є негайним, але розподіляти / розмовляти на Heap може бути дуже дорого (час розподілу + збирання сміття). Деякі основні номери ви можете побачити тут: http://www.macias.info/entry/201802102230_go_values_vs_references.md

Причина 2 : особливо якщо ви зберігаєте повернені значення у фрагментах, ваші об'єкти пам’яті будуть більш ущільнені в пам’яті: циклічний фрагмент, де всі елементи є суміжними, набагато швидше, ніж ітерація фрагмента, де всі елементи вказують на інші частини пам’яті . Не для кроку непрямості, а для збільшення пропусків кешу.

Міф-вимикач : типова лінія кеша x86 - 64 байти. Більшість конструкцій менші за це. Час копіювання рядка кеша в пам'ять аналогічний копіюванню покажчика.

Тільки якщо критична частина вашого коду повільна, я спробую здійснити мікрооптимізацію та перевірити, чи використання покажчиків дещо покращує швидкість, ціною меншої читабельності та керованості.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.