Речь пойдет про статью, написанную Michael Knyszek, автора proposal по улучшению трейсинга в Go.
Напомню, пакет runtime
содержит инструментарий для профайлинга ресурсов: CPU и памяти.
Собрав статистику, можно анализировать её с помощью go tool pprof
и обнаружить утечки памяти.
runtime/trace
накапливает события во время выполнения программы: создание, блокировка,
разблокировка go-рутин, момент передачи контроля системному вызову, его блокировка, события GC, работу тредов.
Для анализа этих собранных данных используется go tool trace
. Также можно использовать
gotraceui. В этих инструментах на временной шкале можно увидеть
события, которые происходили во время выполнения программы.
Благодаря runtime/trace
профайлеру можно обнаружить проблемы конкурентного доступа к ресурсам,
например, когда много go-рутин блокируются на чтение из/запись в канал. Для удобной навигации и расстановки
нужных вам акцентов можно использовать задачи (NewTask), которые объединяют регионы (WithRegion()) в группы.
Регион - это область на временной шкале визуализатора, где происходит какое-то событие. Также можно добавить
лог запись в трейсинг данные в нужном вам месте и увидеть её в визуализаторе в контексте go-рутины, которая её
вызвала.
Go Execution Tracer (runtime/trace
) был создан как инструмент для live профайлинга и для
обеспечения производительности был использован бинарный формат данных статистики и получение точного времени
из регистра процессора TSC (комнад RDTSC). Несмотря на это runtime/trace
потреблял от 10 до 20%
CPU для большинства приложений.
Причина потери производительности была в stack unwinding (stack walking). Это процесс, когда профайлер
собирает стек вызовов для каждого события, пробегая по по фреймам этого стека. Помимо этого нужно обогатить
данные именем файла и строкой кода для вывода данных в визуализаторе.
gettraceback
(пакет runtime
) функция исторически была реализована асинхронной, чтобы как можно меньше
препятствовать работе программы, кроме этого пакет trace
появился в версии Go 1.5 до того как
в стеке вызовов был указатель на фрейм, где выполняется программа. С появлением frame pointer'а в Go 1.7
оптимизация стала возможна, но ей попросту никто не занимался.
Благодаря проведенной оптимизации начиная с Go 1.21 runtime/trace
потребляет 1-2% CPU.
Пропоузал можно почитать здесь:
proposal 60773.
Главная цель - добавить в runtime/trace
возможность стриминга данных трейсинга.
Для этого необходимо разбить накопленные данные на партиции, что и было сделано и уже доступно в Go 1.22.
Но go tool trace
еще не зарефакторен, чтобы использовать новый формат стриминговых данных.
Я хотел рассказать про этот челлендж еще в позапрошлом выпуске, но болел и мы решили сделать выпуск коротким. А тут вышла еще одна статья, лучше предыдущей и раз уж мы затронули тему оптимизации, давайте обсудим этот челлендж. Важно, что автор - не мастер по оптимизации и если мне попадется статья от эксперта, то обязательно поделюсь с вами.
Итак, задача проста: прочитать файл с 1 миллиардом строк (13Гб) и вывести репорт. Каждая строка содержит 2 поля,
разделенные ";": название метеорологической станции (строка), температура (плавающая точка).
Отчет должен содержать: имя_станции=мин,сред,макс_температура
, разделенные запятой.
Решение задачи "в лоб" дает результат 95 секунд. Следующий шаг добиться просто быстрого считывания файла, т.е.
просто открываем файл, вычитываем все и дискардим, закрываем файл. Можно файл читать через
bufio.Scanner
, через bufio.Reader
и напрямую, используя метод Read()
.
Метод Read()
позвлоляет читать в слайс определенного размера. Слайс размером 4Мб дал наилучший
результат - 0.98 секунды.
Следующий шаг - быстрый парсинг. Как раз pprof показывает, что больше всего времени уходит на
bytes.Split()
и strconv.ParseFloat()
. Для этого пишем кастомный Split(), который
копирует данные в заранее выделенный буфер, а не создает его каждый раз. А с темпиратурой работаем как с
целым числом - читаем побайтово температуру и если это число, то добавляем его в разряд единиц, предварительно
умножив результат на 10.
Далее, можно использовать Swiss Map - быстрая хэш-таблица, поддерживаемая комьюнити. Swiss Map использует быстрый механизм хеширования и механизм параллельного лукапа.
Можно читать из файла и писать полученный результат в канал, из которого воркеры (go-рутины) будут читать. А можно просто сделать дескриптор файла доступным/видимым для всех go-рутин.
Выводы: используем буферы, делаем профайлинг.