benchmark-swap 0.1.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 +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +113 -0
- data/lib/benchmark/swap/candidate.rb +22 -0
- data/lib/benchmark/swap/discovery.rb +56 -0
- data/lib/benchmark/swap/runner.rb +93 -0
- data/lib/benchmark/swap/swapper.rb +71 -0
- data/lib/benchmark/swap/verifier.rb +48 -0
- data/lib/benchmark/swap/version.rb +7 -0
- data/lib/benchmark/swap.rb +52 -0
- metadata +71 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 300c84291632dc4b0f85e22e83dde631f6baffeaee7d61911adbb62ecceaa288
|
|
4
|
+
data.tar.gz: 7c5fd3e887f3af1d60012ae3f62b6be0f04e9f425ebb4e69fdd2cc663ad6f1fd
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 12661ba83db0e3e3f861768b2f90cf096c3acd273df8c9099b7870def338d8f0698cc0cb5f9db37dcbe02a0f6052e365b592513c1bba8d19e1e21ee1aed3cd86
|
|
7
|
+
data.tar.gz: 29ee23f65939cf66613bc324c3acb8048c9093b59889d9652208340d2f9acbaad5691865df2ee43474a332cbb13f0cf3b447363c53f88244a327186ca2d33d3c
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mehmet Emin INAC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Benchmark::Swap
|
|
2
|
+
|
|
3
|
+
Compare two implementations of a method with benchmark-ips, without lifting the method out of its call chain into two standalone lambdas. That lift is slow to do and easy to get wrong when the method sits deep inside real code. `Benchmark::Swap` keeps the method where it lives and swaps its body in place.
|
|
4
|
+
|
|
5
|
+
Built for exploring performance changes inside a large Rails app from the Rails console.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Add this line to your Gemfile:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem "benchmark-swap"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Then run `bundle install`.
|
|
16
|
+
|
|
17
|
+
Requires Ruby 3.2 or newer. Depends on `benchmark-ips`.
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
Keep the original method. Add a second implementation next to it, with the same name plus a `_perf` suffix:
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
class Pow
|
|
25
|
+
def initialize(number)
|
|
26
|
+
@number = number
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def pow
|
|
30
|
+
do_pow
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def do_pow
|
|
36
|
+
@number**2
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def do_pow_perf
|
|
40
|
+
@number * @number
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
Benchmark.swap { Pow.new(2).pow }
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`Benchmark.swap` is a shortcut for `Benchmark::Swap.test`. Both take the same options.
|
|
48
|
+
|
|
49
|
+
Sample output:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
benchmark-swap: swapping 1 method
|
|
53
|
+
Pow#do_pow -> do_pow_perf
|
|
54
|
+
|
|
55
|
+
ruby 3.3.11 (2026-03-26 revision 1f2d15125a) [arm64-darwin24]
|
|
56
|
+
Warming up --------------------------------------
|
|
57
|
+
original 848.389k i/100ms
|
|
58
|
+
Calculating -------------------------------------
|
|
59
|
+
original 8.480M (± 0.3%) i/s (117.93 ns/i) - 8.484M in 1.000511s
|
|
60
|
+
ruby 3.3.11 (2026-03-26 revision 1f2d15125a) [arm64-darwin24]
|
|
61
|
+
Warming up --------------------------------------
|
|
62
|
+
swapped 905.345k i/100ms
|
|
63
|
+
Calculating -------------------------------------
|
|
64
|
+
swapped 9.083M (± 0.2%) i/s (110.09 ns/i) - 9.959M in 1.096372s
|
|
65
|
+
|
|
66
|
+
Comparison:
|
|
67
|
+
original: 8479556.9 i/s
|
|
68
|
+
swapped: 9083408.7 i/s - 1.07x faster
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Two "Warming up / Calculating" blocks and one "Comparison:" block are expected. Each side gets its own benchmark-ips run, then the two are compared with the original as the baseline.
|
|
72
|
+
|
|
73
|
+
## How it works
|
|
74
|
+
|
|
75
|
+
1. **Discovery.** The block runs once under a Ruby `TracePoint` on the `:call` event. It collects every method that was actually called and has a twin with the suffix defined on the same owner. Only calls from the current thread count. A method whose twin lives on a different class or module (for example the original on a parent class, the twin on the child) is skipped. Frozen owners are skipped too.
|
|
76
|
+
2. **Verification.** The block runs once per side, and the two results are compared with `==`. A mismatch prints a warning, but the benchmark still runs: this step never raises. If both sides raise the same error class and message, that also counts as a match. Turn it off with `verify: false`.
|
|
77
|
+
3. **Benchmark.** Each side gets its own benchmark-ips run, then `Benchmark.compare` reports the two with the original as the baseline.
|
|
78
|
+
|
|
79
|
+
All discovered twins are swapped together. There is no one-at-a-time mode in this version.
|
|
80
|
+
|
|
81
|
+
The swap itself copies the twin's `UnboundMethod` body onto the original method name with `define_method`, keeping the original body aside, and puts it back afterwards. It does not delegate through a wrapper method, so both sides run at the same call depth and the numbers describe the method bodies, not the swap. A spec proves this by comparing `caller.size` on both sides. Method visibility (public, protected, private) is preserved and restored. Originals are restored even when the block raises. When a module is prepended to the owner, the swap targets the definition the owner itself holds, so a prepended override that calls `super` keeps working and its own body is left untouched.
|
|
82
|
+
|
|
83
|
+
## Options
|
|
84
|
+
|
|
85
|
+
| Option | Default | Description |
|
|
86
|
+
| --- | --- | --- |
|
|
87
|
+
| `suffix:` | `"_perf"` | Suffix used to find the twin method |
|
|
88
|
+
| `verify:` | `true` | Compare both sides once before benchmarking |
|
|
89
|
+
| `output:` | `$stdout` | Where the gem's own report lines go |
|
|
90
|
+
| anything else | | Passed to benchmark-ips, for example `time:` and `warmup:` |
|
|
91
|
+
|
|
92
|
+
The call returns a `Runner::Result` struct with `candidates`, `verification`, `original`, and `swapped`. `original` and `swapped` are benchmark-ips `Report` objects. It returns `nil` when no twin was called.
|
|
93
|
+
|
|
94
|
+
## Caveats
|
|
95
|
+
|
|
96
|
+
- The block runs several times: once for discovery, twice for verification, then many times per benchmark side. Side effects add up, so build fresh objects inside the block instead of reusing a memoised one.
|
|
97
|
+
- If the block raises during discovery, the error propagates.
|
|
98
|
+
- Only methods defined in Ruby are found. The TracePoint `:call` event does not fire for methods implemented in C, so an `attr_reader` original, or a method from a C extension, is skipped even when a twin exists next to it. Write the original in Ruby if you want to measure it.
|
|
99
|
+
- While a side is being measured, the swap is visible to the whole process, not just the calling thread. Discovery only watches the current thread, but the swapped body is what every thread sees. So do not run this on a process that is serving real traffic.
|
|
100
|
+
- Remember to delete the `_perf` method before you commit.
|
|
101
|
+
|
|
102
|
+
## Development
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
bundle install
|
|
106
|
+
bundle exec rspec
|
|
107
|
+
bundle exec rubocop
|
|
108
|
+
ruby examples/pow.rb
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## License
|
|
112
|
+
|
|
113
|
+
MIT.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Benchmark
|
|
4
|
+
module Swap
|
|
5
|
+
# A method that has a suffixed twin, plus the owner both are defined on.
|
|
6
|
+
Candidate = Struct.new(:owner, :name, :perf_name) do
|
|
7
|
+
def label
|
|
8
|
+
if owner.singleton_class?
|
|
9
|
+
"#{owner.attached_object}.#{name}"
|
|
10
|
+
else
|
|
11
|
+
"#{owner}##{name}"
|
|
12
|
+
end
|
|
13
|
+
rescue TypeError
|
|
14
|
+
"#{owner}##{name}"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_s
|
|
18
|
+
"#{label} -> #{perf_name}"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Benchmark
|
|
4
|
+
module Swap
|
|
5
|
+
# Runs the block once under a TracePoint and keeps every method call that
|
|
6
|
+
# has a twin with the configured suffix on the same owner.
|
|
7
|
+
class Discovery
|
|
8
|
+
def initialize(suffix:)
|
|
9
|
+
@suffix = suffix
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def call(&block)
|
|
13
|
+
seen = Set.new
|
|
14
|
+
found = []
|
|
15
|
+
thread = Thread.current
|
|
16
|
+
|
|
17
|
+
trace = TracePoint.new(:call) do |trace_point|
|
|
18
|
+
next unless Thread.current.equal?(thread)
|
|
19
|
+
|
|
20
|
+
owner = trace_point.defined_class
|
|
21
|
+
name = trace_point.method_id
|
|
22
|
+
key = [owner.object_id, name]
|
|
23
|
+
|
|
24
|
+
next unless seen.add?(key)
|
|
25
|
+
|
|
26
|
+
candidate = candidate_for(owner, name)
|
|
27
|
+
|
|
28
|
+
found << candidate if candidate
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
trace.enable(&block)
|
|
32
|
+
|
|
33
|
+
found
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def candidate_for(owner, name)
|
|
39
|
+
return unless owner.is_a?(Module)
|
|
40
|
+
return if owner.frozen?
|
|
41
|
+
return if name.nil? || name.to_s.end_with?(@suffix)
|
|
42
|
+
|
|
43
|
+
perf_name = :"#{name}#{@suffix}"
|
|
44
|
+
return unless own_method?(owner, name) && own_method?(owner, perf_name)
|
|
45
|
+
|
|
46
|
+
Candidate.new(owner, name, perf_name)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def own_method?(owner, name)
|
|
50
|
+
owner.public_method_defined?(name, false) ||
|
|
51
|
+
owner.private_method_defined?(name, false) ||
|
|
52
|
+
owner.protected_method_defined?(name, false)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Benchmark
|
|
4
|
+
module Swap
|
|
5
|
+
# Ties the three steps together:
|
|
6
|
+
# - find the twins
|
|
7
|
+
# - check both sides agree
|
|
8
|
+
# - benchmark each side
|
|
9
|
+
#
|
|
10
|
+
# Each side gets its own benchmark-ips run, because a single run executes
|
|
11
|
+
# every report block after the setup block returns, which leaves no point
|
|
12
|
+
# to turn the swap on between them.
|
|
13
|
+
class Runner
|
|
14
|
+
ORIGINAL_LABEL = "original"
|
|
15
|
+
SWAPPED_LABEL = "swapped"
|
|
16
|
+
PREFIX = "benchmark-swap:"
|
|
17
|
+
|
|
18
|
+
Result = Struct.new(:candidates, :verification, :original, :swapped)
|
|
19
|
+
|
|
20
|
+
def initialize(suffix:, verify:, output:, ips_options:)
|
|
21
|
+
@suffix = suffix
|
|
22
|
+
@verify = verify
|
|
23
|
+
@output = output
|
|
24
|
+
@ips_options = ips_options
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def call(&block)
|
|
28
|
+
candidates = Discovery.new(suffix: @suffix).call(&block)
|
|
29
|
+
|
|
30
|
+
return nothing_found if candidates.empty?
|
|
31
|
+
|
|
32
|
+
announce(candidates)
|
|
33
|
+
|
|
34
|
+
swapper = Swapper.new(candidates)
|
|
35
|
+
verification = verify(swapper, &block)
|
|
36
|
+
original, swapped = benchmark(swapper, &block)
|
|
37
|
+
|
|
38
|
+
Result.new(candidates, verification, original, swapped)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def nothing_found
|
|
44
|
+
say "#{PREFIX} no *#{@suffix} twin was called by this block, nothing to compare."
|
|
45
|
+
|
|
46
|
+
nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def announce(candidates)
|
|
50
|
+
count = candidates.size
|
|
51
|
+
say "#{PREFIX} swapping #{count} method#{"s" if count > 1}"
|
|
52
|
+
candidates.each { |candidate| say " #{candidate}" }
|
|
53
|
+
say ""
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def verify(swapper, &block)
|
|
57
|
+
return unless @verify
|
|
58
|
+
|
|
59
|
+
outcome = Verifier.new.call(swapper, &block)
|
|
60
|
+
return outcome if outcome.match?
|
|
61
|
+
|
|
62
|
+
say "#{PREFIX} WARNING both sides returned a different result"
|
|
63
|
+
say " #{ORIGINAL_LABEL}: #{outcome.original}"
|
|
64
|
+
say " #{SWAPPED_LABEL}: #{outcome.swapped}"
|
|
65
|
+
say ""
|
|
66
|
+
|
|
67
|
+
outcome
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def benchmark(swapper, &block)
|
|
71
|
+
original = ips(ORIGINAL_LABEL, &block)
|
|
72
|
+
swapped = swapper.swapped { ips(SWAPPED_LABEL, &block) }
|
|
73
|
+
|
|
74
|
+
Benchmark.compare(*original.entries, *swapped.entries, order: :baseline)
|
|
75
|
+
|
|
76
|
+
[original, swapped]
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def ips(label, &block)
|
|
80
|
+
options = @ips_options
|
|
81
|
+
|
|
82
|
+
Benchmark.ips do |job|
|
|
83
|
+
job.config(**options) unless options.empty?
|
|
84
|
+
job.report(label, &block)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def say(line)
|
|
89
|
+
@output.puts(line)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Benchmark
|
|
4
|
+
module Swap
|
|
5
|
+
# Copies each twin's body onto the original method name, and puts the
|
|
6
|
+
# original body back afterwards.
|
|
7
|
+
#
|
|
8
|
+
# define_method with an UnboundMethod copies the body instead of
|
|
9
|
+
# delegating to it, so both sides of the benchmark run at the same call
|
|
10
|
+
# depth and the numbers describe the bodies, not the swap.
|
|
11
|
+
class Swapper
|
|
12
|
+
def initialize(candidates)
|
|
13
|
+
@candidates = candidates
|
|
14
|
+
@saved = []
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def swapped
|
|
18
|
+
enable
|
|
19
|
+
yield
|
|
20
|
+
ensure
|
|
21
|
+
disable
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def enable
|
|
25
|
+
@candidates.each do |candidate|
|
|
26
|
+
owner = candidate.owner
|
|
27
|
+
original = own_instance_method(owner, candidate.name)
|
|
28
|
+
visibility = visibility_of(owner, candidate.name)
|
|
29
|
+
|
|
30
|
+
define(owner, candidate.name, own_instance_method(owner, candidate.perf_name), visibility)
|
|
31
|
+
@saved << [owner, candidate.name, original, visibility]
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def disable
|
|
36
|
+
while (entry = @saved.pop)
|
|
37
|
+
define(*entry)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
# A module prepended to the owner shadows the owner's own definition, so
|
|
44
|
+
# Module#instance_method can hand back a body the owner does not own.
|
|
45
|
+
# Saving that body and writing it back on restore would overwrite the
|
|
46
|
+
# real one for good, so walk down to the definition the owner itself
|
|
47
|
+
# holds.
|
|
48
|
+
def own_instance_method(owner, name)
|
|
49
|
+
method = owner.instance_method(name)
|
|
50
|
+
method = method.super_method until method.nil? || method.owner.equal?(owner)
|
|
51
|
+
|
|
52
|
+
method || raise(ArgumentError, "#{owner} does not define #{name}, it only inherits it")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def define(owner, name, body, visibility)
|
|
56
|
+
owner.send(:define_method, name, body)
|
|
57
|
+
owner.send(visibility, name)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def visibility_of(owner, name)
|
|
61
|
+
if owner.private_method_defined?(name, false)
|
|
62
|
+
:private
|
|
63
|
+
elsif owner.protected_method_defined?(name, false)
|
|
64
|
+
:protected
|
|
65
|
+
else
|
|
66
|
+
:public
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Benchmark
|
|
4
|
+
module Swap
|
|
5
|
+
# Runs the block once per side and compares the outcomes. A mismatch is a
|
|
6
|
+
# warning, never an error: the block may return something that does not
|
|
7
|
+
# compare with ==, and that is the caller's call to make.
|
|
8
|
+
class Verifier
|
|
9
|
+
# An inspect of a real object graph can fill the console, and this is
|
|
10
|
+
# only here to show the reader what differed.
|
|
11
|
+
MAX_LENGTH = 200
|
|
12
|
+
|
|
13
|
+
Result = Struct.new(:value, :error) do
|
|
14
|
+
def self.capture
|
|
15
|
+
new(yield, nil)
|
|
16
|
+
rescue StandardError, ScriptError => e
|
|
17
|
+
new(nil, e)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def match?(other)
|
|
21
|
+
return value == other.value unless error || other.error
|
|
22
|
+
|
|
23
|
+
other.error.instance_of?(error.class) && other.error.message == error.message
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def to_s
|
|
27
|
+
return "#{error.class}: #{error.message}" if error
|
|
28
|
+
|
|
29
|
+
text = value.inspect
|
|
30
|
+
text.length > MAX_LENGTH ? "#{text[0, MAX_LENGTH]}..." : text
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
Outcome = Struct.new(:original, :swapped) do
|
|
35
|
+
def match?
|
|
36
|
+
original.match?(swapped)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def call(swapper, &block)
|
|
41
|
+
original = Result.capture(&block)
|
|
42
|
+
swapped = swapper.swapped { Result.capture(&block) }
|
|
43
|
+
|
|
44
|
+
Outcome.new(original, swapped)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "benchmark/ips"
|
|
4
|
+
|
|
5
|
+
require_relative "swap/version"
|
|
6
|
+
require_relative "swap/candidate"
|
|
7
|
+
require_relative "swap/discovery"
|
|
8
|
+
require_relative "swap/swapper"
|
|
9
|
+
require_relative "swap/verifier"
|
|
10
|
+
require_relative "swap/runner"
|
|
11
|
+
|
|
12
|
+
module Benchmark
|
|
13
|
+
# Benchmarks a second implementation of a method where it already runs.
|
|
14
|
+
#
|
|
15
|
+
# class Pow
|
|
16
|
+
# def pow
|
|
17
|
+
# do_pow
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# private
|
|
21
|
+
#
|
|
22
|
+
# def do_pow
|
|
23
|
+
# @number**2
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
# def do_pow_perf
|
|
27
|
+
# @number * @number
|
|
28
|
+
# end
|
|
29
|
+
# end
|
|
30
|
+
#
|
|
31
|
+
# Benchmark.swap { Pow.new(2).pow }
|
|
32
|
+
module Swap
|
|
33
|
+
DEFAULT_SUFFIX = "_perf"
|
|
34
|
+
|
|
35
|
+
class << self
|
|
36
|
+
# @param suffix [String] the twin's name suffix
|
|
37
|
+
# @param verify [Boolean] compare both sides once before benchmarking
|
|
38
|
+
# @param output [IO] where the swap report goes
|
|
39
|
+
# @param ips_options [Hash] passed to benchmark-ips, e.g. time:, warmup:
|
|
40
|
+
# @return [Runner::Result, nil] nil when no twin was called
|
|
41
|
+
def test(suffix: DEFAULT_SUFFIX, verify: true, output: $stdout, **ips_options, &block)
|
|
42
|
+
raise ArgumentError, "a block is required" unless block
|
|
43
|
+
|
|
44
|
+
Runner.new(suffix: suffix, verify: verify, output: output, ips_options: ips_options).call(&block)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.swap(...)
|
|
50
|
+
Swap.test(...)
|
|
51
|
+
end
|
|
52
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: benchmark-swap
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Mehmet Emin INAC
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: benchmark-ips
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.13'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2.13'
|
|
26
|
+
description: |
|
|
27
|
+
Write your alternative implementation next to the original one, with a
|
|
28
|
+
_perf suffix on the name. Benchmark.swap runs your block twice, once with
|
|
29
|
+
the original methods and once with the suffixed twins in their place, and
|
|
30
|
+
compares the two. No need to lift the method out of its call chain first.
|
|
31
|
+
email:
|
|
32
|
+
- mehmetemininac@gmail.com
|
|
33
|
+
executables: []
|
|
34
|
+
extensions: []
|
|
35
|
+
extra_rdoc_files: []
|
|
36
|
+
files:
|
|
37
|
+
- CHANGELOG.md
|
|
38
|
+
- LICENSE.txt
|
|
39
|
+
- README.md
|
|
40
|
+
- lib/benchmark/swap.rb
|
|
41
|
+
- lib/benchmark/swap/candidate.rb
|
|
42
|
+
- lib/benchmark/swap/discovery.rb
|
|
43
|
+
- lib/benchmark/swap/runner.rb
|
|
44
|
+
- lib/benchmark/swap/swapper.rb
|
|
45
|
+
- lib/benchmark/swap/verifier.rb
|
|
46
|
+
- lib/benchmark/swap/version.rb
|
|
47
|
+
homepage: https://github.com/meinac/benchmark-swap
|
|
48
|
+
licenses:
|
|
49
|
+
- MIT
|
|
50
|
+
metadata:
|
|
51
|
+
homepage_uri: https://github.com/meinac/benchmark-swap
|
|
52
|
+
changelog_uri: https://github.com/meinac/benchmark-swap/blob/master/CHANGELOG.md
|
|
53
|
+
rubygems_mfa_required: 'true'
|
|
54
|
+
rdoc_options: []
|
|
55
|
+
require_paths:
|
|
56
|
+
- lib
|
|
57
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '3.2'
|
|
62
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
63
|
+
requirements:
|
|
64
|
+
- - ">="
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: '0'
|
|
67
|
+
requirements: []
|
|
68
|
+
rubygems_version: 4.0.10
|
|
69
|
+
specification_version: 4
|
|
70
|
+
summary: Benchmark a second implementation of a method where it already runs.
|
|
71
|
+
test_files: []
|