Rakefile
require "bundler/gem_tasks"
require "rspec/core/rake_task"
require 'benchmark/ips'
require 'qo'
RSpec::Core::RakeTask.new(:spec)
task :default => :spec
# Run a benchmark given a title and a set of benchmarks. Admittedly this
# is done because the Benchmark.ips code can get a tinge repetitive and this
# is easier to write out.
#
# @param title [String] Title of the benchmark
# @param **benchmarks [Hash[Symbol, Proc]] Name to Proc to run to benchmark it
#
# @note Notice I'm using `'String': -> {}` instead of hashrockets? Kwargs doesn't
# take string / hashrocket arguments, probably to prevent abuse of the
# "anything can be a key" bit of Ruby.
#
# @return [Unit] StdOut
def run_benchmark(title, quiet = false, **benchmarks)
puts '', title, '=' * title.size, ''
# Validation
benchmarks.each do |benchmark_name, benchmark_fn|
puts "#{benchmark_name} result: #{benchmark_fn.call()}"
end unless quiet
puts
Benchmark.ips do |bm|
benchmarks.each do |benchmark_name, benchmark_fn|
bm.report(benchmark_name, &benchmark_fn)
end
bm.compare!
end
end
def xrun_benchmark(title, **benchmarks) end
# Note that the current development of Qo is NOT to be performance first, it's to
# be readability first with performance coming later. That means that early iterations
# may well be slower, but the net expressiveness we get is worth it in the short run.
task :perf do
puts "Running on Qo v#{Qo::VERSION} at rev #{`git rev-parse HEAD`} - Ruby #{`ruby -v`}"
# Compare simple array equality. I almost think this isn't fair to Qo considering
# no sane dev should use it for literal 1 to 1 matches like this.
simple_array = [1, 1]
run_benchmark('Array * Array - Literal',
'Vanilla': -> {
simple_array == simple_array
},
'Qo.and': -> {
Qo.and(1, 1).call(simple_array)
}
)
# Compare testing indexed array matches. This gets a bit more into what Qo does,
# though I feel like there are optimizations that could be had here as well.
range_match_set = [1..10, 1..10, 1..10, 1..10]
range_match_target = [1, 2, 3, 4]
run_benchmark('Array * Array - Index pattern match',
'Vanilla': -> {
range_match_target.each_with_index.all? { |x, i| range_match_set[i] === x }
},
'Qo.and': -> {
Qo.and(1..10, 1..10, 1..10, 1..10).call(range_match_target)
}
)
# Now we're getting into things Qo makes sense for. Comparing an entire list
# against a stream of predicates to check
numbers_array = [1, 2.0, 3, 4]
run_benchmark('Array * Object - Predicate match',
'Vanilla': -> {
numbers_array.all? { |i| i.is_a?(Integer) && i.even? && (20..30).include?(i) }
},
'Qo.and': -> {
numbers_array.all?(&Qo.and(Integer, :even?, 20..30))
}
)
# This one is a bit interesting. The vanilla version is written to reflect that
# it has NO idea what the length of either set is, which is exactly what Qo
# has to deal with as well.
people_array_target = [
['Robert', 22],
['Roberta', 22],
['Foo', 42],
['Bar', 18]
]
people_array_query = [/Rob/, 15..25]
run_benchmark('Array * Array - Select index pattern match',
'Vanilla': -> {
people_array_target.select { |person|
person.each_with_index.all? { |a, i| people_array_query[i] === a }
}
},
'Qo.and': -> {
people_array_target.select(&Qo.and(/Rob/, 15..25))
}
)
people_hashes = people_array_target.map { |(name, age)| {name: name, age: age} }
run_benchmark('Hash * Hash - Hash intersection',
'Vanilla': -> {
people_hashes.select { |person| (15..25).include?(person[:age]) && /Rob/ =~ person[:name] }
},
'Qo.and': -> {
people_hashes.select(&Qo.and(name: /Rob/, age: 15..25))
}
)
Person = Struct.new(:name, :age)
people = [
Person.new('Robert', 22),
Person.new('Roberta', 22),
Person.new('Foo', 42),
Person.new('Bar', 17)
]
run_benchmark('Hash * Object - Property match',
'Vanilla': -> {
people.select { |person| (15..25).include?(person.age) && /Rob/ =~ person.name }
},
'Qo.and': -> {
people.select(&Qo.and(name: /Rob/, age: 15..25))
}
)
end
task :perf_pattern_match do
# Going to redefine the way that success and fail happen in here.
# return false
require 'dry-matcher'
# Match `[:ok, some_value]` for success
success_case = Dry::Matcher::Case.new(
match: -> value { value.first == :ok },
resolve: -> value { value.last }
)
# Match `[:err, some_error_code, some_value]` for failure
failure_case = Dry::Matcher::Case.new(
match: -> value, *pattern {
value[0] == :err && (pattern.any? ? pattern.include?(value[1]) : true)
},
resolve: -> value { value.last }
)
# Build the matcher
matcher = Dry::Matcher.new(success: success_case, failure: failure_case)
qo_m = Qo.result_match { |m|
m.success(Any) { |v| v }
m.failure(Any) { "ERR!" }
}
qo_m_case = proc { |target|
Qo.result_case(target) { |m|
m.success(Any) { |v| v }
m.failure(Any) { "ERR!" }
}
}
dm_m = proc { |target|
matcher.(target) do |m|
m.success { |(v)| v }
m.failure { 'ERR!' }
end
}
# v_m = proc { |target|
# next target[1] if target[0] == :ok
# 'ERR!'
# }
ok_target = [:ok, 12345]
err_target = [:err, "OH NO!"]
run_benchmark('Single Item Tuple',
'Qo': -> {
"OK: #{qo_m[ok_target]}, ERR: #{qo_m[err_target]}"
},
'Qo Case': -> {
"OK: #{qo_m_case[ok_target]}, ERR: #{qo_m_case[err_target]}"
},
'DryRB': -> {
"OK: #{dm_m[ok_target]}, ERR: #{dm_m[err_target]}"
},
# 'Vanilla': -> {
# "OK: #{v_m[ok_target]}, ERR: #{v_m[err_target]}"
# },
)
collection = [ok_target, err_target] * 2_000
run_benchmark('Large Tuple Collection', true,
'Qo': -> { collection.map(&qo_m) },
'Qo Case': -> { collection.map(&qo_m_case) },
'DryRB': -> { collection.map(&dm_m) },
# 'Vanilla': -> { collection.map(&v_m) }
)
# Person = Struct.new(:name, :age)
# people = [
# Person.new('Robert', 22),
# Person.new('Roberta', 22),
# Person.new('Foo', 42),
# Person.new('Bar', 17)
# ] * 1_000
# v_om = proc { |target|
# if /^F/.match?(target.name) && (30..50).include?(target.age)
# "It's foo!"
# else
# "Not foo"
# end
# }
# qo_om = Qo.match { |m|
# m.when(name: /^F/, age: 30..50) { "It's foo!" }
# m.else { "Not foo" }
# }
# run_benchmark('Large Object Collection', true,
# 'Qo': -> { people.map(&qo_om) },
# 'Vanilla': -> { people.map(&v_om) }
# )
end
# Below this mark are mostly my experiments to see what features perform a bit better
# than others, and are mostly left to check different versions of Ruby against eachother.
#
# Feel free to use them in development, but the general consensus of them is that
# `send` type methods are barely slower. One _could_ write an IIFE to get around
# that and maintain the flexibility but it's a net loss of clarity.
#
# Proc wise, they're all within margin of error. We just need to be really careful
# of the 2.4+ bug of lambdas not destructuring automatically, which will wreak
# havoc on hash matchers.
task :kwargs_vs_positional do
def add_kw(a:, b:, c:, d:) a + b + c + d end
def add_pos(a,b,c,d) a + b + c + d end
run_benchmark('Positional vs KW Args',
'keyword': -> { add_kw(a: 1, b: 2, c: 3, d: 4) },
'positional': -> { add_pos(1,2,3,4) }
)
end
task :perf_predicates do
array = (1..1000).to_a
run_benchmark('Predicates any?',
'block_any?': -> { array.any? { |v| v.even? } },
'proc_any?': -> { array.any?(&:even?) },
'send_proc_any?': -> { array.public_send(:any?, &:even?) }
)
run_benchmark('Predicates all?',
'block_all?': -> { array.all? { |v| v.even? } },
'proc_all?': -> { array.all?(&:even?) },
'send_proc_all?': -> { array.public_send(:all?, &:even?) }
)
run_benchmark('Predicates none?',
'block_none?': -> { array.none? { |v| v.even? } },
'proc_none?': -> { array.none?(&:even?) },
'send_proc_none?': -> { array.public_send(:none?, &:even?) },
)
even_stabby_lambda = -> n { n % 2 == 0 }
even_lambda = lambda { |n| n % 2 == 0 }
even_proc_new = Proc.new { |n| n % 2 == 0 }
even_proc_short = proc { |n| n % 2 == 0 }
even_to_proc = :even?.to_proc
run_benchmark('Types of Functions in Ruby',
even_stabby_lambda: -> { array.all?(&even_stabby_lambda) },
even_lambda: -> { array.all?(&even_lambda) },
even_proc_new: -> { array.all?(&even_proc_new) },
even_proc_short: -> { array.all?(&even_proc_short) },
even_to_proc: -> { array.all?(&even_to_proc) },
)
end
task :perf_random do
run_benchmark('Empty on blank array',
'empty?': -> { [].empty? },
'size == 0': -> { [].size == 0 },
'size.zero?': -> { [].size.zero? },
)
array = (1..1000).to_a
run_benchmark('Empty on several elements array',
'empty?': -> { array.empty? },
'size == 0': -> { array.size == 0 },
'size.zero?': -> { array.size.zero? },
)
hash = array.map { |v| [v, v] }.to_h
run_benchmark('Empty on blank hash vs array',
'hash empty?': -> { {}.empty? },
'array empty?': -> { [].empty? },
'full hash empty?': -> { hash.empty? },
'full array empty?': -> { array.empty? },
)
end