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