Go 1.20 и арена памяти

b22aa432407824b5cbd01add7de0bc4d

Одной из революционных особенностей Go в сравнении с другими компилируемыми языками стало автоматическое управление освобождением памяти от неиспользуемых объектов (сборка мусора). В то же время она может привести к потере производительности при передаче контроля процессу управления памятью, но альтернативного механизма в Go представлено не было. Начиная с Go 1.20 появляется поддержка экспериментального решения для управления памятью, которое позволяет совместить безопасное выделение динамической памяти и уменьшить влияние интегрированного в скомпилированный код управления памятью на производительность приложения. В этой статье мы рассмотрим основные аспекты использования Memory Arena в Go 1.20.

Для запуска кода будем использовать актуальную на данный момент версию Go 1.20rc2, которая может быть получена из установочного пакета или через go install golang.org/dl/go1.20rc2@latest

Для включения поддержки нового механизма управления памятью добавим переменную окружения:

export GOEXPERIMENT=arenas

теперь для выделения памяти будем использовать новый модуль arena:

package main

import "arena"

type Person struct{
  Lastname string
  Firstname string
}

func main() {
  mem := arena.NewArena()
  defer mem.Free()

  for i:=0; i<10; i++ {
    obj := arena.New[Person](mem)
    print(obj)
  }
}

Как можно увидеть при запуске, адреса объектов будут выделяться последовательно из единой области памяти и (после вызова free) вся выделенная арена будет освобождаться. При правильном использовании это улучшает производительность кода, поскольку для арены не будет вызываться автоматическая сборка мусора. При необходимости копирования данных в обычный heap, может использоваться метод Clone, который создает копию структуры из арены в обычную динамическую память (например, при необходимости возврата результата обработки в основное приложение). Также в арене можно создавать слайсы с указанием начального размера и потенциальной емкости arena.MakeSlice(mem, initial, capacity).

Для выделения памяти на основе типа из reflect также можно использовать новый метод reflect.ArenaNew(mem, typ), который возвращает указатель на объект заданного типа, выделенный в арене, сохраненной в mem.

Обнаруживать ошибки при использовании арены (например, чтение или запись значения в структуру после освобождения арены) можно механизмами go run -asan (Address Sanitizer) или go run -msan (Memory Sanitizer), например:

package main

import "arena"

type Data struct {
  value int32
}

func main() {
  mem := arena.NewArena()
  v := arena.New[Data](mem)
  mem.Free()
  v.value = 1
}

при запуске с asan/msan покажет ошибку некорректного использования указателя после освобождения арены. 

Для хранения строк в арене можно использовать создание области памяти из последовательности байт и копировать в нее содержимое строки, например так:

src := "original"

mem := arena.NewArena()
defer mem.Free()

bs := arena.MakeSlice[byte](mem, len(src), len(src))
copy(bs, src)
str := unsafe.String(&bs[0], len(bs))

Арена также может использоваться не только для хранения структур, но и для примитивных типов данных (или их последовательностей), в этом случае взаимодействие ничем не отличается от работы с указателем на переменную:

package main

import "arena"

func main() {
  mem := arena.NewArena()
  defer mem.Free()
  v := arena.New[int32](mem)
  *v = 10
  println(*v)
}

Аналогично поведение слайсов в арене не отличается от обычных слайсов в Go:

package main

import "arena"

func main() {
  mem := arena.NewArena()
  defer mem.Free()
  v := arena.MakeSlice[int32](mem,50,100)
  v[49] = 10;
  v = append(v, 20)
  println(v[49])		//10
  println(v[50])		//20
  println(len(v))		//51
  println(cap(v))		//100
}

Для обнаружения утечек памяти при использовании арены можно использовать обычные механизмы профилирования в Go (go tool pprof для визуализации сэмплирования выделения памяти, которое может быть сохранено через функции модуля runtime/pprof). С точки зрения выделения памяти работа с ареной похожа на выделение одного блока памяти (который может увеличиваться в размере) и при освобождении арены все выделенные в ней объекты становятся недоступными.

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

Статья подготовлена в преддверии старта курса Golang Developer. Professional. Приглашаю всех на бесплатный вебинар, где руководитель курса проведет собеседование выпускника программы. Реальные вопросы, комментарии по ответам, советы. Будет интересно.

© Habrahabr.ru