d_heap 0.1.0 → 0.4.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 +4 -4
- data/.github/workflows/main.yml +26 -0
- data/.rubocop.yml +199 -0
- data/CHANGELOG.md +42 -0
- data/Gemfile +6 -2
- data/Gemfile.lock +36 -5
- data/README.md +394 -76
- data/Rakefile +8 -2
- data/benchmarks/push_n.yml +28 -0
- data/benchmarks/push_n_pop_n.yml +31 -0
- data/benchmarks/push_pop.yml +24 -0
- data/bin/bench_n +7 -0
- data/bin/benchmark-driver +29 -0
- data/bin/benchmarks +10 -0
- data/bin/console +1 -0
- data/bin/profile +10 -0
- data/bin/rubocop +29 -0
- data/d_heap.gemspec +11 -6
- data/docs/benchmarks-2.txt +52 -0
- data/docs/benchmarks.txt +443 -0
- data/docs/profile.txt +392 -0
- data/ext/d_heap/d_heap.c +677 -134
- data/ext/d_heap/d_heap.h +29 -18
- data/ext/d_heap/extconf.rb +13 -0
- data/lib/benchmark_driver/runner/ips_zero_fail.rb +120 -0
- data/lib/d_heap.rb +17 -31
- data/lib/d_heap/benchmarks.rb +111 -0
- data/lib/d_heap/benchmarks/benchmarker.rb +113 -0
- data/lib/d_heap/benchmarks/implementations.rb +168 -0
- data/lib/d_heap/benchmarks/profiler.rb +71 -0
- data/lib/d_heap/benchmarks/rspec_matchers.rb +374 -0
- data/lib/d_heap/version.rb +4 -1
- metadata +51 -3
data/ext/d_heap/d_heap.h
CHANGED
@@ -3,31 +3,42 @@
|
|
3
3
|
|
4
4
|
#include "ruby.h"
|
5
5
|
|
6
|
-
//
|
7
|
-
// gains from fewer levels can outweight doing this many comparisons per level.
|
8
|
-
// Since the comparisons will still be executed using <=> on ruby objects, it's
|
9
|
-
// likely they will be too slow to make any d > 8 worthwhile.
|
10
|
-
#define DHEAP_MAX_D 128
|
6
|
+
// d=4 uses the fewest comparisons for insert + delete-min (in the worst case).
|
11
7
|
#define DHEAP_DEFAULT_D 4
|
12
8
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
#define
|
20
|
-
|
9
|
+
// This is a somewhat arbitary maximum. But benefits from more leaf nodes
|
10
|
+
// are very unlikely to outweigh the increasinly higher number of worst-case
|
11
|
+
// comparisons as d gets further from 4.
|
12
|
+
#define DHEAP_MAX_D 32
|
13
|
+
|
14
|
+
#define DHEAP_DEFAULT_SIZE 16
|
15
|
+
#define DHEAP_MAX_SIZE (LONG_MAX / (int)sizeof(long double))
|
16
|
+
|
17
|
+
// 10MB
|
18
|
+
#define DHEAP_CAPA_INCR_MAX (10 * 1024 * 1024 / (int)sizeof(long double))
|
21
19
|
|
22
20
|
VALUE rb_cDHeap;
|
23
|
-
ID id_cmp;
|
24
21
|
|
25
|
-
|
22
|
+
// copied from pg gem
|
23
|
+
|
24
|
+
#define UNUSED(x) ((void)(x))
|
25
|
+
|
26
|
+
#ifdef HAVE_RB_GC_MARK_MOVABLE
|
27
|
+
#define dheap_compact_callback(x) ((void (*)(void*))(x))
|
28
|
+
#define dheap_gc_location(x) x = rb_gc_location(x)
|
29
|
+
#else
|
30
|
+
#define rb_gc_mark_movable(x) rb_gc_mark(x)
|
31
|
+
#define dheap_compact_callback(x) {(x)}
|
32
|
+
#define dheap_gc_location(x) UNUSED(x)
|
33
|
+
#endif
|
34
|
+
|
35
|
+
#ifdef __D_HEAP_DEBUG
|
36
|
+
#define debug(v) { \
|
26
37
|
ID sym_puts = rb_intern("puts"); \
|
27
38
|
rb_funcall(rb_mKernel, sym_puts, 1, v); \
|
28
39
|
}
|
29
|
-
|
30
|
-
|
31
|
-
|
40
|
+
#else
|
41
|
+
#define debug(v)
|
42
|
+
#endif
|
32
43
|
|
33
44
|
#endif /* D_HEAP_H */
|
data/ext/d_heap/extconf.rb
CHANGED
@@ -1,3 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "mkmf"
|
2
4
|
|
5
|
+
# For testing in CI (because I don't otherwise have easy access to Mac OS):
|
6
|
+
# $CFLAGS << " -D__D_HEAP_DEBUG" if /darwin/ =~ RUBY_PLATFORM
|
7
|
+
|
8
|
+
have_func "rb_gc_mark_movable" # since ruby-2.7
|
9
|
+
|
10
|
+
check_sizeof("long")
|
11
|
+
check_sizeof("unsigned long long")
|
12
|
+
check_sizeof("long double")
|
13
|
+
have_macro("LDBL_MANT_DIG", "float.h")
|
14
|
+
|
15
|
+
CONFIG["warnflags"] << " -Werror"
|
3
16
|
create_makefile("d_heap/d_heap")
|
@@ -0,0 +1,120 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "English" # $CHILD_STATUS
|
4
|
+
require "timeout" # Timeout::Error
|
5
|
+
|
6
|
+
require "benchmark_driver"
|
7
|
+
|
8
|
+
# monkey-patch to convert miniscule values to 0.0
|
9
|
+
class BenchmarkDriver::Output::Compare
|
10
|
+
|
11
|
+
# monkey-patch to convert miniscule values to 0.0
|
12
|
+
module MinisculeToZero
|
13
|
+
|
14
|
+
def humanize(value, width = 10)
|
15
|
+
value <= 0.0.next_float.next_float ? 0.0 : super(value, width)
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
prepend MinisculeToZero
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
# A simple patch to let slow specs error out without
|
25
|
+
class BenchmarkDriver::Runner::IpsZeroFail < BenchmarkDriver::Runner::Ips
|
26
|
+
METRIC = BenchmarkDriver::Runner::Ips::METRIC
|
27
|
+
|
28
|
+
# always run at least once
|
29
|
+
class Job < BenchmarkDriver::DefaultJob
|
30
|
+
attr_accessor :warmup_value, :warmup_duration, :warmup_loop_count
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
# BenchmarkDriver::Runner looks for this class
|
35
|
+
JobParser = BenchmarkDriver::DefaultJobParser.for(klass: Job, metrics: [METRIC])
|
36
|
+
|
37
|
+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/BlockLength, Layout/LineLength, Layout/SpaceInsideBlockBraces, Style/BlockDelimiters
|
38
|
+
|
39
|
+
# This method is dynamically called by `BenchmarkDriver::JobRunner.run`
|
40
|
+
# @param [Array<BenchmarkDriver::Default::Job>] jobs
|
41
|
+
def run(jobs)
|
42
|
+
if jobs.any? { |job| job.loop_count.nil? }
|
43
|
+
@output.with_warmup do
|
44
|
+
jobs = jobs.map do |job|
|
45
|
+
next job if job.loop_count # skip warmup if loop_count is set
|
46
|
+
|
47
|
+
@output.with_job(name: job.name) do
|
48
|
+
context = job.runnable_contexts(@contexts).first
|
49
|
+
duration, loop_count = run_warmup(job, context: context)
|
50
|
+
value, duration = value_duration(duration: duration, loop_count: loop_count)
|
51
|
+
|
52
|
+
@output.with_context(name: context.name, executable: context.executable, gems: context.gems, prelude: context.prelude) do
|
53
|
+
@output.report(values: { metric => value }, duration: duration, loop_count: loop_count)
|
54
|
+
end
|
55
|
+
|
56
|
+
warmup_loop_count = loop_count
|
57
|
+
|
58
|
+
loop_count = (loop_count.to_f * @config.run_duration / duration).floor
|
59
|
+
Job.new(**job.to_h.merge(loop_count: loop_count))
|
60
|
+
.tap {|j| j.warmup_value = value }
|
61
|
+
.tap {|j| j.warmup_duration = duration }
|
62
|
+
.tap {|j| j.warmup_loop_count = warmup_loop_count }
|
63
|
+
end
|
64
|
+
end
|
65
|
+
.compact
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
@output.with_benchmark do
|
70
|
+
jobs.each do |job|
|
71
|
+
@output.with_job(name: job.name) do
|
72
|
+
job.runnable_contexts(@contexts).each do |context|
|
73
|
+
repeat_params = { config: @config, larger_better: true, rest_on_average: :average }
|
74
|
+
result =
|
75
|
+
if job.loop_count&.positive?
|
76
|
+
loop_count = job.loop_count
|
77
|
+
BenchmarkDriver::Repeater.with_repeat(**repeat_params) do
|
78
|
+
run_benchmark(job, context: context)
|
79
|
+
end
|
80
|
+
else
|
81
|
+
loop_count = job.warmup_loop_count
|
82
|
+
repeater_value = [job.warmup_value, job.warmup_duration]
|
83
|
+
BenchmarkDriver::Repeater::RepeatResult.new(
|
84
|
+
value: repeater_value, all_values: [repeater_value]
|
85
|
+
)
|
86
|
+
end
|
87
|
+
value, duration = result.value
|
88
|
+
@output.with_context(name: context.name, executable: context.executable, gems: context.gems, prelude: context.prelude) do
|
89
|
+
@output.report(
|
90
|
+
values: { metric => value },
|
91
|
+
all_values: { metric => result.all_values },
|
92
|
+
duration: duration,
|
93
|
+
loop_count: loop_count,
|
94
|
+
)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/BlockLength, Layout/LineLength, Layout/SpaceInsideBlockBraces, Style/BlockDelimiters
|
103
|
+
|
104
|
+
def run_warmup(job, context:)
|
105
|
+
start = Time.now
|
106
|
+
super(job, context: context)
|
107
|
+
rescue Timeout::Error
|
108
|
+
[Time.now - start, 0.0.next_float]
|
109
|
+
end
|
110
|
+
|
111
|
+
def execute(*args, exception: true)
|
112
|
+
super
|
113
|
+
rescue RuntimeError => ex
|
114
|
+
if args.include?("timeout") && $CHILD_STATUS&.exitstatus == 124
|
115
|
+
raise Timeout::Error, ex.message
|
116
|
+
end
|
117
|
+
raise ex
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
data/lib/d_heap.rb
CHANGED
@@ -1,38 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "d_heap/d_heap"
|
2
4
|
require "d_heap/version"
|
3
5
|
|
6
|
+
# A fast _d_-ary heap implementation for ruby, useful in priority queues and graph
|
7
|
+
# algorithms.
|
8
|
+
#
|
9
|
+
# The _d_-ary heap data structure is a generalization of the binary heap, in which
|
10
|
+
# the nodes have _d_ children instead of 2. This allows for "decrease priority"
|
11
|
+
# operations to be performed more quickly with the tradeoff of slower delete
|
12
|
+
# minimum. Additionally, _d_-ary heaps can have better memory cache behavior than
|
13
|
+
# binary heaps, allowing them to run more quickly in practice despite slower
|
14
|
+
# worst-case time complexity.
|
15
|
+
#
|
4
16
|
class DHeap
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
def freeze
|
12
|
-
_ary_.freeze
|
13
|
-
super
|
14
|
-
end
|
15
|
-
|
16
|
-
def peek
|
17
|
-
_ary_[0]
|
18
|
-
end
|
19
|
-
|
20
|
-
def empty?
|
21
|
-
_ary_.empty?
|
22
|
-
end
|
23
|
-
|
24
|
-
def size
|
25
|
-
_ary_.size
|
26
|
-
end
|
27
|
-
|
28
|
-
def each_in_order
|
29
|
-
return to_enum(__method__) unless block_given?
|
30
|
-
heap = dup
|
31
|
-
yield val until heap.emptu?
|
32
|
-
end
|
33
|
-
|
34
|
-
def to_a
|
35
|
-
_ary_.dup
|
17
|
+
# ruby 3.0+ (2.x can just use inherited initialize_clone)
|
18
|
+
if Object.instance_method(:initialize_clone).arity == -1
|
19
|
+
def initialize_clone(other, freeze: nil)
|
20
|
+
__init_clone__(other, freeze ? true : freeze)
|
21
|
+
end
|
36
22
|
end
|
37
23
|
|
38
24
|
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "d_heap"
|
4
|
+
require "ostruct"
|
5
|
+
|
6
|
+
# Different benchmark scenarios and implementations to benchmark
|
7
|
+
module DHeap::Benchmarks
|
8
|
+
|
9
|
+
def self.puts_version_info(type = "Benchmark", io = $stdout)
|
10
|
+
io.puts "#{type} run at %s" % [Time.now]
|
11
|
+
io.puts "ruby v%s, DHeap v%s" % [RUBY_VERSION, DHeap::VERSION]
|
12
|
+
io.puts
|
13
|
+
end
|
14
|
+
|
15
|
+
# rubocop:disable Style/NumericPredicate
|
16
|
+
|
17
|
+
# moves "rand" outside the benchmarked code, to avoid measuring that too.
|
18
|
+
module Randomness
|
19
|
+
|
20
|
+
def default_randomness_size; 1_000_000 end
|
21
|
+
|
22
|
+
def fill_random_vals(target_size = default_randomness_size, io: $stdout)
|
23
|
+
@dheap_bm_random_vals ||= []
|
24
|
+
count = target_size - @dheap_bm_random_vals.length
|
25
|
+
return 0 if count <= 0
|
26
|
+
millions = (count / 1_000_000.0).round(3)
|
27
|
+
io&.puts "~~~~~~ filling @dheap_bm_random_vals with #{millions}M ~~~~~~"
|
28
|
+
io&.flush
|
29
|
+
count.times do @dheap_bm_random_vals << rand(0..10_000) end
|
30
|
+
@dheap_bm_random_len = @dheap_bm_random_vals.length
|
31
|
+
@dheap_bm_random_idx = (((@dheap_bm_random_idx || -1) + 1) % @dheap_bm_random_len)
|
32
|
+
nil
|
33
|
+
end
|
34
|
+
|
35
|
+
def random_val
|
36
|
+
@dheap_bm_random_vals.fetch(
|
37
|
+
@dheap_bm_random_idx = ((@dheap_bm_random_idx + 1) % @dheap_bm_random_len)
|
38
|
+
)
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
# different scenarios to be benchmarked or profiled
|
44
|
+
module Scenarios
|
45
|
+
|
46
|
+
def push_n_multiple_queues(count, *queues)
|
47
|
+
while 0 < count
|
48
|
+
value = @dheap_bm_random_vals.fetch(
|
49
|
+
@dheap_bm_random_idx = ((@dheap_bm_random_idx + 1) % @dheap_bm_random_len)
|
50
|
+
)
|
51
|
+
queues.each do |queue|
|
52
|
+
queue << value
|
53
|
+
end
|
54
|
+
count -= 1
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def push_n(queue, count)
|
59
|
+
while 0 < count
|
60
|
+
queue << @dheap_bm_random_vals.fetch(
|
61
|
+
@dheap_bm_random_idx = ((@dheap_bm_random_idx + 1) % @dheap_bm_random_len)
|
62
|
+
)
|
63
|
+
count -= 1
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def push_n_then_pop_n(queue, count) # rubocop:disable Metrics/MethodLength
|
68
|
+
i = 0
|
69
|
+
while i < count
|
70
|
+
queue << @dheap_bm_random_vals.fetch(
|
71
|
+
@dheap_bm_random_idx = ((@dheap_bm_random_idx + 1) % @dheap_bm_random_len)
|
72
|
+
)
|
73
|
+
i += 1
|
74
|
+
end
|
75
|
+
while 0 < i
|
76
|
+
queue.pop
|
77
|
+
i -= 1
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def repeated_push_pop(queue, count)
|
82
|
+
while 0 < count
|
83
|
+
queue << @dheap_bm_random_vals.fetch(
|
84
|
+
@dheap_bm_random_idx = ((@dheap_bm_random_idx + 1) % @dheap_bm_random_len)
|
85
|
+
)
|
86
|
+
queue.pop
|
87
|
+
count -= 1
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
include Randomness
|
94
|
+
include Scenarios
|
95
|
+
|
96
|
+
def initq(klass, count = 0)
|
97
|
+
queue = klass.new
|
98
|
+
while 0 < count
|
99
|
+
queue << @dheap_bm_random_vals.fetch(
|
100
|
+
@dheap_bm_random_idx = ((@dheap_bm_random_idx + 1) % @dheap_bm_random_len)
|
101
|
+
)
|
102
|
+
count -= 1
|
103
|
+
end
|
104
|
+
queue
|
105
|
+
end
|
106
|
+
|
107
|
+
# rubocop:enable Style/NumericPredicate
|
108
|
+
|
109
|
+
require "d_heap/benchmarks/implementations"
|
110
|
+
|
111
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "d_heap/benchmarks"
|
4
|
+
|
5
|
+
require "benchmark_driver"
|
6
|
+
require "shellwords"
|
7
|
+
require "English"
|
8
|
+
|
9
|
+
module DHeap::Benchmarks
|
10
|
+
# Benchmarks different implementations with different sizes
|
11
|
+
class Benchmarker
|
12
|
+
include Randomness
|
13
|
+
include Scenarios
|
14
|
+
|
15
|
+
N_COUNTS = [
|
16
|
+
5, # 1 + 4
|
17
|
+
21, # 1 + 4 + 16
|
18
|
+
85, # 1 + 4 + 16 + 64
|
19
|
+
341, # 1 + 4 + 16 + 64 + 256
|
20
|
+
1365, # 1 + 4 + 16 + 64 + 256 + 1024
|
21
|
+
5461, # 1 + 4 + 16 + 64 + 256 + 1024 + 4096
|
22
|
+
21_845, # 1 + 4 + 16 + 64 + 256 + 1024 + 4096 + 16384
|
23
|
+
87_381, # 1 + 4 + 16 + 64 + 256 + 1024 + 4096 + 16384 + 65536
|
24
|
+
].freeze
|
25
|
+
|
26
|
+
attr_reader :time
|
27
|
+
attr_reader :iterations_for_push_pop
|
28
|
+
attr_reader :io
|
29
|
+
|
30
|
+
def initialize(
|
31
|
+
time: Integer(ENV.fetch("BENCHMARK_TIME", 10)),
|
32
|
+
iterations_for_push_pop: 10_000,
|
33
|
+
io: $stdout
|
34
|
+
)
|
35
|
+
@time = time
|
36
|
+
@iterations_for_push_pop = Integer(iterations_for_push_pop)
|
37
|
+
@io = io
|
38
|
+
end
|
39
|
+
|
40
|
+
def call(queue_size: ENV.fetch("BENCHMARK_QUEUE_SIZE", :unset))
|
41
|
+
DHeap::Benchmarks.puts_version_info("Benchmarking")
|
42
|
+
sizes = (queue_size == :unset) ? N_COUNTS : [Integer(queue_size)]
|
43
|
+
sizes.each do |size|
|
44
|
+
benchmark_size(size)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def benchmark_size(size)
|
49
|
+
sep "#", "Benchmarks with N=#{size} (t=#{time}sec/benchmark)", big: true
|
50
|
+
io.puts
|
51
|
+
benchmark_push_n size
|
52
|
+
benchmark_push_n_then_pop_n size
|
53
|
+
benchmark_repeated_push_pop size
|
54
|
+
end
|
55
|
+
|
56
|
+
def benchmark_push_n(queue_size)
|
57
|
+
benchmarking("push N", "push_n", queue_size)
|
58
|
+
end
|
59
|
+
|
60
|
+
def benchmark_push_n_then_pop_n(queue_size)
|
61
|
+
benchmarking("push N then pop N", "push_n_pop_n", queue_size)
|
62
|
+
end
|
63
|
+
|
64
|
+
def benchmark_repeated_push_pop(queue_size)
|
65
|
+
benchmarking(
|
66
|
+
"Push/pop with pre-filled queue (size=N)", "push_pop", queue_size
|
67
|
+
)
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
# TODO: move somewhere else...
|
73
|
+
def skip_profiling?(queue_size, impl)
|
74
|
+
impl.klass == DHeap::Benchmarks::PushAndResort && 10_000 < queue_size
|
75
|
+
end
|
76
|
+
|
77
|
+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
78
|
+
|
79
|
+
def benchmarking(name, file, size)
|
80
|
+
Bundler.with_unbundled_env do
|
81
|
+
sep "==", "#{name} (N=#{size})"
|
82
|
+
cmd = %W[
|
83
|
+
bin/benchmark-driver
|
84
|
+
--bundler
|
85
|
+
--run-duration 6
|
86
|
+
--timeout 15
|
87
|
+
--runner ips_zero_fail
|
88
|
+
benchmarks/#{file}.yml
|
89
|
+
]
|
90
|
+
env = ENV.to_h.merge(
|
91
|
+
"BENCH_N" => size.to_s,
|
92
|
+
"RUBYLIB" => File.expand_path("../..", __dir__),
|
93
|
+
)
|
94
|
+
system(env, *cmd)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def sep(sep, msg = "", width: 80, big: false)
|
99
|
+
txt = String.new
|
100
|
+
txt += "#{sep * (width / sep.length)}\n" if big
|
101
|
+
txt += sep
|
102
|
+
txt += " #{msg}" if msg && !msg.empty?
|
103
|
+
txt += " " unless big
|
104
|
+
txt += sep * ((width - txt.length) / sep.length) unless big
|
105
|
+
txt += "\n"
|
106
|
+
txt += "#{sep * (width / sep.length)}\n" if big
|
107
|
+
io.print txt
|
108
|
+
end
|
109
|
+
|
110
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
111
|
+
|
112
|
+
end
|
113
|
+
end
|