v/vlib/x/benchmark/benchmark.v

254 lines
7.4 KiB
V

module benchmark
import time
import math
import sync
// Benchmark represent all significant data for benchmarking. Provide clear way for getting result in convinient way by exported methods
@[noinit]
pub struct Benchmark {
pub mut:
n i64 // Number of iterations. Set explicitly or computed from expected time of benchmarking
bench_func fn () ! @[required] // function for benchmarking
bench_time time.Duration // benchmark duration
is_parallel bool // if true every bench_func run in separate coroutine
benchmark_result BenchmarkResult // accumulator of benchmark metrics
timer_on bool // inner flag of time recording
start_time time.Time // start timestamp of timer
duration time.Duration // expected time of benchmark process
failed bool // flag of bench_func failure. true if one of bench_func run failed
start_memory usize // memory status on start benchmark
start_allocs usize // size of object allocated on heap
}
// BenchmarkDefaults is params struct for providing parameters of benchmarking to setup function
// - n - number of iterations. set if you know how many runs of function you need. if you don't know how many you need - set 0
// - duration - by default 1s. expecting duration of all benchmark runs. doesn't work if is_parallel == true
// - is_parallel - if true, every bench_func run in separate coroutine
@[params]
pub struct BenchmarkDefaults {
pub:
duration time.Duration = time.second
is_parallel bool
n i64
}
// Benchmark.new - constructor of benchmark
// arguments:
// - bench_func - function to benchmark. required, if you have no function - you don't need benchmark
// - params - structure of benchmark parameters
pub fn setup(bench_func fn () !, params BenchmarkDefaults) !Benchmark {
if bench_func == unsafe { nil } {
return error('Benchmark function cannot be `nil`')
}
if params.duration > 0 && params.is_parallel {
return error('can not predict number of parallel iterations')
}
return Benchmark{
n: params.n
bench_func: bench_func
bench_time: params.duration
is_parallel: params.is_parallel
}
}
// run_benchmark - function for start benchmarking
// run benchmark n times, or duration time
pub fn (mut b Benchmark) run() {
// run bench_func one time for heat up processor cache and get elapsed time for n prediction
b.run_n(1)
// if one iteration failed no need to do more
if b.failed {
b.n = 1
// show failed result. bad result is steel result
b.benchmark_result.print()
}
// if n is provided we should run exactly n times. but 1 time we already run
if b.n > 1 {
b.run_n(b.n - 1)
}
// if n is zero then we should run bench_func enough time for estimate duration time of execution
if b.n == 0 {
b.n = 1
// if one of runs failed - bench_func is not valid
// but 1e9 times of evaluation is too much
// so we need to repeat prediction-execition process while elapsed time less then expected time
for !b.failed && b.duration < b.bench_time && b.n < 1000000000 {
// we need predict new amount of executions to estimate expected time
n := b.predict_n()
// later we predict how many runs we need yet. so we run predicted times
b.run_n(n)
b.n += n
}
}
// if n is provided, duration will be calculated. otherwise n will
b.benchmark_result.n = b.n
b.benchmark_result.t = b.duration
// despite of the way of usage of benchmark result(send py api, send to chat, process, logging, etc), we print it
b.benchmark_result.print()
}
// run_n - run bench_func n times
fn (mut b Benchmark) run_n(n i64) {
// clear memory for avoid GC influence
gc_collect()
// reset and start timer for get elapsed time
b.reset_timer()
b.start_timer()
// unwrap function from struct field
mut f := b.bench_func
if !b.is_parallel {
// run n times consistently
for i := i64(0); i < n; i++ {
f() or {
// if one execution failed print err, set failed flag and stop execution
b.failed = true
// workaround for consider unsuccesful runs
b.n -= n - i
eprintln('Error: ${err}')
return
}
}
}
// spawn n coroutines, wait end of spawning and unpause all coroutines
if b.is_parallel {
// WaitGroup for spawn and pause enough coroutines
mut spawnwg := sync.new_waitgroup()
spawnwg.add(int(n))
// WaitGroup for wait of end of execution
mut workwg := sync.new_waitgroup()
workwg.add(int(n))
for i := i64(0); i < n; i++ {
spawn run_in_one_time(mut workwg, mut spawnwg, f)
spawnwg.done()
}
workwg.wait()
}
// stop timer and collect data
b.stop_timer()
}
fn run_in_one_time(mut workwg sync.WaitGroup, mut spawnwg sync.WaitGroup, f fn () !) {
defer {
workwg.done()
}
spawnwg.wait()
f() or { return } // TODO: add error handling
}
// predict_n - predict number of executions to estimate duration
// based on previous values
fn (mut b Benchmark) predict_n() i64 {
// goal duration in nanoseconds
mut goal_ns := b.bench_time.nanoseconds()
// get number of previous iterations
prev_iters := b.n
// get elapsed time in nanoseconds
mut prev_ns := b.duration.nanoseconds()
// to avoid division by zero
if prev_ns <= 0 {
prev_ns = 1
}
// multiple first to avoid division with less then 0 result
mut n := goal_ns * prev_iters
n = n / prev_ns
// grow at least in 1.2
n += n / 5
// to not grow to fast
n = math.min(n, 100 * b.n)
// to grow at least on 1
n = math.max(n, b.n + 1)
// to avoid run more then 1e9 times
n = math.min(n, 1000000000)
return n
}
// reset_timer - clear timer and reset memory start data
fn (mut b Benchmark) reset_timer() {
// if timer_on we should restart it
if b.timer_on {
b.start_time = time.now()
b.start_memory = gc_memory_use()
b.start_allocs = gc_heap_usage().bytes_since_gc
}
}
// starttimer - register start measures of memory
fn (mut b Benchmark) start_timer() {
// you do not need to start timer that already started
if !b.timer_on {
b.start_time = time.now()
b.start_memory = gc_memory_use()
b.start_allocs = gc_heap_usage().bytes_since_gc
b.timer_on = true
}
}
// stop_timer - accumulate menchmark data
fn (mut b Benchmark) stop_timer() {
if b.timer_on {
// accumulate delta time of execution
b.duration += time.since(b.start_time)
// accumulate memory growth
b.benchmark_result.mem += gc_memory_use() - b.start_memory
// accumulate heap usage
b.benchmark_result.allocs += gc_heap_usage().bytes_since_gc - b.start_allocs
b.timer_on = false
}
}
// BenchmarkResult - struct for represent result of benchmark
struct BenchmarkResult {
pub mut:
n i64 // iterations count
t time.Duration // elapsed time
mem usize // all allocated memory
allocs usize // heap allocated memory
}
// ns_per_op - elapsed time in nanoseconds per iteration
fn (r BenchmarkResult) ns_per_op() i64 {
if r.n <= 0 {
return 0
}
return r.t.nanoseconds() / i64(r.n)
}
// allocs_per_op - heap usage per iteration
fn (r BenchmarkResult) allocs_per_op() i64 {
if r.n <= 0 {
return 0
}
return i64(r.allocs) / i64(r.n)
}
// alloced_bytes_per_op - memory usage per iteration
fn (r BenchmarkResult) alloced_bytes_per_op() i64 {
if r.n <= 0 {
return 0
}
return i64(r.mem) / i64(r.n)
}
// print - all measurements
fn (r BenchmarkResult) print() {
println('Iterations: ${r.n:10}\t\tTotal Duration: ${r.t:10}\tns/op: ${r.ns_per_op():10}\tB/op: ${r.alloced_bytes_per_op():6}\tallocs/op: ${r.allocs_per_op():6}')
}