Go: не оптимизируй код, пока не посмотрел pprof 🔥

Решил проверить на практике, насколько сильно реализация одной операции может влиять на производительность Go-кода. Задача простая: собрать строку из последовательности чисел. Первый вариант - обычная конкатенация:

func buildSlow(n int) string { result := ""

for i := 0; i < n; i++ { result += strconv.Itoa(i) + "," }

return result }

Второй вариант использует strings.Builder:

func buildFast(n int) string { var b strings.Builder b.Grow(n * 7)

for i := 0; i < n; i++ { b.WriteString(strconv.Itoa(i)) b.WriteByte(',') }

return b.String() }

Обе функции возвращают одинаковый результат. Разница в том, как они работают с памятью. Но вместо того чтобы просто объявить второй вариант более эффективным, я запустил бенчмарки и посмотрел CPU profile через pprof.

Результаты на Intel Core i7-14700KF, Go 1.27.1: Метрика Конкатенация strings.Builder Время 368 857 ns/op 10 427 ns/op Память 2 033 568 B/op 11 072 B/op Аллокации 1900 allocs/op 901 allocs/op В этом тесте strings.Builder оказался примерно в 35 раз быстрее и использовал примерно в 184 раза меньше памяти на операцию.

Затем я открыл CPU profile: go tool pprof http://localhost:8080/debug/pprof/profile?seconds=20

И выполнил: top list buildSlow

В отчёте pprof функция buildSlow получила 450 мс cumulative CPU time - 32,61% от общего времени собранных образцов.

На строку с конкатенацией пришлось 440 мс cumulative time.

Это согласуется с тем, что многократное создание новых строк и копирование данных обходятся дорого.

Мой вывод: не стоит начинать оптимизацию с переписывания кода наугад. Сначала измеряем производительность, затем ищем узкое место через профайлер, исправляем его и снова запускаем бенчмарки. И обязательно проверяем, что результат вычислений не изменился.

Я подготовил небольшой проект с HTTP endpoint, двумя реализациями, тестами и бенчмарками.

репо с кодом https://github.com/wolverineo250kr/go-pprof-demo