hyll 0.1.1 → 1.0.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +102 -0
- data/README.md +132 -18
- data/examples/redis_comparison_benchmark.rb +539 -0
- data/examples/v1_benchmark.rb +93 -0
- data/lib/hyll/algorithms/enhanced_hyperloglog.rb +240 -119
- data/lib/hyll/algorithms/hyperloglog.rb +263 -327
- data/lib/hyll/constants.rb +75 -0
- data/lib/hyll/utils/hash.rb +132 -21
- data/lib/hyll/utils/math.rb +136 -66
- data/lib/hyll/version.rb +1 -1
- metadata +4 -2
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "hyll"
|
|
4
|
+
require "redis"
|
|
5
|
+
require "benchmark"
|
|
6
|
+
require "benchmark/ips"
|
|
7
|
+
require "memory_profiler"
|
|
8
|
+
require "optparse"
|
|
9
|
+
require "json"
|
|
10
|
+
|
|
11
|
+
class HyllRedisComparison
|
|
12
|
+
def initialize(precision: 10, data_size: 10_000, overlapping: 1_000, warm_up: true, benchmark_type: :all)
|
|
13
|
+
@precision = precision
|
|
14
|
+
@data_size = data_size
|
|
15
|
+
@overlapping = overlapping
|
|
16
|
+
@warm_up = warm_up
|
|
17
|
+
@benchmark_type = benchmark_type
|
|
18
|
+
@redis = Redis.new
|
|
19
|
+
@results = {}
|
|
20
|
+
|
|
21
|
+
puts "Initializing benchmark with:"
|
|
22
|
+
puts " - Precision: #{@precision}"
|
|
23
|
+
puts " - Data size: #{@data_size} elements"
|
|
24
|
+
puts " - Overlapping elements for merge tests: #{@overlapping}"
|
|
25
|
+
puts " - Warm-up: #{@warm_up ? "Enabled" : "Disabled"}"
|
|
26
|
+
puts "\n"
|
|
27
|
+
|
|
28
|
+
# Clean up any existing Redis keys
|
|
29
|
+
@redis.del("hll1", "hll2", "hll_merged", "redis_hll1", "redis_hll2", "redis_merged", "accuracy_test")
|
|
30
|
+
|
|
31
|
+
# Pre-generazione di dati per i test
|
|
32
|
+
@elements = (0...@data_size).map { |i| "element-#{i}" }.freeze
|
|
33
|
+
@set1_elements = (0...@data_size).map { |i| "set1-#{i}" }.freeze
|
|
34
|
+
@set2_elements = (0...@data_size).map { |i| "set2-#{i + @data_size - @overlapping}" }.freeze
|
|
35
|
+
|
|
36
|
+
# Pre-caricamento Redis
|
|
37
|
+
if %i[all cardinality memory].include?(benchmark_type)
|
|
38
|
+
@redis.pipelined do |pipeline|
|
|
39
|
+
@elements.each { |e| pipeline.pfadd("hll1", e) }
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
if %i[all merge].include?(benchmark_type)
|
|
44
|
+
@redis.pipelined do |pipeline|
|
|
45
|
+
@set1_elements.each { |e| pipeline.pfadd("redis_hll1", e) }
|
|
46
|
+
@set2_elements.each { |e| pipeline.pfadd("redis_hll2", e) }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
if %i[all cardinality].include?(benchmark_type)
|
|
51
|
+
@pre_hll_standard = Hyll::HyperLogLog.new(@precision)
|
|
52
|
+
@pre_hll_enhanced = Hyll::EnhancedHyperLogLog.new(@precision)
|
|
53
|
+
|
|
54
|
+
@elements.each do |e|
|
|
55
|
+
@pre_hll_standard.add(e)
|
|
56
|
+
@pre_hll_enhanced.add(e)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
if %i[all merge].include?(benchmark_type)
|
|
61
|
+
@pre_merge_hll1 = Hyll::HyperLogLog.new(@precision)
|
|
62
|
+
@pre_merge_hll2 = Hyll::HyperLogLog.new(@precision)
|
|
63
|
+
@pre_merge_enhanced1 = Hyll::EnhancedHyperLogLog.new(@precision)
|
|
64
|
+
@pre_merge_enhanced2 = Hyll::EnhancedHyperLogLog.new(@precision)
|
|
65
|
+
|
|
66
|
+
@set1_elements.each do |e|
|
|
67
|
+
@pre_merge_hll1.add(e)
|
|
68
|
+
@pre_merge_enhanced1.add(e)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
@set2_elements.each do |e|
|
|
72
|
+
@pre_merge_hll2.add(e)
|
|
73
|
+
@pre_merge_enhanced2.add(e)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
@pre_merge_hll1_serialized = Marshal.dump(@pre_merge_hll1)
|
|
77
|
+
@pre_merge_enhanced1_serialized = Marshal.dump(@pre_merge_enhanced1)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
warm_up_benchmarks if @warm_up
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def run_benchmarks
|
|
84
|
+
case @benchmark_type
|
|
85
|
+
when :insertion
|
|
86
|
+
benchmark_insertion
|
|
87
|
+
when :cardinality
|
|
88
|
+
benchmark_cardinality
|
|
89
|
+
when :merge
|
|
90
|
+
benchmark_merge
|
|
91
|
+
when :memory
|
|
92
|
+
benchmark_memory_usage
|
|
93
|
+
when :accuracy
|
|
94
|
+
benchmark_accuracy
|
|
95
|
+
else
|
|
96
|
+
benchmark_insertion
|
|
97
|
+
benchmark_cardinality
|
|
98
|
+
benchmark_merge
|
|
99
|
+
benchmark_memory_usage
|
|
100
|
+
benchmark_accuracy
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
print_summary
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def warm_up_benchmarks
|
|
107
|
+
puts "Performing warm-up..."
|
|
108
|
+
# Warm-up JIT compiler
|
|
109
|
+
warm_up_count = [@data_size / 10, 1000].min
|
|
110
|
+
|
|
111
|
+
# Warm-up insertion
|
|
112
|
+
hll_warmup = Hyll::HyperLogLog.new(@precision)
|
|
113
|
+
enhanced_warmup = Hyll::EnhancedHyperLogLog.new(@precision)
|
|
114
|
+
|
|
115
|
+
warm_up_count.times do |i|
|
|
116
|
+
hll_warmup.add("warmup-#{i}")
|
|
117
|
+
enhanced_warmup.add("warmup-#{i}")
|
|
118
|
+
@redis.pfadd("warmup_hll", "warmup-#{i}")
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Warm-up cardinality
|
|
122
|
+
10.times do
|
|
123
|
+
hll_warmup.cardinality
|
|
124
|
+
hll_warmup.mle_cardinality
|
|
125
|
+
enhanced_warmup.cardinality
|
|
126
|
+
enhanced_warmup.cardinality(use_streaming: true)
|
|
127
|
+
@redis.pfcount("warmup_hll")
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Warm-up merge
|
|
131
|
+
warm_up_hll1 = Hyll::HyperLogLog.new(@precision)
|
|
132
|
+
warm_up_hll2 = Hyll::HyperLogLog.new(@precision)
|
|
133
|
+
5.times do
|
|
134
|
+
warm_up_copy = Marshal.load(Marshal.dump(warm_up_hll1))
|
|
135
|
+
warm_up_copy.merge(warm_up_hll2)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
@redis.del("warmup_hll")
|
|
139
|
+
puts "Warm-up complete.\n\n"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def benchmark_insertion
|
|
143
|
+
puts "=== Insertion Performance ==="
|
|
144
|
+
GC.start # Sincronizzazione GC per risultati più consistenti
|
|
145
|
+
|
|
146
|
+
results = Benchmark.ips do |x|
|
|
147
|
+
x.config(time: 2, warmup: 1)
|
|
148
|
+
|
|
149
|
+
# Hyll standard insertion
|
|
150
|
+
x.report("Hyll Standard") do
|
|
151
|
+
hll = Hyll::HyperLogLog.new(@precision)
|
|
152
|
+
@elements.each { |e| hll.add(e) }
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Hyll enhanced insertion
|
|
156
|
+
x.report("Hyll Enhanced") do
|
|
157
|
+
hll = Hyll::EnhancedHyperLogLog.new(@precision)
|
|
158
|
+
@elements.each { |e| hll.add(e) }
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Hyll batch insertion
|
|
162
|
+
x.report("Hyll Batch") do
|
|
163
|
+
Hyll::HyperLogLog.new(@precision).add_all(@elements)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Redis insertion
|
|
167
|
+
x.report("Redis PFADD") do
|
|
168
|
+
@redis.del("bench_hll")
|
|
169
|
+
@elements.each { |e| @redis.pfadd("bench_hll", e) }
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Redis pipelined insertion
|
|
173
|
+
x.report("Redis Pipelined") do
|
|
174
|
+
@redis.del("bench_hll")
|
|
175
|
+
@redis.pipelined do |pipeline|
|
|
176
|
+
@elements.each { |e| pipeline.pfadd("bench_hll", e) }
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
x.compare!
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
@results[:insertion] = results
|
|
184
|
+
puts "\n"
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def benchmark_cardinality
|
|
188
|
+
puts "=== Cardinality Estimation Performance ==="
|
|
189
|
+
GC.start
|
|
190
|
+
|
|
191
|
+
results = Benchmark.ips do |x|
|
|
192
|
+
x.config(time: 2, warmup: 1)
|
|
193
|
+
|
|
194
|
+
# Hyll standard cardinality
|
|
195
|
+
x.report("Hyll Standard") do
|
|
196
|
+
@pre_hll_standard.cardinality
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Hyll standard MLE
|
|
200
|
+
x.report("Hyll MLE") do
|
|
201
|
+
@pre_hll_standard.mle_cardinality
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Hyll enhanced cardinality
|
|
205
|
+
x.report("Hyll Enhanced") do
|
|
206
|
+
@pre_hll_enhanced.cardinality
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Hyll enhanced streaming
|
|
210
|
+
x.report("Hyll Enhanced Stream") do
|
|
211
|
+
@pre_hll_enhanced.cardinality(use_streaming: true)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Redis cardinality
|
|
215
|
+
x.report("Redis PFCOUNT") do
|
|
216
|
+
@redis.pfcount("hll1")
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
x.compare!
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
@results[:cardinality] = results
|
|
223
|
+
puts "\n"
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def benchmark_merge
|
|
227
|
+
puts "=== Merge Performance ==="
|
|
228
|
+
GC.start
|
|
229
|
+
|
|
230
|
+
results = Benchmark.ips do |x|
|
|
231
|
+
x.config(time: 2, warmup: 1)
|
|
232
|
+
|
|
233
|
+
# Hyll standard merge
|
|
234
|
+
x.report("Hyll Standard") do
|
|
235
|
+
hll_copy = Marshal.load(@pre_merge_hll1_serialized)
|
|
236
|
+
hll_copy.merge(@pre_merge_hll2)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Hyll enhanced merge
|
|
240
|
+
x.report("Hyll Enhanced") do
|
|
241
|
+
enhanced_copy = Marshal.load(@pre_merge_enhanced1_serialized)
|
|
242
|
+
enhanced_copy.merge(@pre_merge_enhanced2)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Redis merge
|
|
246
|
+
x.report("Redis PFMERGE") do
|
|
247
|
+
@redis.pfmerge("redis_merged", "redis_hll1", "redis_hll2")
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
x.compare!
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
@results[:merge] = results
|
|
254
|
+
puts "\n"
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def benchmark_memory_usage
|
|
258
|
+
puts "=== Memory Usage ==="
|
|
259
|
+
GC.start
|
|
260
|
+
|
|
261
|
+
# Memory usage of standard HLL
|
|
262
|
+
hll_standard_memory = report_memory("Hyll Standard") do
|
|
263
|
+
hll = Hyll::HyperLogLog.new(@precision)
|
|
264
|
+
@elements.each { |e| hll.add(e) }
|
|
265
|
+
hll
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Memory usage of enhanced HLL
|
|
269
|
+
hll_enhanced_memory = report_memory("Hyll Enhanced") do
|
|
270
|
+
hll = Hyll::EnhancedHyperLogLog.new(@precision)
|
|
271
|
+
@elements.each { |e| hll.add(e) }
|
|
272
|
+
hll
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Memory usage of actual elements (for comparison)
|
|
276
|
+
raw_elements_memory = report_memory("Raw Elements Array") do
|
|
277
|
+
@elements.dup
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Redis memory usage
|
|
281
|
+
redis_memory = @redis.memory("USAGE", "hll1")
|
|
282
|
+
puts "Redis memory usage for HLL key: #{redis_memory} bytes"
|
|
283
|
+
|
|
284
|
+
# Calcola compression ratio
|
|
285
|
+
puts "\nCompression ratios:"
|
|
286
|
+
puts " Hyll Standard: #{(raw_elements_memory[:allocated] / hll_standard_memory[:retained]).round(2)}x"
|
|
287
|
+
puts " Hyll Enhanced: #{(raw_elements_memory[:allocated] / hll_enhanced_memory[:retained]).round(2)}x"
|
|
288
|
+
puts " Redis: #{(raw_elements_memory[:allocated] * 1024 / redis_memory).round(2)}x"
|
|
289
|
+
|
|
290
|
+
@results[:memory] = {
|
|
291
|
+
hyll_standard: hll_standard_memory,
|
|
292
|
+
hyll_enhanced: hll_enhanced_memory,
|
|
293
|
+
raw_elements: raw_elements_memory,
|
|
294
|
+
redis: redis_memory
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
puts "\n"
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def benchmark_accuracy
|
|
301
|
+
puts "=== Accuracy Comparison ==="
|
|
302
|
+
GC.start
|
|
303
|
+
|
|
304
|
+
accuracy_elements = (0...@data_size).map { |i| "accuracy-#{i}" }
|
|
305
|
+
|
|
306
|
+
# Hyll standard
|
|
307
|
+
hll_standard = Hyll::HyperLogLog.new(@precision)
|
|
308
|
+
hll_standard.add_all(accuracy_elements)
|
|
309
|
+
|
|
310
|
+
# Hyll enhanced
|
|
311
|
+
hll_enhanced = Hyll::EnhancedHyperLogLog.new(@precision)
|
|
312
|
+
hll_enhanced.add_all(accuracy_elements)
|
|
313
|
+
|
|
314
|
+
hll_standard_high = Hyll::HyperLogLog.new([@precision + 2, 16].min)
|
|
315
|
+
hll_standard_high.add_all(accuracy_elements)
|
|
316
|
+
|
|
317
|
+
# Redis
|
|
318
|
+
@redis.del("accuracy_test")
|
|
319
|
+
@redis.pipelined do |pipeline|
|
|
320
|
+
accuracy_elements.each { |e| pipeline.pfadd("accuracy_test", e) }
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# Get estimates
|
|
324
|
+
hll_standard_est = hll_standard.cardinality
|
|
325
|
+
hll_standard_mle = hll_standard.mle_cardinality
|
|
326
|
+
hll_standard_high_est = hll_standard_high.cardinality
|
|
327
|
+
hll_enhanced_est = hll_enhanced.cardinality
|
|
328
|
+
hll_enhanced_stream = hll_enhanced.cardinality(use_streaming: true)
|
|
329
|
+
redis_est = @redis.pfcount("accuracy_test")
|
|
330
|
+
|
|
331
|
+
# Calcola errori
|
|
332
|
+
standard_error = calculate_error("Hyll Standard", hll_standard_est, @data_size)
|
|
333
|
+
standard_mle_error = calculate_error("Hyll Standard MLE", hll_standard_mle, @data_size)
|
|
334
|
+
standard_high_error = calculate_error("Hyll Standard (High Precision)", hll_standard_high_est, @data_size)
|
|
335
|
+
enhanced_error = calculate_error("Hyll Enhanced", hll_enhanced_est, @data_size)
|
|
336
|
+
enhanced_stream_error = calculate_error("Hyll Enhanced Stream", hll_enhanced_stream, @data_size)
|
|
337
|
+
redis_error = calculate_error("Redis", redis_est, @data_size)
|
|
338
|
+
|
|
339
|
+
@results[:accuracy] = {
|
|
340
|
+
hyll_standard: standard_error,
|
|
341
|
+
hyll_standard_mle: standard_mle_error,
|
|
342
|
+
hyll_standard_high: standard_high_error,
|
|
343
|
+
hyll_enhanced: enhanced_error,
|
|
344
|
+
hyll_enhanced_stream: enhanced_stream_error,
|
|
345
|
+
redis: redis_error
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
# Grafico dell'errore (ASCII art)
|
|
349
|
+
puts "\nError comparison (lower is better):"
|
|
350
|
+
print_error_bar("Hyll Standard", standard_error[:percent])
|
|
351
|
+
print_error_bar("Hyll MLE", standard_mle_error[:percent])
|
|
352
|
+
print_error_bar("Hyll (High Prec)", standard_high_error[:percent])
|
|
353
|
+
print_error_bar("Hyll Enhanced", enhanced_error[:percent])
|
|
354
|
+
print_error_bar("Hyll Enh Stream", enhanced_stream_error[:percent])
|
|
355
|
+
print_error_bar("Redis", redis_error[:percent])
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def print_error_bar(label, error_pct)
|
|
359
|
+
display_error = [error_pct, 300].min
|
|
360
|
+
bars = (display_error / 5).to_i
|
|
361
|
+
truncated = display_error < error_pct
|
|
362
|
+
|
|
363
|
+
printf("%-18s |%-60s| %.2f%%%s\n",
|
|
364
|
+
label,
|
|
365
|
+
"#" * bars,
|
|
366
|
+
error_pct,
|
|
367
|
+
truncated ? " (truncated)" : "")
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def print_summary
|
|
371
|
+
puts "\n=== BENCHMARK SUMMARY ==="
|
|
372
|
+
puts "Precision: #{@precision}, Data size: #{@data_size}"
|
|
373
|
+
|
|
374
|
+
puts "\nACCURACY WINNER: #{get_accuracy_winner}" if @results[:accuracy]
|
|
375
|
+
|
|
376
|
+
if @results[:insertion] && @results[:cardinality] && @results[:merge]
|
|
377
|
+
puts "\nPERFORMANCE WINNERS:"
|
|
378
|
+
puts " Insertion: #{get_insertion_winner}"
|
|
379
|
+
puts " Cardinality: #{get_cardinality_winner}"
|
|
380
|
+
puts " Merge: #{get_merge_winner}"
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
puts "\nMEMORY USAGE WINNER: #{get_memory_winner}" if @results[:memory]
|
|
384
|
+
|
|
385
|
+
puts "\nRECOMMENDATION:"
|
|
386
|
+
puts generate_recommendation
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
def get_accuracy_winner
|
|
390
|
+
errors = @results[:accuracy].transform_values { |v| v[:percent] }
|
|
391
|
+
winner = errors.min_by { |_, v| v }
|
|
392
|
+
"#{winner[0].to_s.split("_").map(&:capitalize).join(" ")} (#{winner[1].round(2)}% error)"
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def get_insertion_winner
|
|
396
|
+
@results[:insertion].entries.max_by(&:ips).label
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def get_cardinality_winner
|
|
400
|
+
@results[:cardinality].entries.max_by(&:ips).label
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def get_merge_winner
|
|
404
|
+
@results[:merge].entries.max_by(&:ips).label
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def get_memory_winner
|
|
408
|
+
memories = {
|
|
409
|
+
hyll_standard: @results[:memory][:hyll_standard][:retained],
|
|
410
|
+
hyll_enhanced: @results[:memory][:hyll_enhanced][:retained],
|
|
411
|
+
redis: @results[:memory][:redis]
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
winner = memories.min_by { |_, v| v }
|
|
415
|
+
"#{winner[0].to_s.split("_").map(&:capitalize).join(" ")} (#{winner[1] / 1024.0} KB)"
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
def generate_recommendation
|
|
419
|
+
return "Run accuracy benchmark to generate recommendation" unless @results[:accuracy]
|
|
420
|
+
|
|
421
|
+
errors = @results[:accuracy].transform_values { |v| v[:percent] }
|
|
422
|
+
|
|
423
|
+
if errors[:redis] < 5.0
|
|
424
|
+
"Redis offers excellent accuracy and good performance, recommended for most use cases."
|
|
425
|
+
elsif errors[:hyll_standard] < errors[:hyll_enhanced] && errors[:hyll_standard] < 15.0
|
|
426
|
+
"Hyll Standard with precision #{@precision} offers good accuracy and best insertion performance."
|
|
427
|
+
elsif errors[:hyll_enhanced] < 15.0
|
|
428
|
+
"Hyll Enhanced offers better accuracy than Standard and good overall performance."
|
|
429
|
+
else
|
|
430
|
+
"Consider using higher precision (#{[@precision + 2, 16].min}) for better accuracy."
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
def export_results(filename)
|
|
435
|
+
File.write(filename, JSON.pretty_generate(@results))
|
|
436
|
+
puts "Results exported to #{filename}"
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
private
|
|
440
|
+
|
|
441
|
+
def report_memory(label)
|
|
442
|
+
GC.start # Force GC before measurement
|
|
443
|
+
result = nil
|
|
444
|
+
report = MemoryProfiler.report do
|
|
445
|
+
result = yield
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
allocated = report.total_allocated_memsize / 1024.0
|
|
449
|
+
retained = report.total_retained_memsize / 1024.0
|
|
450
|
+
|
|
451
|
+
puts "#{label}:"
|
|
452
|
+
puts " Total allocated: #{allocated.round(2)} KB"
|
|
453
|
+
puts " Total retained: #{retained.round(2)} KB"
|
|
454
|
+
|
|
455
|
+
# Return memory stats
|
|
456
|
+
{ allocated: allocated, retained: retained, result: result }
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
def calculate_error(label, estimate, actual)
|
|
460
|
+
error_pct = ((estimate - actual).abs / actual.to_f) * 100
|
|
461
|
+
result = {
|
|
462
|
+
estimate: estimate.round,
|
|
463
|
+
actual: actual,
|
|
464
|
+
difference: (estimate - actual).round,
|
|
465
|
+
percent: error_pct.round(2)
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
puts "#{label}: Estimated #{result[:estimate]} vs Actual #{actual} (Error: #{result[:percent]}%)"
|
|
469
|
+
result
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
# Parse command line options
|
|
474
|
+
options = {
|
|
475
|
+
precision: 10,
|
|
476
|
+
data_size: 10_000,
|
|
477
|
+
overlapping: 1_000,
|
|
478
|
+
warm_up: true,
|
|
479
|
+
benchmark_type: :all,
|
|
480
|
+
output_file: nil
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
OptionParser.new do |opts|
|
|
484
|
+
opts.banner = "Usage: ruby redis_comparison_benchmark.rb [options]"
|
|
485
|
+
|
|
486
|
+
opts.on("-p", "--precision PRECISION", Integer, "HyperLogLog precision (4-16)") do |p|
|
|
487
|
+
options[:precision] = p
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
opts.on("-d", "--data-size SIZE", Integer, "Number of elements to add") do |d|
|
|
491
|
+
options[:data_size] = d
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
opts.on("-o", "--overlapping SIZE", Integer, "Number of overlapping elements for merge tests") do |o|
|
|
495
|
+
options[:overlapping] = o
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
opts.on("--no-warm-up", "Skip warm-up phase") do
|
|
499
|
+
options[:warm_up] = false
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
opts.on("-b", "--benchmark TYPE", %i[all insertion cardinality merge memory accuracy],
|
|
503
|
+
"Run specific benchmark type (all, insertion, cardinality, merge, memory, accuracy)") do |b|
|
|
504
|
+
options[:benchmark_type] = b
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
opts.on("--output FILE", "Export results to JSON file") do |f|
|
|
508
|
+
options[:output_file] = f
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
opts.on("-h", "--help", "Show this help message") do
|
|
512
|
+
puts opts
|
|
513
|
+
exit
|
|
514
|
+
end
|
|
515
|
+
end.parse!
|
|
516
|
+
|
|
517
|
+
# Run benchmarks
|
|
518
|
+
puts "Starting HyperLogLog Comparison Benchmark: Hyll vs Redis"
|
|
519
|
+
puts "-----------------------------------------------------"
|
|
520
|
+
|
|
521
|
+
begin
|
|
522
|
+
comparison = HyllRedisComparison.new(
|
|
523
|
+
precision: options[:precision],
|
|
524
|
+
data_size: options[:data_size],
|
|
525
|
+
overlapping: options[:overlapping],
|
|
526
|
+
warm_up: options[:warm_up],
|
|
527
|
+
benchmark_type: options[:benchmark_type]
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
comparison.run_benchmarks
|
|
531
|
+
|
|
532
|
+
comparison.export_results(options[:output_file]) if options[:output_file]
|
|
533
|
+
rescue Redis::CannotConnectError
|
|
534
|
+
puts "ERROR: Cannot connect to Redis server."
|
|
535
|
+
puts "Please ensure Redis is running locally on the default port (6379)."
|
|
536
|
+
puts "You can start Redis with: redis-server"
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
puts "Benchmark complete!"
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Hyll v1.0.0 - Blazing Fast Edition Benchmark
|
|
4
|
+
# Run with: ruby examples/v1_benchmark.rb
|
|
5
|
+
|
|
6
|
+
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
|
|
7
|
+
require "hyll"
|
|
8
|
+
require "benchmark"
|
|
9
|
+
|
|
10
|
+
puts "=" * 60
|
|
11
|
+
puts "HYLL v1.0.0 - BLAZING FAST EDITION BENCHMARK"
|
|
12
|
+
puts "=" * 60
|
|
13
|
+
puts
|
|
14
|
+
|
|
15
|
+
# Test 1: Element Addition
|
|
16
|
+
puts "1. Element Addition Performance"
|
|
17
|
+
puts "-" * 40
|
|
18
|
+
|
|
19
|
+
hll_standard = Hyll.new(type: :standard)
|
|
20
|
+
hll_enhanced = Hyll.new(type: :enhanced)
|
|
21
|
+
|
|
22
|
+
# Use integers for cleaner testing
|
|
23
|
+
elements = (1..100_000).to_a
|
|
24
|
+
|
|
25
|
+
time_standard = Benchmark.measure { elements.each { |e| hll_standard.add(e) } }
|
|
26
|
+
puts "Standard HLL (100k elements): #{time_standard.real.round(4)}s"
|
|
27
|
+
|
|
28
|
+
time_enhanced = Benchmark.measure { elements.each { |e| hll_enhanced.add(e) } }
|
|
29
|
+
puts "Enhanced HLL (100k elements): #{time_enhanced.real.round(4)}s"
|
|
30
|
+
puts
|
|
31
|
+
|
|
32
|
+
# Test 2: Batch Addition
|
|
33
|
+
puts "2. Batch Addition Performance"
|
|
34
|
+
puts "-" * 40
|
|
35
|
+
|
|
36
|
+
hll_batch = Hyll.new(type: :standard)
|
|
37
|
+
time_batch = Benchmark.measure { hll_batch.add_all(elements) }
|
|
38
|
+
puts "Standard HLL batch (100k elements): #{time_batch.real.round(4)}s"
|
|
39
|
+
puts "Speedup vs individual: #{(time_standard.real / time_batch.real).round(2)}x"
|
|
40
|
+
puts
|
|
41
|
+
|
|
42
|
+
# Test 3: Cardinality Calculation
|
|
43
|
+
puts "3. Cardinality Calculation Performance"
|
|
44
|
+
puts "-" * 40
|
|
45
|
+
|
|
46
|
+
hll = Hyll.new(type: :standard)
|
|
47
|
+
hll.add_all(elements)
|
|
48
|
+
|
|
49
|
+
time_card = Benchmark.measure { 1000.times { hll.cardinality } }
|
|
50
|
+
puts "Standard HLL (1000 calls): #{time_card.real.round(4)}s"
|
|
51
|
+
puts "Per call: #{((time_card.real / 1000) * 1_000_000).round(2)} microseconds"
|
|
52
|
+
puts
|
|
53
|
+
|
|
54
|
+
# Test 4: Memory Efficiency
|
|
55
|
+
puts "4. Memory Efficiency"
|
|
56
|
+
puts "-" * 40
|
|
57
|
+
|
|
58
|
+
# For integers, estimate as 8 bytes each (Fixnum size)
|
|
59
|
+
array_size = elements.size * 8
|
|
60
|
+
hll_size = hll.serialize.bytesize
|
|
61
|
+
|
|
62
|
+
puts "Raw data size: #{array_size} bytes"
|
|
63
|
+
puts "HLL serialized size: #{hll_size} bytes"
|
|
64
|
+
puts "Compression ratio: #{(array_size.to_f / hll_size).round(2)}x"
|
|
65
|
+
puts
|
|
66
|
+
|
|
67
|
+
# Test 5: Accuracy
|
|
68
|
+
puts "5. Accuracy Check"
|
|
69
|
+
puts "-" * 40
|
|
70
|
+
puts "Actual count: 100,000"
|
|
71
|
+
puts "Estimated count: #{hll.cardinality.round(0)}"
|
|
72
|
+
puts "Error: #{((hll.cardinality - 100_000).abs / 100_000.0 * 100).round(2)}%"
|
|
73
|
+
puts
|
|
74
|
+
|
|
75
|
+
# Test 6: Hash Performance
|
|
76
|
+
puts "6. Hash Function Performance"
|
|
77
|
+
puts "-" * 40
|
|
78
|
+
|
|
79
|
+
class HashTester
|
|
80
|
+
include Hyll::Utils::Hash
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
tester = HashTester.new
|
|
84
|
+
time_hash = Benchmark.measure do
|
|
85
|
+
100_000.times { |i| tester.murmurhash3(i.to_s) }
|
|
86
|
+
end
|
|
87
|
+
puts "MurmurHash3 (100k hashes): #{time_hash.real.round(4)}s"
|
|
88
|
+
puts "Per hash: #{((time_hash.real / 100_000) * 1_000_000).round(2)} microseconds"
|
|
89
|
+
puts
|
|
90
|
+
|
|
91
|
+
puts "=" * 60
|
|
92
|
+
puts "BENCHMARK COMPLETE"
|
|
93
|
+
puts "=" * 60
|