Benchmark 모듈

Benchmark 모듈

Ruby 코드를 실행하는 데 걸린 시간을 측정하고 보고하는 메서드를 제공하는 모듈이에요.

require 'benchmark'

puts Benchmark.measure { "a"*1_000_000_000 }

이 코드는 이렇게 출력해요.

0.350000   0.400000   0.750000 (  0.835234)

이 보고는 사용자 CPU 시간, 시스템 CPU 시간, 이 둘의 합, 그리고 경과한 실제 시간(elapsed real time)을 보여줘요. 시간 단위는 초예요.

출처: Ruby 3.3 API

본문

bm 메서드로 실험 순차 실행하기

require 'benchmark'

n = 5000000
Benchmark.bm do |x|
  x.report { for i in 1..n; a = "1"; end }
  x.report { n.times do   ; a = "1"; end }
  x.report { 1.upto(n) do ; a = "1"; end }
end

결과:

    user     system      total        real
1.010000   0.000000   1.010000 (  1.014479)
1.000000   0.000000   1.000000 (  0.998261)
0.980000   0.000000   0.980000 (  0.981335)

각 보고서에 라벨 붙이기

앞선 예시를 이어서, 각 보고서에 라벨을 붙여볼게요.

require 'benchmark'

n = 5000000
Benchmark.bm(7) do |x|
  x.report("for:")   { for i in 1..n; a = "1"; end }
  x.report("times:") { n.times do   ; a = "1"; end }
  x.report("upto:")  { 1.upto(n) do ; a = "1"; end }
end

결과:

              user     system      total        real
for:      1.010000   0.000000   1.010000 (  1.015688)
times:    1.000000   0.000000   1.000000 (  1.003611)
upto:     1.030000   0.000000   1.030000 (  1.028098)

bmbm: 가비지 컬렉션 영향 줄이기

일부 벤치마크의 시간은 항목이 실행되는 순서에 따라 달라져요. 이 차이는 메모리 할당과 가비지 컬렉션 비용 때문이에요. 이런 불일치를 피하기 위해 bmbm 메서드가 제공돼요. 예를 들어 float 배열을 정렬하는 방법들을 비교해볼게요.

require 'benchmark'

array = (1..1000000).map { rand }

Benchmark.bmbm do |x|
  x.report("sort!") { array.dup.sort! }
  x.report("sort")  { array.dup.sort  }
end

결과:

Rehearsal -----------------------------------------
sort!   1.490000   0.010000   1.500000 (  1.490520)
sort    1.460000   0.000000   1.460000 (  1.463025)
-------------------------------- total: 2.960000sec

            user     system      total        real
sort!   1.460000   0.000000   1.460000 (  1.460465)
sort    1.450000   0.010000   1.460000 (  1.448327)

benchmark 메서드로 고급 보고

require 'benchmark'
include Benchmark         # we need the CAPTION and FORMAT constants

n = 5000000
Benchmark.benchmark(CAPTION, 7, FORMAT, ">total:", ">avg:") do |x|
  tf = x.report("for:")   { for i in 1..n; a = "1"; end }
  tt = x.report("times:") { n.times do   ; a = "1"; end }
  tu = x.report("upto:")  { 1.upto(n) do ; a = "1"; end }
  [tf+tt+tu, (tf+tt+tu)/3]
end

결과:

             user     system      total        real
for:      0.950000   0.000000   0.950000 (  0.952039)
times:    0.980000   0.000000   0.980000 (  0.984938)
upto:     0.950000   0.000000   0.950000 (  0.946787)
>total:   2.880000   0.000000   2.880000 (  2.883764)
>avg:     0.960000   0.000000   0.960000 (  0.961255)

Constants

  • CAPTION: 기본 캡션 문자열(출력 시간 위의 제목 행).
  • FORMAT: 시간 표시에 사용되는 기본 포맷 문자열. Benchmark::Tms#format도 참고하세요.

Public Class Methods

benchmark(caption = "", label_width = nil, format = nil, *labels) { |report| ... }

블록에 Benchmark::Report 객체를 전달해 호출하는데, 이 객체는 개별 벤치마크 테스트 결과를 수집·보고하는 데 사용돼요. 각 줄의 라벨에 label_width만큼 선행 공백을 확보해요. 보고서 맨 위에 caption을 출력하고, format으로 각 줄을 포맷해요. (주의: caption은 끝에 개행 문자를 포함해야 해요. 기본 Benchmark::Tms::CAPTION을 참고하세요.)

Benchmark::Tms 객체 배열을 반환해요.

블록이 Benchmark::Tms 객체 배열을 반환하면, 이 객체들을 사용해 추가 출력 줄을 포맷해요. labels 파라미터가 주어지면 이 추가 줄들을 라벨링하는 데 사용돼요.

주의: 다른 메서드들이 이 메서드에 대한 더 단순한 인터페이스를 제공하며, 거의 모든 벤치마킹 요구에 적합해요. Benchmark의 예시와 bm, bmbm 메서드를 참고하세요.

예시:

require 'benchmark'
include Benchmark          # we need the CAPTION and FORMAT constants

n = 5000000
Benchmark.benchmark(CAPTION, 7, FORMAT, ">total:", ">avg:") do |x|
  tf = x.report("for:")   { for i in 1..n; a = "1"; end }
  tt = x.report("times:") { n.times do   ; a = "1"; end }
  tu = x.report("upto:")  { 1.upto(n) do ; a = "1"; end }
  [tf+tt+tu, (tf+tt+tu)/3]
end

생성 결과:

              user     system      total        real
for:      0.970000   0.000000   0.970000 (  0.970493)
times:    0.990000   0.000000   0.990000 (  0.989542)
upto:     0.970000   0.000000   0.970000 (  0.972854)
>total:   2.930000   0.000000   2.930000 (  2.932889)
>avg:     0.976667   0.000000   0.976667 (  0.977630)
# File lib/benchmark.rb, line 170
def benchmark(caption = "", label_width = nil, format = nil, *labels) # :yield: report
  sync = $stdout.sync
  $stdout.sync = true
  label_width ||= 0
  label_width += 1
  format ||= FORMAT
  print ' '*label_width + caption unless caption.empty?
  report = Report.new(label_width, format)
  results = yield(report)
  Array === results and results.grep(Tms).each {|t|
    print((labels.shift || t.label || "").ljust(label_width), t.format(format))
  }
  report.list
ensure
  $stdout.sync = sync unless sync.nil?
end

bm(label_width = 0, *labels) { |report| ... }

benchmark 메서드에 대한 단순한 인터페이스로, bm은 라벨이 있는 순차 보고서를 생성해요. label_widthlabels 파라미터는 benchmark와 같은 의미예요.

require 'benchmark'

n = 5000000
Benchmark.bm(7) do |x|
  x.report("for:")   { for i in 1..n; a = "1"; end }
  x.report("times:") { n.times do   ; a = "1"; end }
  x.report("upto:")  { 1.upto(n) do ; a = "1"; end }
end

생성 결과:

              user     system      total        real
for:      0.960000   0.000000   0.960000 (  0.957966)
times:    0.960000   0.000000   0.960000 (  0.960423)
upto:     0.950000   0.000000   0.950000 (  0.954864)
# File lib/benchmark.rb, line 209
def bm(label_width = 0, *labels, &blk) # :yield: report
  benchmark(CAPTION, label_width, FORMAT, *labels, &blk)
end

bmbm(width = 0) { |job| ... }

때로는 앞서 실행된 코드가 나중에 실행된 코드보다 다른 가비지 컬렉션 오버헤드를 만나기 때문에 벤치마크 결과가 왜곡될 수 있어요. bmbm은 테스트를 두 번 실행해서 이 영향을 최소화하려 해요. 첫 번째는 실행 환경을 안정화하기 위한 리허설, 두 번째는 실제 측정이에요. 각 실제 측정이 시작되기 전에 GC.start가 실행되며, 이 비용은 측정에 포함되지 않아요. 물론 현실적으로 bmbm이 할 수 있는 건 제한적이고, 결과가 가비지 컬렉션 및 다른 영향에서 완전히 격리된다는 보장은 없어요.

bmbm은 테스트를 두 번 통과하므로 필요한 라벨 폭을 계산할 수 있어요.

require 'benchmark'

array = (1..1000000).map { rand }

Benchmark.bmbm do |x|
  x.report("sort!") { array.dup.sort! }
  x.report("sort")  { array.dup.sort  }
end

생성 결과:

Rehearsal -----------------------------------------
sort!   1.440000   0.010000   1.450000 (  1.446833)
sort    1.440000   0.000000   1.440000 (  1.448257)
-------------------------------- total: 2.890000sec

            user     system      total        real
sort!   1.460000   0.000000   1.460000 (  1.458065)
sort    1.450000   0.000000   1.450000 (  1.455963)

bmbmBenchmark::Job 객체를 yield 하고 Benchmark::Tms 객체 배열을 반환해요.

# File lib/benchmark.rb, line 251
def bmbm(width = 0) # :yield: job
  job = Job.new(width)
  yield(job)
  width = job.width + 1
  sync = $stdout.sync
  $stdout.sync = true

  # rehearsal
  puts 'Rehearsal '.ljust(width+CAPTION.length,'-')
  ets = job.list.inject(Tms.new) { |sum,(label,item)|
    print label.ljust(width)
    res = Benchmark.measure(&item)
    print res.format
    sum + res
  }.format("total: %tsec")
  print " #{ets}\n\n".rjust(width+CAPTION.length+2,'-')

  # take
  print ' '*width + CAPTION
  job.list.map { |label,item|
    GC.start
    print label.ljust(width)
    Benchmark.measure(label, &item).tap { |res| print res }
  }
ensure
  $stdout.sync = sync unless sync.nil?
end

measure(label = "") { || ... }

주어진 블록을 실행하는 데 사용된 시간을 Benchmark::Tms 객체로 반환해요. label 옵션을 받아요.

require 'benchmark'

n = 1000000

time = Benchmark.measure do
  n.times { a = "1" }
end
puts time

생성 결과:

0.220000   0.000000   0.220000 (  0.227313)
# File lib/benchmark.rb, line 296
def measure(label = "") # :yield:
  t0, r0 = Process.times, Process.clock_gettime(Process::CLOCK_MONOTONIC)
  yield
  t1, r1 = Process.times, Process.clock_gettime(Process::CLOCK_MONOTONIC)
  Benchmark::Tms.new(t1.utime  - t0.utime,
                     t1.stime  - t0.stime,
                     t1.cutime - t0.cutime,
                     t1.cstime - t0.cstime,
                     r1 - r0,
                     label)
end

realtime() { || ... }

주어진 블록을 실행하는 데 사용된 경과 실제 시간을 반환해요.

# File lib/benchmark.rb, line 311
def realtime # :yield:
  r0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
  yield
  Process.clock_gettime(Process::CLOCK_MONOTONIC) - r0
end

(이 모듈의 메서드들은 module_function으로 정의되어, 모듈 자체의 public 클래스 메서드이자 private 인스턴스 메서드로도 쓰여요. 위의 bm, bmbm, measure, realtime 등은 include Benchmark 후 인스턴스 메서드로도 호출할 수 있어요.)