Как стать автором
Обновить

Оптимизация Go: как повысить скорость и эффективность кода

Уровень сложностиСредний
Время на прочтение9 мин
Количество просмотров5.9K
Всего голосов 11: ↑10 и ↓1+9
Комментарии8

Комментарии 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 необходимое количество байт, если это возможно. В вашем примере это реально, это может ускорить конкатенацию за счет того, что не будет потерь на увеличение вместимости слайса

Вы полностью правы. Grow выделяет нужную память под срез, который находится внутри strings.Builder, чем ускоряет конкатенацию в моем примере в целых два раза

Зарегистрируйтесь на Хабре, чтобы оставить комментарий