оригинал:medium.com/ah-journey-i…
Эта статья основана на Go 1.12 и 1.13, давайте посмотрим на революционные изменения в sync/pool.go между этими двумя версиями.
Пакет Sync предоставляет мощный пул повторно используемых экземпляров, чтобы уменьшить нагрузку на сборку мусора. Прежде чем использовать этот пакет, вам необходимо запустить ваше приложение из тестовых данных до и после использования пула, потому что в некоторых случаях, если вы не знаете внутреннюю работу пула, это ухудшит производительность приложения.
Ограничения пулов
Давайте сначала рассмотрим несколько основных примеров, чтобы увидеть, как это работает в довольно простом случае (выделение 1 КБ памяти):
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 выделения, но только один экземпляр структуры выделяется в памяти. На данный момент видно, что использование пула более удобно для обработки и потребления памяти.
Однако в практическом примере, когда вы используете пул, ваше приложение будет иметь много новых выделений памяти в куче. В этом случае при увеличении памяти будет запускаться сборка мусора.
Мы можем заставить сборку мусора происходить, используя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%
Теперь мы видим, что с пулом выделение памяти выше, чем без пула. Давайте внимательно посмотрим на исходный код этого пакета, чтобы понять, почему это так.
внутренний рабочий процесс
посмотриsync/pool.go
Файл покажет нам функцию инициализации, содержание этой функции может объяснить только что возникшую ситуацию:
func init() {
runtime_registerPoolCleanup(poolCleanup)
}
Это регистрируется как метод во время выполнения для очистки пулов. И этот же метод срабатывает и при сборке мусора, в файлеruntime/mgc.go
в
func gcStart(trigger gcTrigger) {
[...]
// clearpools before we start the GC
clearpools()
Это объясняет, почему при вызове сборки мусора производительность снижается. Пулы очищаются каждый раз, когда начинается сборка мусора. этоДокументацияНа самом деле нас предупредили
Any item stored in the Pool may be removed automatically at any time without notification
Далее давайте создадим рабочий процесс, чтобы понять, как им управлять.
каждый мы создаемsync.Pool
, go создаст внутренний пулpoolLocal
Подключен к каждому процессору (P в GMP). Эти внутренние пулы состоят из двух свойствprivate
иshared
. К первому может получить доступ только его владелец (операции push и pop, поэтому блокировки не требуются), в то время как `shared может быть прочитан любым процессором и должен поддерживать безопасность параллелизма сам по себе. На самом деле пул — это не просто локальный кеш, его можно использовать для любой сопрограммы или горутин в нашей программе.
Версия 1.13 Go улучшитshared
, также принесет новый кеш, который решает проблемы, связанные со сборщиком мусора и пулами очистки.
Новый незаблокированный пул и кеш жертвы
Go 1.13 использует новый двусвязный список какshared pool
, снимает блокировку, улучшаетshared
эффективность доступа. Эта модификация в основном предназначена для повышения производительности кэша. вот визитshared
процесс
В этом новом связанном пуле каждый processpr может помещать и выталкивать начало связанного списка, а затем получать доступ кshared
Подблоки могут быть извлечены из конца связанного списка. Размер структуры станет вдвое больше исходного размера при ее расширении, а затем использовать структуру между структурами.next/prev
указатель для подключения. Размер структуры по умолчанию таков, что она может содержать 8 дочерних элементов. Это означает, что вторая структура может содержать 16 дочерних элементов, третья — 32 и так далее. Точно так же нам больше не нужны блокировки, а выполнение кода атомарно.
Что касается нового кеша, новая стратегия очень проста. Теперь есть 2 набора пулов: активные пулы и архивные пулы. Когда сборщик мусора запускается, он сохраняет ссылку каждого пула на новое свойство в этом пуле, а затем копирует коллекцию пула в архивный пул перед очисткой текущего пула:
// Drop victim caches from all pools.
for _, p := range oldPools {
p.victim = nil
p.victimSize = 0
}
// Move primary cache to victim cache.
for _, p := range allPools {
p.victim = p.local
p.victimSize = p.localSize
p.local = nil
p.localSize = 0
}
// The pools with non-empty primary caches now have non-empty
// victim caches and no pools have primary caches.
oldPools, allPools = allPools, nil
С этой стратегией приложение теперь будет иметь еще один цикл сборщика мусора для создания/сбора новых элементов с резервными копиями из-за кеширования жертвы. В рабочем процессе кэш жертвы будет запрошен в конце процесса после общего пула.