benchmark-trend 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3396e746a6b1b03c60abcfce59c39bf7ad59eb182092b6900bd5cef6d9a21b8d
4
- data.tar.gz: b6dc71b8aa5aa02dd45d6cfe4e3c423006c0aa153573a84f1f476d13dbee5b57
3
+ metadata.gz: 2684a8a9c5ed41c53e7510602d352dccb111150e7ce757567f3e1340a679eb5f
4
+ data.tar.gz: 8b7363a4ca90e53557d1a4397e30a5a37a93515976a42773c5225d1715e00ae3
5
5
  SHA512:
6
- metadata.gz: 03466650b2858047f192477c9c69e9d4f41b27ca6138acfed84f6ef2024346672b3b7d087aabd85d8259c24d858410e6b8efaa05d4669a607a70d824136295c7
7
- data.tar.gz: 8036b5662f6392c6b13a9d8d69a03b700294e909f1496de46c8582a95fbdcbcf6d62b61e901d6af1b659e2056d5e724d185ab2c061dc3750d32c7857de9c9843
6
+ metadata.gz: 4d27391b226536bb79d6dbe5e75ba3d66cf2362993b9d6afcb5409ea2a8a999deef276d7708a6c0bfa9b28eeeaffd9f31d8b2238e9f3daff21e3357baa80397d
7
+ data.tar.gz: bdae380f8bdaf37432cda9f03820421fb9f7fb00f9633d0c8a9a0b5b575f566ef905f9ec70ab83c4d0eaeb64162c2a43d7fe659722c90576db40ad621a553ed6
@@ -1,7 +1,23 @@
1
1
  # Change log
2
2
 
3
+ ## [v0.2.0] - 2018-09-30
4
+
5
+ ### Added
6
+ * Add ability to measure monotonic time
7
+ * Add ability to repeat measurements to increase stability of execution times
8
+
9
+ ### Changed
10
+ * Change to prefer simpler complexity for similar measurements
11
+ * Change to use monotonic clock
12
+ * Change to differentiate linear vs logarithmic complexity for small values
13
+ * Change to differentiate linear vs constant complexity for small values
14
+
15
+ ## Fixed
16
+ * Fix fit_power to correctly calculate slope and intercept
17
+
3
18
  ## [v0.1.0] - 2018-09-08
4
19
 
5
20
  * Inital implementation and release
6
21
 
22
+ [v0.2.0]: https://github.com/piotrmurach/benchmark-trend/compare/v0.1.0...v0.2.0
7
23
  [v0.1.0]: https://github.com/piotrmurach/benchmark-trend/compare/v0.1.0
data/README.md CHANGED
@@ -14,7 +14,7 @@
14
14
  [coverage]: https://coveralls.io/github/piotrmurach/benchmark-trend?branch=master
15
15
  [inchpages]: http://inch-ci.org/github/piotrmurach/benchmark-trend
16
16
 
17
- > Measure pefromance trends of Ruby code based on the input size distribution.
17
+ > Measure performance trends of Ruby code based on the input size distribution.
18
18
 
19
19
  **Benchmark::Trend** will help you estimate the computational complexity of Ruby code by running it on inputs increasing in size, measuring their execution times, and then fitting these observations into a model that best predicts how a given Ruby code will scale as a function of growing workload.
20
20
 
@@ -43,11 +43,14 @@ Or install it yourself as:
43
43
  ## Contents
44
44
 
45
45
  * [1. Usage](#1-usage)
46
- * [2. API](#2--api)
46
+ * [2. API](#2-api)
47
47
  * [2.1 range](#21-range)
48
48
  * [2.2 infer_trend](#22-infer_trend)
49
+ * [2.2.1 repeat](#221-repeat)
49
50
  * [2.3 fit](#23-fit)
50
51
  * [2.4 fit_at](#24-fit_at)
52
+ * [3. Examples](#3-examples)
53
+ * [3.1 Ruby array max](#31-ruby-array-max)
51
54
 
52
55
  ## 1. Usage
53
56
 
@@ -59,12 +62,12 @@ def fibonacci(n)
59
62
  end
60
63
  ```
61
64
 
62
- To measure the actual complexity of above function, we will use `infer_tren` method and pass it as a first argument an array of integer sizes and a block to execute the method:
65
+ To measure the actual complexity of above function, we will use `infer_trend` method and pass it as a first argument an array of integer sizes and a block to execute the method:
63
66
 
64
67
  ```ruby
65
68
  numbers = Benchmark::Trend.range(1, 28, ratio: 2)
66
69
 
67
- trend, trends = Benchmark::Trend.infer_trend(numbers) do |n|
70
+ trend, trends = Benchmark::Trend.infer_trend(numbers) do |n, i|
68
71
  fibonacci(n)
69
72
  end
70
73
  ```
@@ -134,7 +137,7 @@ Benchmark::Trend.range(8, 8 << 10, ratio: 2)
134
137
 
135
138
  ### 2.2 infer_trend
136
139
 
137
- To calculate an asymptotic behaviour of Rub code by inferring its computational complexity use `infer_trend`. This method takes as an argument an array of inputs which can be generated using [range](#21-range). The code to measure needs to be provided inside a block.
140
+ To calculate an asymptotic behaviour of Ruby code by inferring its computational complexity use `infer_trend`. This method takes as an argument an array of inputs which can be generated using [range](#21-range). The code to measure needs to be provided inside a block. Two parameters are always yielded to a block, first, the actual data input and second the current index matching the input.
138
141
 
139
142
  For example, let's assume you would like to find out asymptotic behaviour of a Fibonacci algorithm:
140
143
 
@@ -154,7 +157,7 @@ numbers = Benchmark::Trend.range(1, 32, ratio: 2)
154
157
  Then measure the performance of the Fibonacci algorithm for each of the data points and fit the observations into a model to predict behaviour as a function of input size:
155
158
 
156
159
  ```ruby
157
- trend, trends = Benchmark::Trend.infer_trend(numbers) do |n|
160
+ trend, trends = Benchmark::Trend.infer_trend(numbers) do |n, i|
158
161
  fibonacci(n)
159
162
  end
160
163
  ```
@@ -204,6 +207,23 @@ print trends[trend]
204
207
  # :residual=>0.9052392775178072}
205
208
  ```
206
209
 
210
+ ### 2.2.1 repeat
211
+
212
+ To increase stability of you tests consider repeating all time execution measurements using `:repeat` keyword.
213
+
214
+ Start by generating a range of inputs for your algorithm:
215
+
216
+ ```ruby
217
+ numbers = Benchmark::Trend.range(1, 32, ratio: 2)
218
+ # => [1, 2, 4, 8, 16, 32]
219
+ ```
220
+
221
+ and then run your algorithm for each input repeating measurements `100` times:
222
+
223
+ ```ruby
224
+ Benchmark::Trend.infer_trend(numbers, repeat: 100) { |n, i| ... }
225
+ ```
226
+
207
227
  ### 2.3 fit
208
228
 
209
229
  Use `fit` method if you wish to fit arbitrary data into a model with a slope and intercept parameters that minimize the error.
@@ -264,6 +284,51 @@ Benchamrk::Trend.fit_at(:exponential, slope: 1.382889711685203, intercept: 3.822
264
284
 
265
285
  This means Fibonacci recursive algorithm will take about 1.45 year to complete!
266
286
 
287
+ ## 3. Examples
288
+
289
+ ### 3.1 Ruby array max
290
+
291
+ Suppose you wish to find an asymptotic behaviour of Ruby built Array `max` method.
292
+
293
+ You could start with generating a [range](#21-range) of inputs:
294
+
295
+ ```ruby
296
+ array_sizes = Benchmark::Trend.range(1, 100_000)
297
+ # => [1, 8, 64, 512, 4096, 32768, 100000]
298
+ ```
299
+
300
+ Next, based on the generated ranges create arrays containing randomly generated integers:
301
+
302
+ ```ruby
303
+ number_arrays = array_sizes.map { |n| Array.new(n) { rand(n) } }
304
+ ```
305
+
306
+ Then feed this information to infer a trend:
307
+
308
+ ```ruby
309
+ trend, trends = Benchmark::Trend.infer_trend(array_sizes) do |n, i|
310
+ number_arrays[i].max
311
+ end
312
+ ```
313
+
314
+ Unsuprisingly, we discover that Ruby's `max` call scales linearily with the input size:
315
+
316
+ ```ruby
317
+ print trend
318
+ # => linear
319
+ ```
320
+
321
+ We can also see from the residual value that this is a near perfect fit:
322
+
323
+ ```ruby
324
+ print trends[trend]
325
+ # =>
326
+ # {:trend=>"0.00 + 0.00*x",
327
+ # :slope=>5.873536409841244e-09,
328
+ # :intercept=>3.028647045635842e-05,
329
+ # :residual=>0.9986764704492359}
330
+ ```
331
+
267
332
  ## Development
268
333
 
269
334
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -0,0 +1,16 @@
1
+ require_relative '../lib/benchmark-trend'
2
+
3
+ # constant
4
+ def fib_const(n)
5
+ phi = (1 + Math.sqrt(5))/2
6
+ (phi ** n / Math.sqrt(5)).round
7
+ end
8
+
9
+ numbers = Benchmark::Trend.range(1, 1400, ratio: 2)
10
+ trend, trends = Benchmark::Trend.infer_trend(numbers, repeat: 100) do |n|
11
+ fib_const(n)
12
+ end
13
+
14
+ puts "Trend: #{trend}"
15
+ puts "Trend data:"
16
+ pp trends
@@ -0,0 +1,17 @@
1
+ require_relative '../lib/benchmark-trend'
2
+
3
+ # linear
4
+ def fib_iter(n)
5
+ a, b = 0, 1
6
+ n.times { a, b = b, a + b}
7
+ a
8
+ end
9
+
10
+ numbers = Benchmark::Trend.range(1, 20_000)
11
+ trend, trends = Benchmark::Trend.infer_trend(numbers) do |n|
12
+ fib_iter(n)
13
+ end
14
+
15
+ puts "Trend: #{trend}"
16
+ puts "Trend data:"
17
+ pp trends
@@ -14,6 +14,31 @@ module Benchmark
14
14
  private_class_method(method)
15
15
  end
16
16
 
17
+ if defined?(Process::CLOCK_MONOTONIC)
18
+ # Object representing current time
19
+ def time_now
20
+ Process.clock_gettime Process::CLOCK_MONOTONIC
21
+ end
22
+ module_function :time_now
23
+ else
24
+ # Object represeting current time
25
+ def time_now
26
+ Time.now
27
+ end
28
+ module_function :time_now
29
+ end
30
+
31
+ # Measure time elapsed with a monotonic clock
32
+ #
33
+ # @public
34
+ def clock_time
35
+ before = time_now
36
+ yield
37
+ after = time_now
38
+ after - before
39
+ end
40
+ module_function :clock_time
41
+
17
42
  # Generate a range of inputs spaced by powers.
18
43
  #
19
44
  # The default range is generated in the multiples of 8.
@@ -66,18 +91,25 @@ module Benchmark
66
91
  # @param [Array[Numeric]] data
67
92
  # the data to run measurements for
68
93
  #
94
+ # @param [Integer] repeat
95
+ # nubmer of times work is called to compute execution time
96
+ #
69
97
  # @return [Array[Array, Array]]
70
98
  #
71
99
  # @api public
72
- def measure_execution_time(data = nil, &work)
100
+ def measure_execution_time(data = nil, repeat: 1, &work)
73
101
  inputs = data || range(1, 10_000)
74
102
  times = []
75
103
 
76
- inputs.each do |input|
104
+ inputs.each_with_index do |input, i|
77
105
  GC.start
78
- times << ::Benchmark.realtime do
79
- work.(input)
106
+ measurements = []
107
+
108
+ repeat.times do
109
+ measurements << clock_time { work.(input, i) }
80
110
  end
111
+
112
+ times << measurements.reduce(&:+).to_f / measurements.size
81
113
  end
82
114
  [inputs, times]
83
115
  end
@@ -126,7 +158,7 @@ module Benchmark
126
158
 
127
159
  # Finds a line of best fit that approxmimates power function
128
160
  #
129
- # Function form: y = ax^b
161
+ # Function form: y = bx^a
130
162
  #
131
163
  # @return [Numeric, Numeric, Numeric]
132
164
  # returns a, b, and rr values
@@ -136,7 +168,7 @@ module Benchmark
136
168
  a, b, rr = fit(xs, ys, tran_x: ->(x) { Math.log(x) },
137
169
  tran_y: ->(y) { Math.log(y) })
138
170
 
139
- [Math.exp(b), a, rr]
171
+ [a, Math.exp(b), rr]
140
172
  end
141
173
  module_function :fit_power
142
174
 
@@ -172,7 +204,7 @@ module Benchmark
172
204
  #
173
205
  # @api public
174
206
  def fit(xs, ys, tran_x: ->(x) { x }, tran_y: ->(y) { y })
175
- eps = 0.000001
207
+ eps = (10 ** -10)
176
208
  n = 0
177
209
  sum_x = 0.0
178
210
  sum_x2 = 0.0
@@ -193,9 +225,11 @@ module Benchmark
193
225
  tx = n * sum_x2 - sum_x ** 2
194
226
  ty = n * sum_y2 - sum_y ** 2
195
227
 
228
+ is_linear = tran_x.(Math::E) * tran_y.(Math::E) == Math::E ** 2
229
+
196
230
  if tx.abs < eps # no variation in xs
197
231
  raise ArgumentError, "No variation in data #{xs}"
198
- elsif ty.abs < eps # no variation in ys - constant fit
232
+ elsif ty.abs < eps && is_linear # no variation in ys - constant fit
199
233
  slope = 0
200
234
  intercept = sum_y / n
201
235
  residual_sq = 1 # doesn't exist
@@ -249,7 +283,7 @@ module Benchmark
249
283
  case type
250
284
  when :logarithmic, :log
251
285
  "%.2f + %.2f*ln(x)"
252
- when :linear
286
+ when :linear, :constant
253
287
  "%.2f + %.2f*x"
254
288
  when :power
255
289
  "%.2f * x^%.2f"
@@ -268,14 +302,18 @@ module Benchmark
268
302
  #
269
303
  # Fits the executiom times for each range to several fit models.
270
304
  #
305
+ # @param [Integer] repeat
306
+ # nubmer of times work is called to compute execution time
307
+ #
271
308
  # @yieldparam work
309
+ # the block of which the complexity is measured
272
310
  #
273
311
  # @return [Array[Symbol, Hash]]
274
312
  # the best fitting and all the trends
275
313
  #
276
314
  # @api public
277
- def infer_trend(data, &work)
278
- ns, times = *measure_execution_time(data, &work)
315
+ def infer_trend(data, repeat: 1, &work)
316
+ ns, times = *measure_execution_time(data, repeat: repeat, &work)
279
317
  best_fit = :none
280
318
  best_residual = 0
281
319
  fitted = {}
@@ -287,9 +325,12 @@ module Benchmark
287
325
  a, b, rr = *send(:"fit_#{fit}", ns, times)
288
326
  # goodness of model
289
327
  aic = n * (Math.log(Math::PI) + 1) + n * Math.log(rr / n)
328
+ if a == 0 && fit == :linear
329
+ fit = :constant
330
+ end
290
331
  fitted[fit] = { trend: format_fit(fit) % [a, b],
291
332
  slope: a, intercept: b, residual: rr }
292
- if rr > best_residual && aic > best_aic
333
+ if rr >= best_residual && aic >= best_aic
293
334
  best_residual = rr
294
335
  best_fit = fit
295
336
  best_aic = aic
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Benchmark
4
4
  module Trend
5
- VERSION = "0.1.0"
5
+ VERSION = "0.2.0"
6
6
  end # Trend
7
7
  end # Benchmark
@@ -3,12 +3,12 @@
3
3
  RSpec.describe Benchmark::Trend, '#fit_power' do
4
4
  it 'calculates perfect power fit' do
5
5
  xs = [1, 2, 3, 4, 5]
6
- ys = xs.map { |x| 1.5*(x ** 2) }
6
+ ys = xs.map { |x| 1.5 * (x ** 2) }
7
7
 
8
8
  a, b, rr = Benchmark::Trend.fit_power(xs, ys)
9
9
 
10
- expect(a).to be_within(0.001).of(1.5)
11
- expect(b).to be_within(0.001).of(2.0)
10
+ expect(a).to be_within(0.001).of(2.0)
11
+ expect(b).to be_within(0.001).of(1.5)
12
12
  expect(rr).to be_within(0.001).of(1.0)
13
13
  end
14
14
 
@@ -19,8 +19,8 @@ RSpec.describe Benchmark::Trend, '#fit_power' do
19
19
 
20
20
  a, b, rr = Benchmark::Trend.fit_power(xs, ys)
21
21
 
22
- expect(a).to be_within(0.001).of(1.0)
23
- expect(b).to be_within(0.001).of(1.5)
22
+ expect(a).to be_within(0.001).of(1.5)
23
+ expect(b).to be_within(0.001).of(1.0)
24
24
  expect(rr).to be_within(0.001).of(0.999)
25
25
  end
26
26
  end
@@ -23,15 +23,23 @@ RSpec.describe Benchmark::Trend, '#infer_trend' do
23
23
  a
24
24
  end
25
25
 
26
- # constant
26
+ # logarithmic
27
27
  def fib_const(n)
28
28
  phi = (1 + Math.sqrt(5))/2
29
29
  (phi ** n / Math.sqrt(5)).round
30
30
  end
31
31
 
32
+ it "infers constant trend" do
33
+ numbers = Benchmark::Trend.range(1, 100_000)
34
+ trend, = Benchmark::Trend.infer_trend(numbers, repeat: 100) do |n|
35
+ n
36
+ end
37
+
38
+ expect(trend).to eq(:constant)
39
+ end
40
+
32
41
  it "infers fibonacci classic algorithm trend to be exponential" do
33
- numbers = Benchmark::Trend.range(1, 28, ratio: 2)
34
- trend, trends = Benchmark::Trend.infer_trend(numbers) do |n|
42
+ trend, trends = Benchmark::Trend.infer_trend((1..20), repeat: 10) do |n|
35
43
  fibonacci(n)
36
44
  end
37
45
 
@@ -52,29 +60,38 @@ RSpec.describe Benchmark::Trend, '#infer_trend' do
52
60
  expect(trend).to eq(:linear)
53
61
  end
54
62
 
55
- it "infers fibonacci constant algorithm trend to be linear" do
56
- numbers = Benchmark::Trend.range(1, 500)
57
- trend, trends = Benchmark::Trend.infer_trend(numbers) do |n|
63
+ it "infers fibonacci constant algorithm trend to be constant" do
64
+ # exponetiation by squaring has logarithmic complexity
65
+ numbers = Benchmark::Trend.range(1, 1400, ratio: 2)
66
+ trend, trends = Benchmark::Trend.infer_trend(numbers, repeat: 100) do |n|
58
67
  fib_const(n)
59
68
  end
60
69
 
61
- expect(trend).to eq(:linear)
62
- expect(trends[trend][:slope]).to eq(0)
70
+ expect(trend).to eq(:constant)
71
+ expect(trends[trend][:slope]).to be_within(0.0001).of(0)
63
72
  end
64
73
 
65
74
  it "infers finding maximum value trend to be linear" do
66
75
  array_sizes = Benchmark::Trend.range(1, 100_000)
67
- number_arrays = array_sizes.map { |n| Array.new(n) { rand(n) } }.each
76
+ numbers = array_sizes.map { |n| Array.new(n) { rand(n) } }
68
77
 
69
- trend, trends = Benchmark::Trend.infer_trend(array_sizes) do
70
- number_arrays.next.max
78
+ trend, trends = Benchmark::Trend.infer_trend(array_sizes, repeat: 10) do |n, i|
79
+ numbers[i].max
71
80
  end
72
81
 
73
82
  expect(trend).to eq(:linear)
74
- expect(trends).to match(
75
- hash_including(:exponential, :power, :linear, :logarithmic))
76
- expect(trends[:exponential]).to match(
77
- hash_including(:trend, :slope, :intercept, :residual)
78
- )
83
+ expect(trends[trend][:slope]).to be_within(0.0001).of(0)
84
+ end
85
+
86
+ it "infers binary search trend to be constant" do
87
+ range = Benchmark::Trend.range(10, 8 << 10, ratio: 2)
88
+ numbers = range.reduce([]) { |acc, n| acc << (1..n).to_a; acc }
89
+
90
+ trend, trends = Benchmark::Trend.infer_trend(range, repeat: 100) do |n, i|
91
+ numbers[i].bsearch { |x| x == n/2 }
92
+ end
93
+
94
+ expect(trend).to eq(:constant)
95
+ expect(trends[trend][:slope]).to be_within(0.0001).of(0)
79
96
  end
80
97
  end
@@ -2,7 +2,7 @@
2
2
  #
3
3
  RSpec.describe Benchmark::Trend, "#measure_execution_time" do
4
4
  it "measures performance times" do
5
- func = -> (x) { x ** 2 }
5
+ func = -> (x, i) { x ** 2 }
6
6
 
7
7
  data = Benchmark::Trend.measure_execution_time(&func)
8
8
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: benchmark-trend
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Piotr Murach
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-09-08 00:00:00.000000000 Z
11
+ date: 2018-09-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -70,6 +70,8 @@ files:
70
70
  - benchmark-trend.gemspec
71
71
  - bin/console
72
72
  - bin/setup
73
+ - examples/fib_constant.rb
74
+ - examples/fib_linear.rb
73
75
  - exe/bench-trend
74
76
  - lib/benchmark-trend.rb
75
77
  - lib/benchmark/trend.rb