d_heap 0.3.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.
@@ -11,6 +11,12 @@
11
11
  // comparisons as d gets further from 4.
12
12
  #define DHEAP_MAX_D 32
13
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))
19
+
14
20
  VALUE rb_cDHeap;
15
21
 
16
22
  // copied from pg gem
@@ -26,9 +32,6 @@ VALUE rb_cDHeap;
26
32
  #define dheap_gc_location(x) UNUSED(x)
27
33
  #endif
28
34
 
29
- // from internal/compar.h
30
- #define STRING_P(s) (RB_TYPE_P((s), T_STRING) && CLASS_OF(s) == rb_cString)
31
-
32
35
  #ifdef __D_HEAP_DEBUG
33
36
  #define debug(v) { \
34
37
  ID sym_puts = rb_intern("puts"); \
@@ -2,10 +2,15 @@
2
2
 
3
3
  require "mkmf"
4
4
 
5
- # if /darwin/ =~ RUBY_PLATFORM
6
- # $CFLAGS << " -D__D_HEAP_DEBUG"
7
- # end
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
8
7
 
9
8
  have_func "rb_gc_mark_movable" # since ruby-2.7
10
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"
11
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
@@ -14,9 +14,11 @@ require "d_heap/version"
14
14
  # worst-case time complexity.
15
15
  #
16
16
  class DHeap
17
-
18
- def initialize_copy(other)
19
- raise NotImplementedError, "initialize_copy should deep copy array"
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
20
22
  end
21
23
 
22
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
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DHeap::Benchmarks
4
+
5
+ # base class for example priority queues
6
+ class ExamplePriorityQueue
7
+ attr_reader :a
8
+
9
+ def initialize
10
+ @a = []
11
+ end
12
+
13
+ def clear
14
+ @a.clear
15
+ end
16
+
17
+ def empty?
18
+ @a.empty?
19
+ end
20
+
21
+ if ENV["LOG_LEVEL"] == "debug"
22
+ def dbg(msg)
23
+ puts "%20s: %p, %p" % [msg, @a.first, (@a[1..-1] || []).each_slice(2).to_a]
24
+ end
25
+ else
26
+ def dbg(msg) nil end
27
+ end
28
+
29
+ end
30
+
31
+ # The most naive approach--completely unsorted!--is ironically not the worst.
32
+ class FindMin < ExamplePriorityQueue
33
+
34
+ # O(1)
35
+ def <<(score)
36
+ raise ArgumentError unless score
37
+ @a.push score
38
+ end
39
+
40
+ # O(n)
41
+ def pop
42
+ return unless (score = @a.min)
43
+ index = @a.rindex(score)
44
+ @a.delete_at(index)
45
+ score
46
+ end
47
+
48
+ end
49
+
50
+ # Re-sorting after each insert: this both naive and performs the worst.
51
+ class Sorting < ExamplePriorityQueue
52
+
53
+ # O(n log n)
54
+ def <<(score)
55
+ raise ArgumentError unless score
56
+ @a.push score
57
+ @a.sort!
58
+ end
59
+
60
+ # O(1)
61
+ def pop
62
+ @a.shift
63
+ end
64
+
65
+ end
66
+
67
+ # A very simple example priority queue that is implemented with a sorted array.
68
+ #
69
+ # It uses Array#bsearch + Array#insert to push new values, and Array#pop to pop
70
+ # the min value.
71
+ class BSearch < ExamplePriorityQueue
72
+
73
+ # Array#bsearch_index is O(log n)
74
+ # Array#insert is O(n)
75
+ #
76
+ # So this should be O(n).
77
+ #
78
+ # In practice though, memcpy has a *very* small constant factor.
79
+ # And bsearch_index uses *exactly* (log n / log 2) comparisons.
80
+ def <<(score)
81
+ raise ArgumentError unless score
82
+ index = @a.bsearch_index {|other| score > other } || @a.length
83
+ @a.insert(index, score)
84
+ end
85
+
86
+ # Array#pop is O(1). It updates length without changing capacity or contents.
87
+ #
88
+ # No comparisons are necessary.
89
+ #
90
+ # shift is usually also O(1) and could be used if it were sorted normally.
91
+ def pop
92
+ @a.pop
93
+ end
94
+
95
+ end
96
+
97
+ # a very simple pure ruby binary heap
98
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
99
+ class RbHeap < ExamplePriorityQueue
100
+
101
+ def <<(score)
102
+ raise ArgumentError unless score
103
+ @a.push(score)
104
+ # shift up
105
+ index = @a.size - 1
106
+ while 0 < index # rubocop:disable Style/NumericPredicate
107
+ parent_index = (index - 1) / 2
108
+ break if @a[parent_index] <= @a[index]
109
+ @a[index] = @a[parent_index]
110
+ index = parent_index
111
+ @a[index] = score
112
+ # check_heap!(index)
113
+ end
114
+ self
115
+ end
116
+
117
+ def pop
118
+ return if @a.empty?
119
+ popped = @a.first
120
+ @a[0] = shifting = @a.last
121
+ @a.pop
122
+ # shift down
123
+ index = 0
124
+ last_index = @a.size - 1
125
+ while (child_index = index * 2 + 1) <= last_index
126
+ # select min child
127
+ if child_index < last_index && @a[child_index + 1] < @a[child_index]
128
+ child_index += 1
129
+ end
130
+ break if @a[index] <= @a[child_index]
131
+ @a[index] = @a[child_index]
132
+ index = child_index
133
+ @a[index] = shifting
134
+ end
135
+ popped
136
+ end
137
+
138
+ private
139
+
140
+ def check_heap!(idx, last = @a.size - 1)
141
+ pscore = @a[idx]
142
+ child = idx * 2 + 1
143
+ if child <= last
144
+ cscore = check_heap!(child)
145
+ raise "#{pscore} > #{cscore}" if pscore > cscore
146
+ end
147
+ child += 1
148
+ if child <= last
149
+ check_heap!(child)
150
+ cscore = check_heap!(child)
151
+ raise "#{pscore} > #{cscore}" if pscore > cscore
152
+ end
153
+ pscore
154
+ end
155
+
156
+ end
157
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
158
+
159
+ # Different duck-typed priority queue implemenations
160
+ IMPLEMENTATIONS = [
161
+ OpenStruct.new(name: " push and resort", klass: Sorting).freeze,
162
+ OpenStruct.new(name: " find min + del", klass: FindMin).freeze,
163
+ OpenStruct.new(name: "bsearch + insert", klass: BSearch).freeze,
164
+ OpenStruct.new(name: "ruby binary heap", klass: RbHeap).freeze,
165
+ OpenStruct.new(name: "quaternary DHeap", klass: DHeap).freeze,
166
+ ].freeze
167
+
168
+ end