d_heap 0.1.0 → 0.4.0

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