Одной из революционных особенностей 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. Приглашаю всех на бесплатный вебинар, где руководитель курса проведет собеседование выпускника программы. Реальные вопросы, комментарии по ответам, советы. Будет интересно.

Комментарии (30)


  1. miga
    11.01.2023 19:50
    +10

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

    Реальность: программы на го текут пуще прежнего, тонны новых use-after-free/buffer overruns

    /s


    1. SabMakc
      11.01.2023 22:50
      +2

      Ладно утечки... Лично меня больше напрягают битые ссылки, которые выявляются только при запуске со специальными флагами...
      А там и гонки - что быстрее? Переменная прочтется или перезапишется?


      1. miga
        11.01.2023 22:52

        ооо, гонок в голанге и без арен хватает :) ref/mem написан кровью программистов


      1. Vadim_Aleks
        12.01.2023 15:48
        +3

        битые ссылки

        В смысле nil-указатели? Так используйте меньше указателей. Часто вижу []*Object и прочее таскание указателей по всем функциям, из-за чего страдает и программист, и GC)

        По части nil-указателей у меня лишь болит от использования неинициализированной map'ы. Так легко объявить и так легко забыть инициализировать)


        1. SabMakc
          12.01.2023 17:17
          +1

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

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


          1. Vadim_Aleks
            12.01.2023 22:13
            +4

            А как это можно воспроизвести без cgo и unsafe?


            1. muturgan
              13.01.2023 18:21

              С помощью арены?


              1. Vadim_Aleks
                13.01.2023 21:07

                А, речь была про битые ссылки после введения арен, теперь понял посыл


    1. Vadim_Aleks
      12.01.2023 15:35

      программы на го текут пуще прежнего

      А что именно текло: приложения или библиотека? У меня Go-приложения текли только когда неправильно использовался unsafe)


      1. miga
        12.01.2023 16:19
        +2

        Течет не напрямую память (хотя и с ней до того, как начали использовать MADV_FREE были приколы), а горутины, которые тем или иным способом не умирают и коптят на каком-нибудь канале до самой смерти процесса, вместе со всеми своими ресурсами


      1. gudvinr
        13.01.2023 13:24
        +1

        Могут довольно интересно течь.


        Например через горутины, которые не завершаются.


        Ещё если у вас есть массивы и вы их переиспользуете, то у них capacity не меняется, а меняется только длина, соответственно, в памяти такой массив будет занимать много места, т.к. лишние элементы просто скрыты между len и cap и GC их не будет удалять.


  1. danslapman
    11.01.2023 20:44
    +12

    Одной из революционных особенностей Go в сравнении с другими компилируемыми языками стало автоматическое управление освобождением памяти от неиспользуемых объектов

    Вы это серьёзно? Языки с GC были задолго до go


    1. andreyverbin
      11.01.2023 21:11

      Хабр захватили переводчики на окладе :(


    1. Scratch
      11.01.2023 21:20
      +1

      ключевое слово - компилируемыми. Приведёте пример языка, который генерит самодостаточный экзэшник с GC внутри?


      1. danslapman
        11.01.2023 21:31
        +5

        Конечно: Standart ML/OCaml, Haskell, Eiffel, да даже в Ada была ограниченная сборка мусора. Rust, кстати, тоже раньше go начали разрабатывать


        1. Desprit
          11.01.2023 22:29

          А rust здесь каким боком?


          1. danslapman
            11.01.2023 22:36

            RC/ARC можно считать простейшим способом сборки мусора (но конечно же это не полноценный GC в обычном понимании)


            1. Kelbon
              11.01.2023 22:51

              тогда вы явно забыли добавить в список С++, откуда shared_ptr и был скопирован растом


          1. danslapman
            11.01.2023 22:44
            +8

            Давайте, чтобы никогда не смущать, вместо Rust упомяну D :)


      1. SabMakc
        11.01.2023 22:44
        +3

        А причем компиляция и самодостаточность екзешника?
        Java и C# вполне компилируются, пускай и в промежуточное представление.


        1. Free_ze
          12.01.2023 23:40
          +2

          1. hello_my_name_is_dany
            13.01.2023 13:24

            А в новой версии .NET c ограничениями, но можно даже сразу в машинный код


      1. Kelbon
        11.01.2023 22:46
        +3

        Джава компилируемый язык, там гц. И ещё миллиард языков. Хаскель также компилируемый с гц.

        Это не то что не революция, это даунгрейд.

        А приведённый "отличный новый код" это просто ужас, код на С и то лучше чем на go.

        Дизайнеры go буквально сделали сишку с сборкой мусора и без единого плюса сишки


        1. Vadim_Aleks
          12.01.2023 17:12
          +1

          А приведённый "отличный новый код" это просто ужас, код на С и то лучше чем на go.

          Вам не предлагают отныне и навсегда пользоваться arena-аллокаторами и забыть про старый API. Важно понимать, что арена-аллокаторы нужны не всем. Любой низкоуровневый код выглядит уродливо, будь то Java, Haskell, Go, C


  1. akurilov
    12.01.2023 00:15
    +4

    Мне кажется или это превращает го в си?


    1. host13
      12.01.2023 10:13
      +2

      Скорее в object pascal ))


  1. Helltraitor
    12.01.2023 17:33

    Революционные костыли


  1. Arturius92
    12.01.2023 19:14

    Подскажите пожалуйста: если всю динамическую память выделяем через arena, то сборщик мусора тогда вообще не запускается?


    1. Vadim_Aleks
      12.01.2023 21:41
      +1

      В теории, да. Чтобы наверняка, можно установить GOGC=off


    1. Devoter
      13.01.2023 14:30

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