Go sync.Pool

  • Tutorial
Вольный пересказ документации к sync.Pool

Сборщик мусора (далее GC) не постоянно собирает мусор, а через определённые промежутки времени. В случае если ваш код выделяет память под некоторые структуры данных, а потом освобождает их — и так по кругу — это вызывает определённое давление на GC, в том числе заставляет runtime обратиться к ОС для выделения новой памяти. Представьте: выделяем кусок (например []byte), работаем с ним, освобождаем. Пройдёт определённое время, прежде GC "очнётся ото сна" и соберёт этот кусок. Если в это время мы выделим ещё один такой же кусок и уже выделенной у ОС памяти на это не хватит, то приложение будет вынуждено запросить у ОС ещё памяти. По времени приложения запрос памяти у ОС длится вечность. А в это самое время где-то пылится, ждёт своего часа тот прежний "отработанный" кусок.
Что же делать?

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

Создать пул

import(
    "sync"
)

var bytesPool = sync.Pool{
    New: func() interface{} { return []byte{} },
}

/*
В данном примере функция `New` не нужна. Если пул пуст,
и `New` не `nil` - то она будет использована для создания нового
объекта. Его нужно будет преобразовать из `interace{}` - привести
к нужному типу. Смотри ниже - про это есть децл.
*/

Сбросить состояние

// пусть ary у нас []byte определённой длины и ёмкости
ary = ary[:0]
// усекаем len, сохраняем cap

Положить в пул

/*
так или иначе у нас могут оказаться слишком большие куски,
которые в принципе нам не понадобятся (во всяком случае
не часто) - выбросим их; иначе: кусок размером 2048 байт
будет использоваться там где нужно всего 500-800 байт,
при большом количестве это негативно отразится на памяти
- а ведь мы с этим и боремся
*/
const maxCap = 1024

if cap(ary) <= maxCap {
    // кладём в пул куски ограниченного размера
    bytesPool.Put(ary)
}

Взять из пула

nextAry := bytesPool.Get().([]byte)

Пояснение про New

Функция New создаёт пустой []byte{}, да ещё и эти преобразования в interface{} и обратно. В случае с []byte мы скорее всего будем наращивать его с помощью append, что в принципе делает такой подход не выгодным:

  • создание []byte нулевой ёмкости
  • двойное преобразование в interface{} и обратно
  • append всё равно создаст новый кусок
  • append можно скормить nil, только типа []byte (а не interface{})

Гораздо удобней сделать две функции, которые бы занимались всей вознёй с пулом

// получить
func getBytes() (b []byte) {
    ifc := bytesPool.Get()
    if ifc != nil {
        b = ifc.([]byte)
    }
    return
}
// положить
func putBytes(b []byte) {
    if cap(b) <= maxCap {
        b = b[:0] // сброс
        bytesPool.Put(b)
    }
}

Помните

  • sync.Pool не панацея
  • пул горутино-безопасен
  • пул не обязательно освободит данные при первом пробуждении GC, но он может освободить их в любой момент
  • нет возможности определить и установить размер пула
  • нет необходимости заботится о переполнении пула
  • вовсе незачем городить пул везде где ни попади, он создавался как амортизатор при множественном совместном использовании некоторых общих объектов, даже не просто внутри пакета, а даже больше — другими пакетами
  • вероятно у Вас есть или будут ситуации, когда необходимость/возможность помочь GC будет очевидной
  • пул ограниченного размера делается с помощью канала с буфером

Хороший пример использования пула: пакет fmt. Со 109-ой по 150-ую строку.


Only registered users can participate in poll. Log in, please.

Я

  • 19.8%использую sync.Pool успешно32
  • 8.6%пишу свои пулы14
  • 11.1%не нуждаюсь в подобном18
  • 60.5%впервые слышу об этом98
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 6

    0
    Я бы проголосовал за первые два (оба) варианта.
      0
      За пул из канала с буфером — отдельное спасибо.
      Меня вот давно интересует вопрос — а по какому принципу происходит очистка данных из стандартного пула? То есть как он определяет, что вот сейчас наступил момент, когда данных в пуле ровно столько, что надо удалить лишнее? И удаляет ли он при это всё, что есть сейчас в пуле или только какую-то часть?
        +2
        Спасибо за статью. Было бы здорово ещё добавить визуализации работы GC в случае без и с sync.Pool с помощью gcvis

        Что-то вроде такого:
        image.
          0
          Чёт не получилось у меня красивых графиков (c и без). Хотя бенчмарки радуют:
          Benchmark_withoutPool-2   3000000  4209 ns/op  2390 B/op  3 allocs/op
          Benchmark_withPool-2     10000000  1736 ns/op   692 B/op  1 allocs/op
          

          Код и бенч на pastebin.com
          0
          Спасибо!

          sync.Pool не панацея

          Точно! Если можно применить простое не «goroutine-safe» переиспользование – получится быстрее.
            0
            Важный момент — если делать так, как указано в статье, то на каждую запись в пуле будет выделяться 32 байт в куче.

            Замечу, что в официальном примере в пуле хранится (*bytes.Buffer).

            Это все связано с тем, что при преобразовании структур данных типа []byte к interface{} метаданные структуры «убегают» в кучу. Таким образом, в пул надо записывать нечто, что сразу указывает на кучу, например, (* bytes.Buffer). Можно записывать указатели на что-то попроще:

            type myBuffer struct{
                b []byte
            }
            

            Only users with full accounts can post comments. Log in, please.