Модуль Benchmark в Ruby

Published on 23 Nov 2024

Каждому разработчику важно уметь оптимизировать свой код.

В языке Ruby в стандартной библиотеке уже существует необходимый инстуремент для измерения производительности кода - модуль Benchmark (документация).

Ниже можно увидеть пример его использования из документации:

require 'benchmark'
puts Benchmark.measure { 'a' * 1_000_000_000 }
# =>  0.365794   0.271074   0.636868 (  0.644437)

Чтобы лучше понять как использовать данный инструмент, далее мы будем сравнивать 2 способа изменения массива:

Benchmark.measure

Самый простой способ измерить производительность кода - вызов Benchmark.measure. Как уже было показано ранее, в метод необходимо передать блок кода, который будет измеряться. Чтобы увидеть результат метода, необходимо вызвать команду puts. Давайте посмотрим примеры использования и результаты измерений на наших функциях:

require 'benchmark'
range = (1..10_000_000)
puts Benchmark.measure { increase_using_map(range) }
puts Benchmark.measure { increase_using_each(range) }
# 0.386068   0.010744   0.396812 (  0.396839)
# 0.303948   0.007739   0.311687 (  0.311698)
# => nil

Этих данных достаточно, чтобы понять, что метод increase_using_each исполнился быстрее, чем increase_using_map. Однако есть метод более удобный для сравнения, который мы рассмотрим далее.

Benchmark.bm

Benchmark.bm - более продвинутый метод для сравнения производительности блоков кода, который позволяет видеть результаты в одном месте. В отличие от простого использования Benchmark.measure, здесь каждый блок кода оборачивается в bm.report — нужно указать название отчета и передать сам блок, который мы измеряем. Ниже представлен пример его использования:

require 'benchmark'
range = (1..10_000_000)
Benchmark.bm do |bm|
  bm.report('.map') do
    increase_using_map(range)
  end
  bm.report('.each') do
    increase_using_each(range)
  end
end
#         user     system      total        real
#  .map  0.379351   0.009624   0.388975 (  0.388993)
#  .each  0.306710   0.008468   0.315178 (  0.315204)
# =>
# [#<Benchmark::Tms:0x00000001008baed8
#   @cstime=0.0,
#   @cutime=0.0,
#   @label=".map",
#   @real=0.38899300002958626,
#   @stime=0.009623999999999994,
#   @total=0.38897500000000007,
#   @utime=0.37935100000000005>,
#  #<Benchmark::Tms:0x00000001008b8188
#   @cstime=0.0,
#   @cutime=0.0,
#   @label=".each",
#   @real=0.3152039999840781,
#   @stime=0.00846799999999999,
#   @total=0.315178,
#   @utime=0.30671000000000004>]

Как видите, для каждого блока кода вызывается bm.report, где первым аргументом передаётся описание замера, что добавляет наглядности. При этом нет необходимости использовать puts для вывода, так как Benchmark.bm автоматически форматирует результаты в виде таблицы. Кроме того, этот метод возвращает массив объектов класса Benchmark::Tms — специальных объектов (data object), которые представляют собой детализированные результаты измерения и позволяют легко работать с ними, как с объектами. Подробности можно найти в документации Benchmark::Tms.

Benchmark.bmbm

При измерении производительности код иногда сталкивается с накладными расходами на сбор мусора, что может искажать результаты. Чтобы минимизировать эти помехи, существует метод Benchmark.bmbm.

require 'benchmark'
range = (1..10_000_000)
Benchmark.bmbm do |bm|
  bm.report('.map') do
    increase_using_map(range)
  end
  bm.report('.each') do
    increase_using_each(range)
  end
end
# Rehearsal -----------------------------------------
# .map    0.382209   0.009926   0.392135 (  0.392142)
# .each   0.304083   0.009298   0.313381 (  0.313406)
# -------------------------------- total: 0.705516sec
# 
#             user     system      total        real
# .map    0.341568   0.007595   0.349163 (  0.349168)
# .each   0.306042   0.009010   0.315052 (  0.315067)
# =>
# [#<Benchmark::Tms:0x0000000100e8ec88
#   @cstime=0.0,
#   @cutime=0.0,
#   @label=".map",
#   @real=0.34916800004430115,
#   @stime=0.007594999999999796,
#   @total=0.34916299999999945,
#   @utime=0.34156799999999965>,
#  #<Benchmark::Tms:0x0000000100e8ed78
#   @cstime=0.0,
#   @cutime=0.0,
#   @label=".each",
#   @real=0.3150669999886304,
#   @stime=0.009009999999999962,
#   @total=0.31505200000000055,
#   @utime=0.3060420000000006>]

Benchmark.bmbm сначала проводит репетицию (Rehearsal), выполняя каждый блок кода, чтобы стабилизировать среду выполнения и уменьшить влияние «холодного» запуска. Затем запускается реальное измерение. Перед ним вызывается GC.start, чтобы сборщик мусора Ruby выполнил свою работу заранее и не вмешивался в результаты измерений.

Данный метод позволяет более точно измерить время выполнения кода, однако не гарантирует 100% изоляции от сторонних эфеектов.

Как интерпретировать результаты?

Теперь мы умеем измерять производительность кода, но давайте разберёмся, что означает каждый столбец в результатах:

В каких случая стоит использовать модуль Benchmark?

  1. Сравнение алгоритмов сортировки
  2. Оптимизация запросов к БД
  3. Оптимизация фоновых задач
  4. Оптимизация производительности веб-запросов
  5. Оптимизация работы с коллекциями данных (как в наших примерах) и др.

Заключение

Модуль Benchmark в Ruby — это мощный и удобный инструмент, позволяющий разработчикам анализировать и оптимизировать производительность кода. Важно понимать, что измерение времени выполнения — это не просто способ сделать код «быстрее». Они помогают находить узкие места и принимать обоснованные решения о том, какие части приложения требуют внимания для достижения высокой производительности и улучшения пользовательского опыта.