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.
@@ -3,31 +3,42 @@
3
3
 
4
4
  #include "ruby.h"
5
5
 
6
- // This is somewhat a arbitary boundary, but it is highly unlikely that the
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
- #define CMP_LT(a, b) \
14
- (rb_cmpint(rb_funcallv(a, id_cmp, 1, &b), a, b) < 0)
15
- #define CMP_LTE(a, b) \
16
- (rb_cmpint(rb_funcallv(a, id_cmp, 1, &b), a, b) <= 0)
17
- #define CMP_GT(a, b) \
18
- (rb_cmpint(rb_funcallv(a, id_cmp, 1, &b), a, b) > 0)
19
- #define CMP_GTE(a, b) \
20
- (rb_cmpint(rb_funcallv(a, id_cmp, 1, &b), a, b) >= 0)
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
- #define puts(v) { \
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
- VALUE dheap_ary_sift_up(VALUE heap_array, int d, long sift_idx);
31
- VALUE dheap_ary_sift_down(VALUE heap_array, int d, long sift_idx);
40
+ #else
41
+ #define debug(v)
42
+ #endif
32
43
 
33
44
  #endif /* D_HEAP_H */
@@ -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
@@ -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
- def initialize_dup(other)
7
- super
8
- _ary_.replace(_ary_.dup)
9
- end
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