d_heap 0.2.0 → 0.5.0

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