[Программирование, Go] Указатель или копия структуры: что выгоднее использовать в Golang?

Автор Сообщение
news_bot ®

Стаж: 6 лет 3 месяца
Сообщений: 27286

Создавать темы news_bot ® написал(а)
07-Авг-2020 17:30

Прямо сейчас в OTUS открыт набор на новый поток курса «Разработчик Golang». В преддверии старта курса делимся с вами авторской статьей от нашего коллеги.
Всем привет! Сегодня я хотел бы поговорить об использовании указателя и копии структуры в Golang, и обсудить, какой из вариантов выгоднее по памяти и по производительности.
Что такое структура?
Структура — это тип, который содержит именованные поля. Да, изначально кажется, что это определение объекта или хеш-таблицы, но в Go это структура. Структуры используются для создания кастомных типов данных, например, при получении привычного набора данных из базы данных.
Если вы хотите немного почитать, что из себя представляют структуры, вы можете сделать это здесь.
Что такое указатель?
Некоторые книги вообще предлагают в качестве объяснения спросить у C программиста, что же такое указатели. На самом деле ничего в них такого сложного нет, указатели просто указывают на участок памяти, где хранится значение. Если вам непонятно, что это такое, можете почитать это в go учебнике
Создание данных для тестирования
Давайте создадим структуру для тестирования:
type S struct {
  a, b, c int64
  d, e, f string
  g, h, i float64
}

Ниже у нас представлено создание копии нашей структуры:
func asCopy() S {
  return S{
    a: 10, b: 20, c: 30,
    e: "data", f: "foo",
    g: 1.0, h: 2.0, i: 3.0,
  }
}

А здесь у нас будет представлено копирование при помощи указателя:
func asPointer() *S {
  return &S{
    a: 10, b: 20, c: 30,
    e: "data", f: "foo",
    g: 1.0, h: 2.0, i: 3.0,
  }
}

Используя вышеперечисленные функции, мы можем написать два бенчмарка, первый будет основан на передаче структуры как копии:
func BenchmarkMemoryStack(b *testing.B){
  var s S
  f, err :=os.Create("stack.out")
  if err != nil{
  panic(err)
}
defer f.Close()
err = trace.Start(f)
if err != nil{
  panic(err)
}
for i:= 0; i < b.N; i++{
  s = byCopy()
}
trace.Stop()
b.StopTimer()
_ = fmt.Sprintf("%v", s.a)
}

Также мы создадим другой бенчмарк, в котором будем передавать структуры как указатель:
func BenchMemoryHeap(b *testing.B) {
   var s *S
   f, err := os.Create("heap.out")
   if err != nil {
      panic(err)
   }
   defer f.Close()
   err = trace.Start(f)
   if err != nil {
      panic(err)
   }
   for i := 0; i < b.N; i++ {
      s = byPointer()
   }
   trace.Stop()
   b.StopTimer()
   _ = fmt.Sprintf("%v", s.a)
}

Давайте запустим наши бенчмарки:
go test ./... -bench=BenchmarkMemoryHeap -benchmem -run=^$ -count=10 > head.txt && benchstat head.txt
go test ./... -bench=BenchmarkMemoryStack -benchmem -run=^$ -count=10 > stack.txt && benchstat stack.txt

Здесь я с приведу статистики, которые у нас получились:
MemoryHeap
name
time/op
MemoryHeap-4
75.0ns ± 5%
name
alloc/op
MemoryHeap-4
96.0B ± 0%
name
allocs/op
MemoryHeap-4
1.00 ± 0%
MemoryStack
name
time/op
MemoryStack-4
8.93ns ± 4%
name
alloc/op
MemoryStack-4
0.00B
name
allocs/op
MemoryStack-4
0.00
Использование копии структуры вместо указателя дает 8-кратный выигрыш в скорости.
Чтобы понять почему, давайте посмотрим на графы, которые были сгенерированы нашим трейсом. Первым пойдет граф, в котором у нас структура сгенерирована как копия:

Потом где структура сгенерирована с помощью указателей:

Первый граф достаточно простой. Там не используется куча, и нет сборщика мусора и дополнительной горутины.
На втором графе, использование указателя заставляет компилятор выкидывать переменную из кучи и направляет в работу наш сборщик мусора. Если мы немного приблизим граф, увидим, что сборщик мусора оказывает значительное влияние на важные части нашего процесса:

Мы можем увидеть, что на этом графе сборщик мусора должен срабатывать каждые 4ms.
Если приблизить опять, мы получим дополнительные детали, что у нас действительно происходит:

Голубая, розовая и красная полоски выражают фазы сборщика мусора, в то время коричневые связаны с выделением кучи (помечено на "runtime.bsweep" на этом графе).
Даже если этот пример покажется вам немного специфичным, он вполне показателен, чтобы дать понять, что стоимость выделения переменной в куче больше, чем в стеке. В нашем примере, код быстрее выделяет структуру в стеке и копирует ее, чем выделяет память в куче и передает адрес.
Если вам не совсем понятно, что из себя представляет стек/куча, но вы хотели бы понимать больше, вы можете прочитать эту замечательнуюстатью на нашем сайте.
Интенсивные вызовы функций
Давайте разберем второй кейс, в котором добавим два пустых метода к нашей структуре с небольшой адаптацией к нашим бенчмаркам.
func (s S) stack(s1 S) {}
func (s *S) heap(s1 *S) {}

Давайте посмотрим на выделение оперативной памяти в нашем стеке, в котором мы создаем структуру и передаем ее как копию:
func BenchMemoryStack(b * testing.B){
  var s S
  var s1 S
  s = byCopy()
  s1 = byCopy()
  for i :=0; i< b.N; i++{
    for i:0; i < 10000; i++{
      s.stack(s1)
    }
  }
}

И бенчмарк для кучи передаст структуру в качестве указателя:
func BenchMemoryHeap(b * testing.B){
  var s *S
  var s1 *S
  s = byPointer()
  s1 = byPointer()
  for i: = 0; i < b.N; i++{
  for i := 0; i < 100000; i++{
  s.heap(s1)
    }
  }
}

Вполне ожидаемо, результаты сейчас достаточно разные:
MemoryHeap
name
time/op
MemoryHeap-4
301µs ± 4%
name
alloc/op
MemoryHeap-4
0.00B
name
allocs/op
MemoryHeap-4
0.00
MemoryStack
name
time/op
MemoryStack-4
595µs ± 2%
name
alloc/op
MemoryStack-4
0.00B
name
allocs/op
MemoryStack-4
0.00
Небольшое заключение
Использование указателя вместо копии структуры в Go не всегда лучшая идея. Для закрепления материала я предлагаю вам прочитать вот этот пост. Он больше вам расскажет про стратегии использования структур и встроенных типов данных в Golang.
Кроме того, профайлинг вашей памяти действительно помогает понять, что происходит с выделением памяти и кучей. Используйте его по мере необходимости.
По традиции, некоторые полезные ссылочки, которые могут вам пригодиться:

Узнать о курсе подробнее.
Читать ещё:

===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_programmirovanie (Программирование), #_go, #_go, #_golang, #_blog_kompanii_otus._onlajnobrazovanie (
Блог компании OTUS. Онлайн-образование
)
, #_programmirovanie (
Программирование
)
, #_go
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 16-Май 14:48
Часовой пояс: UTC + 5