Это статья об основах программирования на Go. На канале я рассказываю об опыте перехода в IT с нуля, структурирую информацию и делюсь мнением.
Хой, джедаи и амазонки!
Заинтересовался темой, как оптимально конкатенировать, т.е., скажем так, склеивать строки без изменения их содержимого. В публикации рассматриваю некоторые способы конкатенации строк и ищу явную грань, где следует использовать тот или иной способ. Одновременно осваиваю бенчмарки для оценки производительности. Также рассуждаю на тему оптимизаций - где имеет смысл это делать, где - не принесёт ощутимой пользы.
Рассмотрена конкатенация строк и срезов байт через оператор плюс, fmt.Sprintf, strings.Builder, bytes.Buffer, strings.Join, bytes.Join и использование пулов объектов для конкатенации через буфер.
В тестах использовалась версия Go 1.23.5:
1. Введение
1.1. О типе string
При старте изучения Go в любых книгах или курсах, почти в самом начале, говорится, что строка в Golang - это неизменяемый тип данных в виде среза байт, представленных в кодировке UTF-8. Где каждый символ занимает от 1 до 4 байт. Условно, цифра или буква латиницы в Go - это 1 байт, буква кириллицы - 2 байта, большинство иероглифов - 3 байта, а большинство смайликов - 4.
Свойство неизменяемости строки говорит о том, что если строка создана, она уже не может быть изменена. Если нужно добавить в неё информацию или убрать - нужно создавать новую строку.
Программирование - это процесс создания способов обработки данных для достижения требуемого результата. В веб-разработке плюс-минус половина данных, которые нужно обрабатывать - это строки либо их представления, например JSON/XML. Другими словами - уметь работать со строками - важно для программиста Go.
Часто в работе нужно собрать из нескольких строк одну. Это частая задача: от создания логов и заполнения полей структур до генерации подписи или построения сложного sql-запроса. Как это сделать эффективно: с минимальными расходами по памяти и времени исполнения, а также с минимальным количеством запросов на выделение памяти из heap - что косвенно увеличивает время выполнения.
Другими словами - как научиться эффективно конкатенировать строки, поскольку часто нужно это делать.
Термин конкатенация происходит от латинского concatēnātiō, которое состоит из "con-" (вместе) и "catēnāre" (сцеплять, связывать). Вспоминается фильм Бегущий по лезвию 2049, где на тесте нужно было повторять нечто подобное:
Термин конкатенация использовался в математике и лингвистике задолго до программирования. И в программирование он перешёл, как устоявшийся термин.
В работе встречаются случаи, когда нужно конкатенировать в строку как только тип string, так и в комбинациях с []byte, и различными простыми типами - int, int64, float64 и даже bool или какими-то другими. И мне стало интересно поэкспериментировать - как это делать эффективно и есть ли явная грань, где стоит использовать тот или иной способ. А также какие есть способы конкатенации строк.
1.2. Грань оптимизации
Полезно понимать, для чего делается такое исследование по конкатенации, и окупятся ли его результаты. Читая статьи на эту тему, пришёл к выводу - что тема конкатенации относится к сверхтонкой оптимизации - когда идёт борьба за наносекунды работы веб-приложения.
Такая оптимизация нужна при высокой загрузке: участки кода выполняются десятки - сотни - тысячи раз в секунду. Это может быть важно для популярных соцсетей, маркетплейсов или видеохостингов, например, но не особенно важно для более мелких сайтов с малым числом запросов.
Для чего был создан язык Go? Он был создан для эффективного использования ресурсов многоядерных компьютеров на операциях с задержками от ввода-вывода данных. В первую очередь для таких работ, как взаимодействие компьютеров по сети.
Есть мнение, что Go не подходит для ряда сверхтонких микрооптимизаций, к которым можно отнести и операции конкатенации. Go нужен, когда важна надёжность и простота. Если идёт борьба за наносекунды, то по мнению ряда авторов научно-популярных публикаций, нужно смотреть в сторону Rust или некоторых других языков.
Однако, если код высоконагруженный, золотую середину микрооптимизаций, думаю, полезно знать и использовать.
Какие вообще есть оптимизации? Что это вообще такое?
Вольное определение: оптимизация сетевого приложения - это процесс достижения устойчивого поведения приложения, когда за единицу времени удаётся обработать больше сетевых запросов, сократить число и критичность ошибок, снизить расходы ресурсов компьютера, обеспечить простую поддерживаемость кода и улучшить пользовательский опыт за счёт устранения узких мест по коду и инфраструктуре.
Иногда одна часть определения оптимизации противоречит другой, например встречал мнение, что оптимизированный код не может легко читаться.
Логично, что во время работы над оптимизацией в первую очередь нужно расширить узкие места, которые больше всего стопорят работу всего сетевого приложения. Т.е. сперва разобраться с тем, что съедает десятки секунд, затем - секунды, затем доли секунд и если ещё понадобиться - идти дальше к микрооптимизациям.
Макро-оптимизации, воздействуя на которые, можно добиться максимального эффекта, на мой взгляд, следующие:
- Вертикальное масштабирование - более мощные ЦП, скоростная ОЗУ, SSD, современные способы передачи данных - оптоволокно, а не витая пара - всё что можно улучшить внутри компьютера и сети по "железу". Зачастую этой оптимизацией пользуются частные небольшие интернет-магазины, написанные на php, когда начинают испытывать большую нагрузку от пользователей или хотят отдавать изображения и видео лучшего качества;
- Горизонтальное масштабирование - новые серверы веб-приложения с балансировщиками для равномерного распределения нагрузки между серверами;
- Базы данных: оптимальные схемы таблиц (в т.ч. необходимое и достаточное индексирование), использование современных БД (в т.ч. обновление версий), использование подходящих типов БД под конкретные ситуации;
- Прокси-серверы со множеством возможностей от фильтрации запросов до защиты от ddos-атак;
- Использование современных технологий для работы сетевых приложений - брокеры сообщений, GRPC между микросервисами, кеширование. Думаю, сюда много чего ещё можно добавить.
Микро-оптимизации - чисто про код:
- Асинхронная обработка данных если возможна (часть задач может быть только синхронной);
- Алгоритмическая оптимизация. Пример с бинарным поиском. Для бэкенд-разработчика в большинстве случаев все алгоритмы уже готовы и скорее всего находятся в стандартной библиотеке Go - например, всевозможные сортировки. Т.е. тут ничего изобретать не нужно;
- Эффективное использование существующих типов данных и возможностей языка - их очень много. Например: 1 - использование пустых структур для передачи сигналов в канал или для значений карт когда важны только значения, т.к. пустые структуры не требуют памяти; 2 - в switch'е или if-блоках в первое условие включать наиболее распространённый случай; 3 - задавать ёмкость карте и слайсу, если они известны заранее; 4 - time.Ticker вместо time.Sleep для повторяющихся задач может быть эффективнее; 5 - классический for i:=... может быть быстрее for-range, т.к. второй копирует значения; 6 - разумное использование defer, т.к. он привносит накладные расходы; 7 - вообще, обходиться без объектов, несущих (большие) накладные расходы, когда можно обойтись другими простыми способами - например, конкатенация через + без буфера в простых случаях; 8 - использовать аннотации джейсона для парсинга (Go может сопоставлять значения и без джейсон-аннотаций); 9 - избегать избыточного создания переменных и подставлять простые данные и даже функции сразу в значения других функций или полей структур (если не падает наглядность); 10 - выносить в глобальные переменные или константы часто используемые значения, в т.ч. регулярные выражения, которые дороги при создании; 11 - избегать рефлексии если возможно - т.к. это ресурсоёмкий процесс; 12 - с картами много эффективных решений в сравнении с альтернативами, например, подсчёт количества уникальных элементов.
- Оптимизации работы с базой данных - от построения эффективных SQl-запросов без лишних джойнов и выбора только нужных данных по индексам, до управления соединениями;
- Оптимизация сетевых запросов - современные протоколы, например TLS 1.3 вместо TLS 1.2, ускоряющий соединение ~ на 0,1 сек;
- Использование нестандартных библиотек, которые производительнее стандартных. Это отдельная интересная тема для разбора, но я знаю что авторы Go прислушиваются к сообществу и активно обновляют свои библиотеки; и данные о неэффективности стандартной библиотеки Go часто устаревают с выходом новых версий.
- Использование пула объектов для снижения накладных расходов на создание "дорогих" объектов (интересная тема для разбора - что за объекты часто используются, для которых можно снизить расходы);
- Оптимизации, связанные с тем, что важнее - нагрузка на ЦП или на сеть - можно использовать алгоритмы сжатия передаваемых по сети данных, но это загружает ЦП;
- Работа со ссылками на типы данных - например, копировать не всю структуру в функцию (передача по-значению), а копировать только ссылку на структуру (передача по-ссылке);
- Оптимизации с чтением и записью данных: файлы, сеть.
1.3. Инструмент исследования конкатенации
Исследовать способы конкатенации буду через бенчмарки.
Бенчмарк - это один из инструментов для измерения производительности программного или аппаратного обеспечения.
Бенчмарк Go соответственно измеряет производительность программного обеспечения: он итерирует код некоторое количество раз и выдаёт средний результат. Количество, раз которое повторяется в бенчмарке, регулируется внутренними механиками Go, чтобы был получен стабильный результат и затраты времени были приемлемыми.
Пример результатов бенчмарка:
В Go при анализе кода через бенчмарк выдаётся следующая информация:
- Описание операционной системы и основных компонентов компьютера;
- Наименование тестируемой функции и через тире - количество ядер ЦП;
- Количество повторов кода для достижения стабильного результата;
- ns/op - усреднённое количество наносекунд на операцию. Одна наносекунда это 1/1_000_000_000, т.е. одна миллиардная доля секунды. Напомню, что 1 мс = 1/1_000 доля секунды, 1 мкс = 1/1_000_000 доля секунды. Это значение меняется в бенчмарках на одних и тех же данных при повторениях.
- B/op - усреднённое количество байт памяти из heap, затраченное на операцию. Это значение обычно не изменяется на разных бенчмарках при одних и тех же входных данных;
- allocs/op - количество аллокаций на операцию - обращений в heap за новой порцией памяти. Как и с b/op обычно не изменяется при повторениях теста;
- Информация об успехе/неудаче теста и времени на тестировании.
Также производительность кода можно оценивать через профилирование, но ранее не работал с этим инструментом. Как я понимаю, его логично использовать для больших блоков кода, и результат профилирования покажет больше данных, вплоть до строчек кода на которых происходит наибольший расход ресурса. Интересно будет отдельно изучить эту тему.
Нашёл ещё такое сравнение бенчмарка и профилирования:
- Бенчмарки покажут, сколько код (например, конкретная функция) потребляет ресурсов - времени выполнения и памяти;
- Профилирование объяснит, почему код потребляет много ресурсов происходит и где именно находятся узкие места в большом блоке кода.
Вероятно, принцип оптимизаций таков: запускаем профилирование - ищем узкие места - оптимизируем и сравниваем варианты через бенчмарки. Затем запускаем профилирование и ищем новые узкие места.
Бенчмарки без особого конфигурирования автоматически должны распределить нагрузку на все ядра ЦП. Точнее, на то количество, что установленное в переменной GOMAXPROCS.
GOMAXPROCS определяет максимальное количество потоков, которые одновременно могут выполняться в Go-приложении и по-умолчанию равна числу логических ядер ЦП.
Ещё можно сказать, что хотя бенчмарк много раз запускает код и выделяет среднее арифметическое, может быть полезно запускать несколько раз один бенчмарк для сравнения результатов: что-то вроде получать среднее арифметическое из средних арифметических. Т.к. порой результаты по времени выполнения различаются до 10% на повторных тестах.
1.4. Сценарии исследования конкатенации
Я хочу получить приближенный к реальности результат конкатенации, чтобы понимать как оно будет работать не в идеальном вакууме, а как будет происходить в реальности.
Есть три вида задач, характеризующих, какой компонент компьютера используется интенсивнее всего и ограничивает производительность общей системы:
- CPU-bound - связанные с загрузкой ЦП задачи;
- GPU-bound - связанные с загрузкой графического процессора;
- I/O-bound - связанные с задержками от ввода-вывода (in-out) задачи.
Примеры CPU-bound задач: математические и научные вычисления, управление устройствами и доступом к ним, криптография.
Примеры GPU-bound задач: машинное обучение, добыча криптовалют, обработка видео и графики, игры с 3D-графикой.
Примеры I/O-bound задач: чтение и запись данных на диск или флеш-карту, передача данных по сети, ожидание ввода пользователя.
Golang эффективно использует многоядерные ЦП при множестве поступающих I/O-bound задачах, умело распределяя ресурсы при появляющихся паузах. Основной инструмент для этого - применение подпотоков (горутин) внутри Go, которые работают в потоке ОС, который уже исполняется на ЦП. Для ЦП множество горутин от Go выглядят как непрерывный единый поток, и внутренние механики ОС не меняют преждевременно на ЦП один поток на другой, что повышает эффективную работу ЦП. Горутины и каналы - это такой лайфхак над операционной системой.
Именно поэтому, когда проводят сравнение через бенчмарки различных языков на простых задачах вроде арифметической операции 1+1 в цикле на 100 млрд запросов - Go там далеко не на первом месте: это CPU-bound задача, и другие языки её могут решать эффективнее.
Конкатенация - это тоже CPU-bound задача. Но это не значит, что нет способов сделать её более или менее эффективнее в Go. Другой вопрос, как её протестировать в реальных сценариях, при которых работают веб-серверы в боевой среде, и найти оптимальные варианты для различных сценариев - а сценариев конкатенации может быть немало.
И вот тут возникла серьёзная проблема: чтобы протестировать приемлемое количество сценариев для получения достоверных результатов, нужно протестировать даже не сотни вариантов - тысячи. И не факт, что в новой версии Go не изменятся какие-то внутренние настройки и расчёты тогда прекратят быть достоверными. Затраты на исследование никогда не окупятся.
Какие я посчитал важными сценарии, для получения приблизительно достоверного результата сравнения конкатенации:
- Тесты для подстрок разной длины. Допустим, для 16 случаев - конкатенируем строки из ряда геометрической прогрессии 2, 4, 8, 16 и т.д. до 65536 байт;
- Тесты для разного количества этапов конкатенации - условно, количество итераций в цикле. Допустим, для 1, 2, 3, 5, 10, 25, 50, 100, 500, 1000 этапов - это 10 случаев;
- Тесты для разного количества подстрок в каждом этапе - допустим от 1 до 10 подстрок, т.е. 10 случаев;
- Помножаем всё вышеперечисленное на два: нужно посмотреть на эффективность конкатенации как строк, так и срезов байт - 2 случая;
- Помножить всё на три - для получения достоверных результатов нужно провести хотя бы по 3 измерения для каждого сценария и высчитать среднее арифметическое.
- Всё это для каждого способа конкатенации. Или хотя бы для трёх: "+", strings.Builder, bytes.Buffer. Остальные способы более специфичны.
Общее количество тестов - произведение всех вариантов:
16×10×10×2х3х3=28800 тестов.
Все результаты нужно зафиксировать и как-то визуализировать - как минимум сделать таблицу, как максимум - графики. В моём представлении, это зависимость длины строки чисто к времени выполнения - без учёта аллокаций и расходов по памяти, это:
10х10х2=200 графиков. Плюс по опыту, чтобы на графике можно было разглядеть значения для малого количества строк, нужно разбивать каждый график на два - для строк 2-512 байт и свыше 1024 байт; т.е. уже 400 графиков. Выглядеть таблицы и графики могут примерно так:
И таких графиков нужно 400 (!). Это явно не похоже на какую-то простую схему, на которую взглянешь и сразу поймёшь: "так, у меня 3 этапа, конкатенирую строки такого размера, в каждом этапе по 2 подстроки, нужно использовать вот этот способ".
Я поискал какие-то упрощённые подходы получить достоверные результаты при меньшем количестве тестов, но не нашёл - все упрощения значительно искажают реальность, которая выявляется если потестировать чуть внимательнее.
Так что же делать, вообще нельзя посмотреть на эффективность конкатенации? Можно, но не так, что получится получить абсолютно точные границы, при какой ситуации использовать тот или иной способ. В последующих параграфах я рассуждаю на эту тему.
Ключевой вывод - нет однозначно более и менее эффективного способа конкатенации. Каждый способ может быть хорош, и есть некоторые общие принципы их применения. Но если конкатенация многоэтапная, а код асинхронный и сколь-нибудь нагруженный - нужно выполнить бенчмарки с подстроками, приближенными к реальным, и посмотреть на результат: в одном случае может быть выгоднее strings.Builder, в другом - bytes.Buffer, в третьем strings.Join, а в четвёртом - обычная конкатенация через оператор сложения.
Наивно говорить, что вот конкатенация через плюс: "используется, только если программист не хочет заморачиваться над оптимизацией", - это не так. Где-то конкатенация через плюс - самый эффективный способ по времени выполнения и расходам памяти. Также и везде буферы ставить не стоит - это может быть затратнее по ресурсам, чем strings.Join. Или говорить, что strings.Builder эффективно только строки конкатенирует, а вот для срезов байт нужно использовать bytes.Buffer - это неверно.
А теперь пройдём по каждому способу конкатенации.
2. Способы конкатенации
Есть следующие основные способы конкатенации:
- Оператор "+";
- fmt.Sprintf();
- Буферы bytes.Buffer и strings.Builder;
- strings.Join() и bytes.Join().
- Пул буферов для конкатенации.
Рассмотрим каждый.
2.1. Конкатенация через оператор плюс
Знакомый всем простой способ конкатенации с прямым присваиванием через плюс - очень эффективный, если заранее известны все подстроки и можно выполнить конкатенацию в одно действие:
str := str1 + str2 + str3 + str4 ... + strN
В моих тестах этот код эффективнее любого другого кода конкатенации строк Go в любых условиях: маленькие подстроки или большие, много подстрок или мало, синхронный или асинхронный код.
Эффективность конкатенации через плюс обусловлена тем, что на создание такой строки компилятор Go однократно выделяет память единым блоком из heap. Исключение - когда нужно срезы байт предварительно привести к строке - на каждую такую операцию нужна отдельная аллокация.
Конкатенировать через оператор плюс можно как строки, так и срезы байт через приведение типа к строке. Но эффективность конкатенации через плюс со срезами байт резко падает:
str := string(someDataBytes1) +... string(someDataBytesN)
Такой код менее эффективен, т.к. каждое приведение типа среза байт к строке вызывает дополнительную аллокацию из heap - это доп затраты, прежде всего по-времени.
Конкатенация через плюс может быть намного эффективнее других способов и при многоэтапной конкатенации, например:
str := str1 + str2 + str3
// условия с вариантами появления новых строк
str += str4
// условия с вариантами появления новых строк
str += str5
Но как правило, таких этапов должно быть не очень много, чтобы преимущество сохранилось.
Минус конкатенации через плюс - код не будет наглядным при формировании сложных строк. А ещё нужно не забывать ставить пробелы в литералах, где нужно:
2.2 Конкатенация Sprintf
Возьмём другой пример конкатенации строки - fmt.Sprintf:
str := fmt.Sprintf("%d%s%s", 155, str1, someDataBytes)
Тоже широко известный способ, осваивается с первых шагов знакомства с Go. Мне он напоминает мультитул:
Связь с мультитулом в том - что у fmt.Sprintf множество возможностей, но в обмен на универсальность снижается эффективность. Сравните сами - чем легче почистить рыбу - полноценным охотничьим ножом, или ножиком из мультитула? Тут та же история.
На мой взгляд у fmt.Sprintf две проблемы:
- Каждый аргумент в функции приводит к новой аллокации - увеличивается время на выполнение кода;
- Преобразования типа данных в строку выполняется медленнее, чем если использовать специальную функцию под конкретный тип данных, которая оптимизирована конкретно под него.
Преимущество одно: универсальность - зрительно понятнее, чем конкатенация через оператор плюс, позволяет приводить к строкам другие типы данных, а также форматировать подстроки через спецификаторы.
Рассмотрим первую проблему. Если взять такой код:
str := fmt.Sprintf("%s%s%s", str1, str2, str3)
то он в общем виде примерно в четыре раза затратнее по времени выполнения (т.к. четыре аргумента в функции типа string - четыре аллокации), чем такой код:
str := str1 + str2 + str3
Подтверждение:
Вторая проблема. Если нужно преобразовать число или другой тип данных в строку, то Sprintf выполняет это медленно. Вместо такого:
numStr := fmt.Sprintf("%d", 35)
Полезнее использовать такое:
numStr := strconv.Itoa(35)
или так, если тип данных int64:
numStr := strconv.FormatInt(dataInteger64, 10)
И тому подобное, в т.ч. в более сложных сценариях когда нужно преобразовать типы несколько раз или в связке со строками.
Пример
Возьмём типичную функцию конкатенации комбинированных данных - из строк и чисел, которая может быть использована для формирования служебной информации, например - лога:
_ = fmt.Sprintf("%02d/%02d", month, year)
Спецификатор %02d форматирует числа так, чтобы минимальная ширина каждого значения будет состоять хотя бы из двух знаков. Если меньше - слева от значения добавится ноль.
Функцию удобно использовать в низконагруженных участках кода, но если нужно оптимизировать - можно написать своё более эффективное исполнение. Взглянем на код и бенчмарки, чтобы убедиться в этом:
Во втором случае через комбинацию способов, где конкатенация строк через плюс и специализированное форматирование - получаем кода больше, но расходы на ресурсы компьютера уменьшились вдвое по времени исполнения и памяти.
2.3. Буферы для конкатенации
Буферов есть два: bytes.Buffer и strings.Builder. Оба позволяют конкатенировать строки и срезы байт.
strings.Builder предназначен чисто для конкатенации (запись данных), bytes.Buffer может как записывать (конкатенировать), так и читать данные, в т.ч. по частям. Чтение прежде всего из файла или сети.
При создании буфера можно задать ему размер, и естественно, если мы знаем итоговый размер строки, имеет смысл задать буферу размер - так буфер не будет повторно аллоцировать память из heap. Но чаще размер конечной строки неизвестен.
Примеры
Несколько примеров с рассмотренными выше способами конкатенации, где в каждом этапе будем конкатенировать три подстроки:
Запустим с разным количеством этапов конкатенации:
Глядя на эти тесты можно сделать вывод, что что конкатенация через плюс эффективнее для 1-10 этапов, через strings.Builder примерно для 11-100, а свыше 100 этапов эффективнее bytes.Buffer. Но это неверно, такая эффективность характерна только для конкретно этой ситуации.
Например, возьмём ситуацию, когда конкатенация не из трёх подстрок, а из двух подстрок:
Видим, что эффективнее уже не конкатенация через плюс, а strings.Builder.
Я провёл несколько сотен экспериментов для различных сценариев, в общем виде можно сказать, что если идти от более простых способов конкатенации к более сложным (больше подстрок на этап, больше этапов, строки больше, срезы байт вместо строк), всегда прослеживается тенденция:
- Сперва эффективнее "+";
- Далее эффективен strings.Builder;
- В самых сложных сценариях эффективнее bytes.Buffer.
А вот границы эффективности при разных условиях различны и точное их исследование нецелесообразно из-за большой вариативности сценариев конкатенации и изменчивости внутреннего устройство Golang в разных версиях.
2.5. strings.Join и bytes.Join
Две эти функции объединяют срезы строк и срезы срезов байт соответственно, в т.ч. с возможностью задать разделитель.
Примеры:
Видим, что конкатенация среза строк через Join эффективнее других способов.
Проверим то же с байтами:
Видим, что джойны эффективнее.
2.6. Пулы буферов
Идея пула объектов: в Go для различных целей создаются структуры, и на создание этого объекта - экземпляра структуры (даже без наполнения его содержимым) тратятся ресурсы. Если код асинхронный, и одинаковые объекты часто используются в разных горутинах, то можно внедрить оптимизацию, при которой будет создан набор объектов - пул, для их повторного использования без уничтожения. Горутина берёт объект из пула, очищает его от результатов работы другой горутины и использует.
Для сложных объектов, где множество полей - это может быть логично. Буферы strings.Builder и bytes.Buffer - дешёвые в плане расходов ресурсов при их создании. Но пулы буферов также имеет право к существованию.
Примеры
Напишем код и протестируем несколько сценариев, чтобы просто потрогать этот процесс:
Рассмотрим случай для 10 этапов, а количество буферов и горутин равно количеству ядер ЦП:
С пулами вышло затратнее.
Идём далее. Горутин и буферов в пуле по 500:
С пулами затратнее.
Идём далее. Горутин 500, в пуле в исходном состоянии нет объектов:
С пулами затратнее.
Идём далее. Горутин 10 тысяч, в пуле исходно 500 объектов:
На 10 000 горутин идея с пулами оправдалась - выгоднее.
Идём далее. Горутин 100 тысяч, в пуле исходно 500 буферов:
Идея с пулами также выгоднее.
Горутин 100 тысяч, объектов в пуле изначально нет:
С пулами также выгоднее, даже если исходно в пуле нет объектов.
Какой вывод можно сделать: пулы буферов имеют смысл, если количество одновременно работающих горутин, использующих объекты пула, исчисляется десятками тысяч и более. При этом особой роли не играет есть ли в пуле исходно какое-то количество объектов, или нет: они создадутся по мере надобности сами.
Возможно, большее количество тестов при разных сценариях покажет более точные идеи применения пулов буферов, но для меня достаточно понимания, что идея применения пулов для дешёвых (в плане затрат на создание) объектов имеет смысл при большом количестве горутин.
3. Выводы
Мы познакомились с различными способами конкатенации в Go, а также общим понятием оптимизаций. Можно сделать следующие выводы:
- Конкатенация - это микро-оптимизация. Если плохо спроектированы таблицы в БД или на сайт приходит 10 запросов в сутки, смысла в оптимизации конкатенации нет;
- Если мы хотим оптимизировать конкатенацию в нагруженных частях кода - нужно проводить бенчмарки в сценариях, приближенных к реальности, - только так можно однозначно сказать, какой способ эффективнее;
- Конкатенация через оператор плюс может быть самым эффективным способом конкатенации. Когда нужно конкатенировать в один этап - это всегда самый эффективный способ конкатенации, но менее наглядный;
- Для приведения других типов данных к string не рекомендуется использовать fmt.Sprintf, эффективнее - специализированные функции;
- fmt.Sprintf в целом - самый медленный из всех способов конкатенации и самый затратный по расходам памяти;
- Для конкатенации срезов эффективнее использовать strings.Join() и bytes.Join() соответственно для срезов байт и срезов срезов байт.
- Если определено, что эффективно использовать буферы для конкатенации, а код асинхронный - протестируйте с пулами.
На этом у меня всё, благодарю, что дочитали публикацию до конца. Успехов, и будем на связи.
Бро, ты уже здесь? 👉 Подпишись на канал для начинающих IT-специалистов «Войти в IT» в Telegram, будем изучать IT вместе 👨💻👩💻👨💻