- Оригинальный адрес:medium.com/@expocityohyou.V…
- Оригинальный автор:Vincent Blanchon
- Адрес перевода:GitHub.com/вода Мел О/…
- Переводчик: хаки хаки
- Уровень переводчика ограничен, если есть ошибка в переводе или понимании, помогите указать
ℹ️Эта статья основана на версиях Go 1.12 и 1.13 и объясняет эволюцию sync/pool.go между этими двумя версиями.
sync
Пакет предоставляет мощный многоразовый пул экземпляров для снижения нагрузки на сборщик мусора. Перед использованием пакета нам необходимо протестировать приложение до и после использования пула. Это очень важно, так как это может повлиять на производительность, если вы не понимаете, как это работает внутри.
Лимиты пула
Давайте посмотрим на пример, чтобы увидеть, как он выделяет 10 тысяч раз в очень простом контексте:
type Small struct {
a int
}
var pool = sync.Pool{
New: func() interface{} { return new(Small) },
}
//go:noinline
func inc(s *Small) { s.a++ }
func BenchmarkWithoutPool(b *testing.B) {
var s *Small
for i := 0; i < b.N; i++ {
for j := 0; j < 10000; j++ {
s = &Small{ a: 1, }
b.StopTimer(); inc(s); b.StartTimer()
}
}
}
func BenchmarkWithPool(b *testing.B) {
var s *Small
for i := 0; i < b.N; i++ {
for j := 0; j < 10000; j++ {
s = pool.Get().(*Small)
s.a = 1
b.StopTimer(); inc(s); b.StartTimer()
pool.Put(s)
}
}
}
Выше приведены два теста, один без sync.Pool, а другой с:
name time/op alloc/op allocs/op
WithoutPool-8 3.02ms ± 1% 160kB ± 0% 1.05kB ± 1%
WithPool-8 1.36ms ± 6% 1.05kB ± 0% 3.00 ± 0%
Поскольку цикл имеет 10 000 итераций, тест без пула требует 10 000 выделений памяти в куче, в то время как тест с пулом делает только 3 выделения. 3 выделения выполняются пулом, но выделяется только один экземпляр структуры. Пока все выглядит хорошо, использование sync.Pool работает быстрее и потребляет меньше памяти.
Однако в реальном приложении ваш экземпляр может использоваться для выполнения тяжелых задач и выделения большого количества заголовков. В этом случае при увеличении памяти будет срабатывать GC. Мы также можем использовать командуruntime.GC()
чтобы заставить сборщик мусора в тесте имитировать это поведение:runtime.GC()
)
name time/op alloc/op allocs/op
WithoutPool-8 993ms ± 1% 249kB ± 2% 10.9k ± 0%
WithPool-8 1.03s ± 4% 10.6MB ± 0% 31.0k ± 0%
Теперь мы видим, что производительность пула ниже в случае GC, а количество аллокаций и использование памяти также выше. Мы продолжаем копать глубже, почему.
Внутренний рабочий процесс пула
понять глубжеsync/pool.go
Инициализация пакета может помочь с ответом на наш предыдущий вопрос:
func init() {
runtime_registerPoolCleanup(poolCleanup)
}
Он зарегистрирует метод для очистки объекта пула во время выполнения. ГК в файлеruntime/mgc.go
вызовет этот метод:
func gcStart(trigger gcTrigger) {
[...]
// 在开始 GC 前调用 clearpools
clearpools()
Это объясняет, почему производительность ниже при вызове GC. Поскольку объект пула очищается каждый раз при запуске GC.ДокументацияТакже сообщите нам:
Все, что хранится в пуле, может быть автоматически удалено в любое время без предварительного уведомления.
Теперь давайте создадим блок-схему, чтобы понять, как осуществляется управление пулом:
для каждого мы создаемsync.Pool
, go создает соединение с каждым процессором[P]poolLocal
) внутреннего бассейнаpoolLocal
. Структура состоит из двух свойств:private
а такжеshared
. К первому может получить доступ только его владелец (Push и POP не нуждаются ни в каких блокировках), аshared
Свойства могут быть прочитаны любым другим процессором и должны быть защищены от параллелизма. На самом деле, пул — это не просто локальный кеш, он может использоваться любыми потоками/горутинами в нашем приложении.
Go версии 1.13 улучшитсяshared
доступ, а также принесет новый кеш для решения проблем, связанных с сборкой мусора и очисткой пула.
Новый незаблокированный пул и кеш жертвы
Go версии 1.13 будетshared
использовать одинДвусвязный списокpoolChain
Что касается структуры хранения, это изменение снимает блокировки и улучшаетshared
Доступ. Ниже приведеныshared
Новый поток для доступа:
С помощью этого нового связанного пула каждый процессор можетshared
Голова очереди выдвигается и извлекается, в то время как другие процессоры обращаются к ней.shared
Только поп из хвоста. из-заnext
/prev
Атрибуты,shared
Голову очереди можно расширить, выделив новую структуру вдвое большего размера, которая будет связана с предыдущей структурой. Размер начальной структуры по умолчанию равен 8. Это означает, что второй структуре будет 16, третьей 32 и так далее.
Кроме того, теперьpoolLocal
Структурам не нужны блокировки, а код может полагаться на атомарные операции.
О недавно добавленном кеше жертвы (Примечание переводчика: о введении кеша жертвыcommit, введение этого кеша должно решить проблему предыдущего теста), новая стратегия очень проста. Теперь есть два набора пулов: Активный и Архивный (Примечание переводчика:allPools
а такжеoldPools
). Когда сборщик мусора запускается, он сохраняет ссылку на каждый пул в новом атрибуте (жертве) в пуле, а затем превращает эту группу пулов в архивный пул перед очисткой текущего пула:
// 从所有 pool 中删除 victim 缓存
for _, p := range oldPools {
p.victim = nil
p.victimSize = 0
}
// 把主缓存移到 victim 缓存
for _, p := range allPools {
p.victim = p.local
p.victimSize = p.localSize
p.local = nil
p.localSize = 0
}
// 非空主缓存的池现在具有非空的 victim 缓存,并且池的主缓存被清除
oldPools, allPools = allPools, nil
Благодаря этой стратегии приложение теперь будет иметь циклический GC для создания/сбора новых элементов с резервными копиями благодаря кешу жертвы. В предыдущей блок-схеме кэш жертвы будет запрошен после процесса запроса «общего» пула.