benchmark-trend 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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