Как стать автором
Обновить
890.79
OTUS
Цифровые навыки от ведущих экспертов

Go: стоит ли использовать указатели вместо копий структуры?

Время на прочтение4 мин
Количество просмотров9.6K
Автор оригинала: Vincent Blanchon

Систематическое использование указателей для передачи структур вместо их копирования для многих разработчиков Go кажется лучшим вариантом с точки зрения производительности.

Чтобы понять влияние использования указателя, как альтернативы передачи копии структуры, мы рассмотрим два юзкейса.

Интенсивная аллокация данных

Давайте рассмотрим простой пример, когда вам нужно поделиться структурой, чтобы передать куда-то ее значения:

type S struct {
   a, b, c int64
   d, e, f string
   g, h, i float64
}

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

func byCopy() S {
   return S{
      a: 1, b: 1, c: 1,
      e: "foo", f: "foo",
      g: 1.0, h: 1.0, i: 1.0,
   }
}

func byPointer() *S {
   return &S{
      a: 1, b: 1, c: 1,
      e: "foo", f: "foo",
      g: 1.0, h: 1.0, i: 1.0,
   }
}

На основе этих двух методов мы можем написать 2 теста, в одном из которых будет передаваться копия структуры:

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 BenchmarkMemoryHeap(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

И вот результаты:

name          time/op
MemoryHeap-4  75.0ns ± 5%
name          alloc/op
MemoryHeap-4   96.0B ± 0%
name          allocs/op
MemoryHeap-4    1.00 ± 0%

name           time/op
MemoryStack-4  8.93ns ± 4%
name           alloc/op
MemoryStack-4   0.00B
name           allocs/op
MemoryStack-4    0.00

Использование копии структуры здесь в 8 раз быстрее указателя.

Чтобы понять, почему, давайте взглянем на графики, сгенерированные с помощью trace:

График для структуры, переданной копированием
График для структуры, переданной копированием

График для структуры, переданной копированием

График для структуры, переданной по указателю
График для структуры, переданной по указателю

График для структуры, переданной по указателю

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

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

На этом графике мы видим, что сборщик мусора должен отрабатывать каждые 4 мс.

Если мы увеличим масштаб еще немного, мы сможем получить подробную информацию о том, что именно происходит:

Синий, розовый и красный — это фазы сборки мусора, а коричневый — это выделение памяти в куче (на графике помечено как «runtime.bgsweep»):

Sweeping — это когда восстанавливается память, связанная со значениями в динамической памяти, которые не были помечены как используемые (in-use). Это действие происходит, когда приложение Goroutines пытается выделить новые значения в динамической памяти. Задержка свипинга добавляется к стоимости выделения памяти в куче и не связана ни с какими задержками, связанными со сборкой мусора.

https://www.ardanlabs.com/blog/2018/12/garbage-collection-in-go-part1-semantics.html

Даже если этот пример немного экстремален, мы все-равно можем наблюдать, насколько дорого может обходиться аллокация переменной в куче, а не в стеке. В нашем примере код гораздо быстрее аллоцирует структуру в стеке и копирует ее, нежели размещает ее в куче и передает ее адрес.

Если вы не знакомы со стеком/кучей и хотите больше узнать о внутреннем устройстве каждого из них, вы легко можете найти множество информации по этой теме в интернете (например статья Пола Гриббла).

Ситуация была бы еще хуже, если бы мы ограничили процессор до 1 с GOMAXPROCS = 1:

name        time/op
MemoryHeap  114ns ± 4%
name        alloc/op
MemoryHeap  96.0B ± 0%
name        allocs/op
MemoryHeap   1.00 ± 0%

name         time/op
MemoryStack  8.77ns ± 5%
name         alloc/op
MemoryStack   0.00B
name         allocs/op
MemoryStack    0.00

Если тест с аллокацией в стеке не меняется, то показатели для кучи ухудшились с 75нс/оп до 114нс/оп.

Интенсивные вызовы функций

В этом юзкейсе мы добавим в нашу структуру два пустых метода с небольшой адаптацией для наших тестов:

func (s S) stack(s1 S) {}
func (s *S) heap(s1 *S) {}

Тест с аллокацией в стеке создаст структуру и передаст ее копией:

func BenchmarkMemoryStack(b *testing.B) {
   var s S
   var s1 S
   s = byCopy()
   s1 = byCopy()
   for i := 0; i < b.N; i++ {
      for i := 0; i < 1000000; i++  {
         s.stack(s1)
      }
   }
}

Тест с кучей передаст структуру по указателю:

func BenchmarkMemoryHeap(b *testing.B) {
   var s *S
   var s1 *S
   s = byPointer()
   s1 = byPointer()
   for i := 0; i < b.N; i++ {
      for i := 0; i < 1000000; i++ {
         s.heap(s1)
      }
   }
}

Как и ожидалось, результаты теперь совсем другие:

name          time/op
MemoryHeap-4  301µs ± 4%
name          alloc/op
MemoryHeap-4  0.00B
name          allocs/op
MemoryHeap-4   0.00

name           time/op
MemoryStack-4  595µs ± 2%
name           alloc/op
MemoryStack-4  0.00B
name           allocs/op
MemoryStack-4   0.00

Заключение

Использование указателя в качестве альтернативы копированию структуры в go — не всегда хорошая идея.

Чтобы выбрать хорошую семантику для ваших данных, я настоятельно рекомендую прочитать статью о семантике значения/указателя, написанную Биллом Кеннеди. Это даст вам лучшее представление о стратегиях, которые вы можете использовать со своей структурой и встроенными типами.

Кроме того, профилирование использования памяти определенно поможет вам выяснить, что происходит с вашими аллокациями и вашей кучей.


Материал подготовлен в преддверии старта курса Golang Developer. Professional

Теги:
Хабы:
Всего голосов 13: ↑7 и ↓6+1
Комментарии10

Публикации

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия
Представитель
OTUS