6 советов по повышению производительности в Go
Цель статьи — обсудить 6 советов, которые могут помочь в диагностике и устранении проблем с производительностью в ваших приложениях Go.
@golang_interview – golang разбор вопросов с собеседований.
Бенчмаркинг
Написание эффективных тестов на Go имеет решающее значение для высокой производительности вашего кода. Создадим файл для теста «becnh_test» и испольем функцию Benchmark пакета тестирования. Вот пример:
В этом примере мы считаем время, необходимое для вычисления 20-го числа Фибоначчи. Функция BenchmarkFibonacci
запускает функцию fibonacci
b.N
раз. Это является значением, установленным пакетом тестирования для получения статистически значимого результата.
Чтобы интерпретировать результаты тестов, мы можем запустить go test -bench=. -benchmem
в терминале. Этот код выполнит все тесты в текущем каталоге и выведет статистику распределения памяти. Флаг -bench
используется для указания регулярного выражения для сопоставления имён тестов, а .
будет соответствовать всем тестам в текущем каталоге. Флаг -benchmem
будет выводить статистику выделения памяти вместе с результатами синхронизации.
Профилирование
В Go есть встроенные инструменты профилирования, которые помогут вам понять, что делает ваш код. Наиболее распространенным инструментом профилирования является CPU profiler, который можно включить, добавив флаг -cpuprofile
в команду go test
. Вот пример:
Первая функция, «TestFibonacci», представляет собой простой модульный тест, который проверяет, правильно ли функция фибоначчи возвращает 20-е число в последовательности фибоначчи.
Функция «fibonacci» — это рекурсивная реализация последовательности фибоначчи, которая вычисляет n-е число в последовательности.
Функция «BenchmarkFibonacci» — это тест, который запускает функцию «Fibonacci» 20 раз и измеряет время её выполнения.
Функция «ExampleFibonacci» — это пример, который выводит 20-е число в последовательности фибоначчи с помощью функции «fibonacci» и проверяет, равно ли оно ожидаемому значению 6765.
Чтобы включить профилирование, мы используем флаг «-cpuprofile» с командой «go test», чтобы вывести результаты профилирования в файл с именем «prof.out». Для запуска тестов и создания данных профилирования можно использовать следующую команду:
После запуска тестов мы можем использовать команду «go tool pprof» для анализа данных профилирования. Мы можем запустить инструмент pprof с помощью следующей команды:
Это откроет интерактивную оболочку pprof, где мы можем вводить различные команды для анализа данных профилирования. Например, мы можем использовать команду «top», чтобы отобразить функции, потребляющие больше всего процессорного времени:
Это отобразит список функций с наибольшим использованием процессорного времени, отсортированных по процессорному времени. В этом случае мы должны увидеть функцию «fibonacci» вверху списка, так как она потребляла больше всего процессорного времени во время теста.
Мы также можем использовать команду «web» для отображения данных профилирования в графическом формате и команду «list» для отображения исходного кода, аннотированного данными профилирования.
Профилирование — это мощный инструмент, который может помочь нам определить узкие места производительности в нашем коде. Используя флаг «-cpuprofile» и инструмент pprof, мы можем легко генерировать и анализировать данные профилирования для наших тестов и приложений Go.
Оптимизация
Компилятор Go выполняет несколько оптимизаций, включая встраивание, escape-анализ и удаление мёртвого кода. Встраивание — это процесс замены вызова функции телом функции, что может повысить производительность за счёт уменьшения расходов памяти на вызов функции. Escape-анализ — это процесс определения того, занят ли адрес переменной, что может помочь компилятору разместить его в стеке, а не в куче. Удаление мёртвого кода — это процесс удаления кода, который никогда не выполняется.
Встраивание
В первом примере функция add
вызывается с аргументами 3
и 4
, что приводит к накладным расходам на вызов функции. Во втором примере вызов функции заменяется фактическим кодом, что приводит к более быстрому выполнению.
Escape-анализ
В этом примере переменная a
размещается в стеке, так как её адрес не вызывается. Однако переменная b
выделяется в куче, так как её адрес вызывается оператором &
.
Подробнее о Escape-анализе
В функции createUser
создаётся новый User
и возвращается его адрес. Обратите внимание, что значение User
размещается в стеке с момента возврата его адреса, поэтому оно не уходит в кучу.
Если мы добавим строку, которая принимает адрес значения User
перед его возвратом:
Теперь адрес значения User
берётся и сохраняется в возвращаемой переменной. Это приводит к тому, что значение уходит в кучу, а не распределяется в стеке.
Escape-анализ важен, потому что выделение кучи дороже, чем выделение стека, поэтому минимизация выделения кучи может повысить производительность.
Удаление мёртвого кода
В этом примере код внутри оператора if
никогда не выполняется, поэтому он удаляется компилятором при устранении мёртвого кода.
Использование трассировщика GO
Трассировщик в Go может дать подробную информацию о том, что происходит в программе, включая трассировку стека, блокировку горутин и многое другое. Вот пример того, как его использовать:
В этом примере мы создаём файл трассировки, запускаем трассировку, а затем останавливаем её. При запуске программы данные трассировки будут записаны в файл trace.out
. Затем вы можете проанализировать эти данные трассировки, чтобы лучше понять, что происходит в вашей программе.
Управление памятью и настройка GC
В Go сборка мусора выполняется автоматически и управляется средой выполнения. Однако есть несколько способов настроить сборщик мусора для повышения производительности. Вот пример того, как установить некоторые его параметры:
В этом примере мы устанавливаем максимальное количество используемых ЦП, минимальный размер кучи и процент сбора мусора. Эти параметры можно изменить для повышения производительности в зависимости от потребностей вашей программы.
Конкурентность
Однако важно правильно использовать функции конкурентности, чтобы избежать таких проблем, как состояние гонки и взаимоблокировки. Вот пример того, как использовать каналы для безопасного общения между горутинами:
Оператор make(chan int
) создаёт канал, который используется для передачи целочисленного значения между двумя горутинами.
Первая горутина создаётся с оператором go func() {...}()
, который отправляет значение 1 в канал ch
после ожидания в течение 1 секунды. Это означает, что через 1 секунду канал ch
будет иметь значение 1.
Вторая горутина создаётся с оператором select
, который ожидает связи на канале ch
. Если из канала получено значение, выводится сообщение «Received message». Если значение не получено в течение 2 секунд, выводится сообщение «Timed out».
Таким образом, хотя между оператором и первой горутиной нет явной связи select
, всё же связь происходит через общий канал ch
.
Заключение
Надеюсь, что данная статья была полезна для вас!