Комментарии 8
Отличная статья для новичков. Не понял только пример с sync.Pool. Фактически в варианте default не создаются и не удаляются новые структуры данных, в отличие от варианта pool, где, как минимум, генерируется (alloc) новая ссылка на слайс. Поэтому default не может быть медленнее pool. Представленный ниже вариант бенчмарка дает результат уже в пользу варианта default. Есть подозрение, что наблюдаемое замедление связано с особенностями в инициализации глобальных переменных при запуске тестов. Если не прав - поправьте - очень интересно разобраться.
func BenchmarkDefault(b *testing.B) {
var dataDefault = make([]int, 0, 10000)
for i := 0; i < b.N; i++ {
dataDefault = processDefault(dataDefault[:])
}
}
// Классическая работа.
func processDefault(dataDefault []int) []int {
// Некоторая обработка данных.
for i := 0; i < 10000; i++ {
dataDefault = append(dataDefault, i)
}
// Очистка.
return dataDefault[:0]
}
Рад, что вам было полезно!
sync.Pool предназначен для повторного использования объектов между горутинами, благодаря чему нет необходимости выделять новую память. Мы берем уже выделенную память, используем и кладем назад.
В первую очередь, sync.Pool снижает нагрузку на сборщик мусора (не отслеживает и не очищает объекты). В случае использования обычного слайса сборщик мусора будет очищать уже использованные объекты. То есть sync.Pool выгодно использовать, когда приложение требует работы с короткоживущими объектами.
Я запустил ваш бенчмарк — и да, в этом случае sync.Pool будет немного, но все же проигрывать в скорости (протестировал на двух устройствах)
func BenchmarkDefault(b *testing.B) {
var dataDefault = make([]int, 0, 10000)
for i := 0; i < b.N; i++ {
dataDefault = processDefault(dataDefault[:])
}
}
func BenchmarkPool(b *testing.B) {
for i := 0; i < b.N; i++ {
data := dataPool.Get().([]int)
data = processPool(data)
dataPool.Put(data)
}
}
goos: linux
goarch: amd64
pkg: test/benchmarks
cpu: 11th Gen Intel(R) Core(TM) i5-11400H @ 2.70GHz
BenchmarkDefault-12 356412 3235 ns/op
BenchmarkPool-12 341449 3453 ns/op
goos: windows
goarch: amd64
pkg: awesomeProject
cpu: Intel(R) Core(TM) i5-6600 CPU @ 3.30GHz
BenchmarkDefault-4 207806 5642 ns/opBenchmarkPool-4 207349 5705 ns/op
Пример в статье описан не особо удачно, по-моему в таком виде это не отражает работы с sync.Pool. Либо там скипнули код теста, где это может быть было отражено, судя по результатам бенчмарка. А так в примере создается слайс на 10000 int, но для GC это всего одна структура слайса с массивом int внутри. Каждый одиночный int внутри массива не будет рассматриваться GC вообще. К тому же это глобальная переменная, со временем жизни на всё время программи и её вообще GC трогать не будет.
Для иллюстрации sync.Pool следовало бы показать заведение таких слайсов конкурентно в большом количестве. Например, слайс инитится и заполняется локально внутри функции, после выхода из неё он будет собран GC, но не сразу, а с периодичностью проведения сборки мусора. Далее, для примера, эта функция вызывается в горутине конкурентно и очень часто, как пример, этэ ручка на вебсервере. При высокой нагрузке на вебсервер горутины могут плодиться и создавать конкурентную нагрузку, тогда в памяти будет создаваться много-много копий слайса, GC их будет вынужден чистить -> время сборки мусора вырастет -> сервер начнет медленнее обрабатывать хендлеры -> число выполняющихся запросов будет расти -> ещё больше копий слайса будет заведено в памяти -> ...
Если через sync.Pool объяснить, что эти слайсы реюзаемые и помещать их в пул при выходе через sync.Put(), то в функции перед тем как аллоцировать новый массив и создавать слайс поверх него будет произведена попытка достать старый уже готовый из пула. Это сэкономит время на make([]int, 10000)
, вместо новой аллокации sync.Get() подсунет старый из пула, из тех что уже не нужны для обработки (в функции их вернили в пул), но GC их ещё не успел собрать. Если там более сложная структура или требующая аллокации значительной памяти, то экономия времени может быть значительной.
При этом стоит понимать, что пул это не кеш, объекты в нем не будут храниться и вытесняться по памяти. На очередном срабатывании GC он может вычистить из пула вообще всё. То есть, если конкурентность низкая и для данного примера со слайсами не требуется заводить много этих слайсов в небольшой момент времени, то проку от пула не будет, это только лишняя обертка вокруг создания структур.
Итого, суть sync.Pool в снижении времени работы GC, при генерации с высокой конкурентностью сложных структур или структур требующих аллокации значительной памяти.
Вот нашел лучшее описание про sync.Pool: https://habr.com/ru/articles/277137/ -- примеры похожие, но обьяснение (перевод из документации) выглядит толковее, чем в текущей статье.
Про каналы совет слишком расплывчатый, выглядит так, что во всех случаях стоит делать буферизованные каналы для ускорения работы.
У буферизованных и небуферизованных каналов просто разное назначение, дело тут не в пропускной способности. Если логика программы такова, что запись в канал в принципе выше, чем скорость вычитывания, то с любым размером буфера ваши каналы превратятся в небуферизованные -- после их заполнения, просто большой буфер заполнится через время, a приложение отъест больше памяти без всякой прибавки в скорости работы. В большинстве кейсов как раз применимы именно небуферизованные каналы, а буфер надо увеличивать, только при понимании, что это даст в конкретном случае и даст ли вообще.
Подумал и не стал ставить лайк статье. Автору респект и +1 за попытку и -1 за реализацию, сорри.
Спасибо, ценное замечание. В этой статье sync.Pool не рассматривается в полной мере, как и некоторые остальные приемы. Был предоставлен минимум, необходимый для понимания. Если пользователя заинтересует этот инструмент, он пойдет и изучит его подробнее. В примере с sync.Pool я и правда не показал всю работу, но это сделано было в угоду простоте.
Если бы я в каждом примере дополнительно сервера запускал или с БД работал, было бы труднее донести информацию. А так люди, которые впервые увидят этот же sync.Pool, заинтересуются и начнут искать иные источники информации для понимания. Отдельное спасибо за ссылку на статью!
Выше написал ответ на первый комментарий, на второй тоже есть что добавить:
зачастую можно встретить задачи, где оба вида каналов можно использовать, Если часто приходится перекидываться сообщениями, через каналы лишнее выделение буфера будет однозначно плюсом. В статье и не сказано, что следует использовать только буферизованные каналы, речь идет о случаях, где увеличение буфера не будет пустой траты памяти
Перед записью строк в strings.Builder, желательно предварительно выделить при помощи метода Grow необходимое количество байт, если это возможно. В вашем примере это реально, это может ускорить конкатенацию за счет того, что не будет потерь на увеличение вместимости слайса
Оптимизация Go: как повысить скорость и эффективность кода