d_heap 0.3.0 → 0.4.0

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