d_heap 0.2.0 → 0.5.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.
@@ -11,64 +11,40 @@
11
11
  // comparisons as d gets further from 4.
12
12
  #define DHEAP_MAX_D 32
13
13
 
14
+ typedef long double SCORE;
14
15
 
15
- #define CMP_LT(a, b, cmp_opt) \
16
- (OPTIMIZED_CMP(a, b, cmp_opt) < 0)
17
- #define CMP_LTE(a, b, cmp_opt) \
18
- (OPTIMIZED_CMP(a, b, cmp_opt) <= 0)
19
- #define CMP_GT(a, b, cmp_opt) \
20
- (OPTIMIZED_CMP(a, b, cmp_opt) > 0)
21
- #define CMP_GTE(a, b, cmp_opt) \
22
- (OPTIMIZED_CMP(a, b, cmp_opt) >= 0)
16
+ typedef struct dheap_entry {
17
+ SCORE score;
18
+ VALUE value;
19
+ } ENTRY;
23
20
 
24
- VALUE rb_cDHeap;
25
- ID id_cmp;
26
-
27
- // from internal/numeric.h
28
- #ifndef INTERNAL_NUMERIC_H
29
- int rb_float_cmp(VALUE x, VALUE y);
30
- #endif /* INTERNAL_NUMERIC_H */
21
+ #define DHEAP_DEFAULT_SIZE 256
22
+ #define DHEAP_MAX_SIZE (LONG_MAX / (int)sizeof(ENTRY))
31
23
 
32
- // from internal/compar.h
33
- #ifndef INTERNAL_COMPAR_H
34
- #define STRING_P(s) (RB_TYPE_P((s), T_STRING) && CLASS_OF(s) == rb_cString)
24
+ #define DHEAP_CAPA_INCR_MAX (10 * 1024 * 1024 / (int)sizeof(ENTRY))
35
25
 
36
- enum {
37
- cmp_opt_Integer,
38
- cmp_opt_String,
39
- cmp_opt_Float,
40
- cmp_optimizable_count
41
- };
26
+ VALUE rb_cDHeap;
42
27
 
43
- struct cmp_opt_data {
44
- unsigned int opt_methods;
45
- unsigned int opt_inited;
46
- };
28
+ // copied from pg gem
47
29
 
48
- #define NEW_CMP_OPT_MEMO(type, value) \
49
- NEW_PARTIAL_MEMO_FOR(type, value, cmp_opt)
50
- #define CMP_OPTIMIZABLE_BIT(type) (1U << TOKEN_PASTE(cmp_opt_,type))
51
- #define CMP_OPTIMIZABLE(data, type) \
52
- (((data).opt_inited & CMP_OPTIMIZABLE_BIT(type)) ? \
53
- ((data).opt_methods & CMP_OPTIMIZABLE_BIT(type)) : \
54
- (((data).opt_inited |= CMP_OPTIMIZABLE_BIT(type)), \
55
- rb_method_basic_definition_p(TOKEN_PASTE(rb_c,type), id_cmp) && \
56
- ((data).opt_methods |= CMP_OPTIMIZABLE_BIT(type))))
30
+ #define UNUSED(x) ((void)(x))
57
31
 
58
- #define OPTIMIZED_CMP(a, b, data) \
59
- ((FIXNUM_P(a) && FIXNUM_P(b) && CMP_OPTIMIZABLE(data, Integer)) ? \
60
- (((long)a > (long)b) ? 1 : ((long)a < (long)b) ? -1 : 0) : \
61
- (STRING_P(a) && STRING_P(b) && CMP_OPTIMIZABLE(data, String)) ? \
62
- rb_str_cmp(a, b) : \
63
- (RB_FLOAT_TYPE_P(a) && RB_FLOAT_TYPE_P(b) && CMP_OPTIMIZABLE(data, Float)) ? \
64
- rb_float_cmp(a, b) : \
65
- rb_cmpint(rb_funcallv(a, id_cmp, 1, &b), a, b))
32
+ #ifdef HAVE_RB_GC_MARK_MOVABLE
33
+ #define dheap_compact_callback(x) ((void (*)(void*))(x))
34
+ #define dheap_gc_location(x) x = rb_gc_location(x)
35
+ #else
36
+ #define rb_gc_mark_movable(x) rb_gc_mark(x)
37
+ #define dheap_compact_callback(x) {(x)}
38
+ #define dheap_gc_location(x) UNUSED(x)
39
+ #endif
66
40
 
67
- #define puts(v) { \
41
+ #ifdef __D_HEAP_DEBUG
42
+ #define debug(v) { \
68
43
  ID sym_puts = rb_intern("puts"); \
69
44
  rb_funcall(rb_mKernel, sym_puts, 1, v); \
70
45
  }
71
-
72
- #endif /* INTERNAL_COMPAR_H */
46
+ #else
47
+ #define debug(v)
48
+ #endif
73
49
 
74
50
  #endif /* D_HEAP_H */
@@ -1,3 +1,23 @@
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
+ # $CFLAGS << " -debug inline-debug-info "
8
+ # $CFLAGS << " -g -ginline-points "
9
+ # $CFLAGS << " -fno-omit-frame-pointer "
10
+
11
+ # CONFIG["debugflags"] << " -ggdb3 -gstatement-frontiers -ginline-points "
12
+ CONFIG["optflags"] << " -O3 "
13
+ CONFIG["optflags"] << " -fno-omit-frame-pointer "
14
+ CONFIG["warnflags"] << " -Werror"
15
+
16
+ have_func "rb_gc_mark_movable" # since ruby-2.7
17
+
18
+ check_sizeof("long")
19
+ check_sizeof("unsigned long long")
20
+ check_sizeof("long double")
21
+ have_macro("LDBL_MANT_DIG", "float.h")
22
+
3
23
  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,10 +1,48 @@
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
17
+ alias deq pop
18
+ alias enq push
19
+ alias first peek
20
+ alias pop_below pop_lt
21
+
22
+ alias length size
23
+ alias count size
24
+
25
+ # ruby 3.0+ (2.x can just use inherited initialize_clone)
26
+ if Object.instance_method(:initialize_clone).arity == -1
27
+ # @!visibility private
28
+ def initialize_clone(other, freeze: nil)
29
+ __init_clone__(other, freeze ? true : freeze)
30
+ end
31
+ end
5
32
 
6
- def initialize_copy(other)
7
- raise NotImplementedError, "initialize_copy should deep copy array"
33
+ # Consumes the heap by popping each minumum value until it is empty.
34
+ #
35
+ # If you want to iterate over the heap without consuming it, you will need to
36
+ # first call +#dup+
37
+ #
38
+ # @yieldparam value [Object] each value that would be popped
39
+ #
40
+ # @return [Enumerator] if no block is given
41
+ # @return [nil] if a block is given
42
+ def each_pop
43
+ return to_enum(__method__) unless block_given?
44
+ yield pop until empty?
45
+ nil
8
46
  end
9
47
 
10
48
  end
@@ -0,0 +1,112 @@
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, clear: false)
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.clear if clear
105
+ queue
106
+ end
107
+
108
+ # rubocop:enable Style/NumericPredicate
109
+
110
+ require "d_heap/benchmarks/implementations"
111
+
112
+ end
@@ -0,0 +1,116 @@
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
+ if file == "push_n"
91
+ cmd << "--filter" << /dheap|\bstl\b|\bbsearch\b|\brb_heap\b/.to_s
92
+ end
93
+ env = ENV.to_h.merge(
94
+ "BENCH_N" => size.to_s,
95
+ "RUBYLIB" => File.expand_path("../..", __dir__),
96
+ )
97
+ system(env, *cmd)
98
+ end
99
+ end
100
+
101
+ def sep(sep, msg = "", width: 80, big: false)
102
+ txt = String.new
103
+ txt += "#{sep * (width / sep.length)}\n" if big
104
+ txt += sep
105
+ txt += " #{msg}" if msg && !msg.empty?
106
+ txt += " " unless big
107
+ txt += sep * ((width - txt.length) / sep.length) unless big
108
+ txt += "\n"
109
+ txt += "#{sep * (width / sep.length)}\n" if big
110
+ io.print txt
111
+ end
112
+
113
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
114
+
115
+ end
116
+ end