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

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.
image
Иллюстрация, созданная для «A Journey With Go», из оригинального гофера, созданного Рене Френч.

С точки зрения производительности систематическое использование указателей вместо копирования самой структуры для совместного использования структур многим 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 раз быстрее, чем использование указателя на нее!

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

image
график для структуры, переданной копией

image
график для структуры, переданной указателем

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

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

image

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

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

image

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

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

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

Если бенчмарк размещения в стеке не изменился, то показатель в куче уменьшился с 75ns/op до 114ns/op.

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


Мы добавим два пустых метода в нашу структуру и немного адаптируем наши бенчмарки:

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 не всегда хорошо. Чтобы выбрать хорошую семантику для ваших данных, я настоятельно рекомендую прочитать пост о семантике значения/указателя, написанной Биллом Кеннеди. Это даст вам лучшее представление о стратегиях, которые вы можете использовать со своими структурами и встроенными типами. Кроме того, профилирование использования памяти определенно поможет вам понять, что происходит с вашими аллокациями и кучей.
Источник: https://habr.com/ru/post/490570/


Интересные статьи

Интересные статьи

Ручная многоракурсная стереореконструкция биологической нейронной сети занимает десятки тысяч часов. Специально к старту нового потока продвинутого курса «Machine Learning Pro + Deep Lear...
Стартовал второй набор студентов магистерской программы по data science и business intelligence Ozon Masters – а чтобы решиться оставить заявку и пройти онлайн-тестирование было проще, мы расспро...
Принято считать, что персонализация в интернете это магия, которая создается сотнями серверов на основе БигДата и сложного семантического анализа контента.
Вам приходилось сталкиваться с ситуацией, когда сайт или портал Битрикс24 недоступен, потому что на диске неожиданно закончилось место? Да, последний бэкап съел все место на диске в самый неподходящий...
VR-гарнитуры, конечно, делают жизнь интереснее: некоторый контент с ними воспринимается куда интереснее. Даже ролики с Youtube, становясь чуть объемнее, буквально переносят тебя в твой персональн...