ractor-pipeline 0.2.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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +367 -0
- data/Rakefile +12 -0
- data/examples/demo.rb +138 -0
- data/examples/logstats.rb +114 -0
- data/examples/perf.rb +184 -0
- data/examples/readme_bench.rb +129 -0
- data/examples/vs_parallel.rb +116 -0
- data/lib/ractor/pipeline/version.rb +7 -0
- data/lib/ractor/pipeline.rb +462 -0
- data/sig/ractor/pipeline.rbs +6 -0
- metadata +56 -0
data/examples/perf.rb
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Performance demos of Ractor::Pipeline. Self-contained (synthetic data).
|
|
4
|
+
#
|
|
5
|
+
# ruby -I lib examples/perf.rb
|
|
6
|
+
#
|
|
7
|
+
# Note: speedup ceilings depend heavily on the machine (P/E core mix,
|
|
8
|
+
# all-core clock drop, etc.), and allocation-heavy stage blocks scale worse
|
|
9
|
+
# than pure computation.
|
|
10
|
+
|
|
11
|
+
Warning[:experimental] = false # suppress "Ractor API is experimental"
|
|
12
|
+
|
|
13
|
+
require "ractor/pipeline"
|
|
14
|
+
|
|
15
|
+
include Ractor::Pipeline
|
|
16
|
+
|
|
17
|
+
def section(title)
|
|
18
|
+
puts
|
|
19
|
+
puts "== #{title}"
|
|
20
|
+
yield
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def bench
|
|
24
|
+
t = Time.now
|
|
25
|
+
result = yield
|
|
26
|
+
[result, Time.now - t]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def report(label, dt, base = nil)
|
|
30
|
+
if base
|
|
31
|
+
printf " %-36s %8.3fs (x%.2f)\n", label, dt, base / dt
|
|
32
|
+
else
|
|
33
|
+
printf " %-36s %8.3fs\n", label, dt
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def fib(n) = n < 2 ? n : fib(n - 1) + fib(n - 2)
|
|
38
|
+
|
|
39
|
+
section "CPU-bound scaling: 16 x fib(30), pipe(lanes: n)" do
|
|
40
|
+
items = 16
|
|
41
|
+
|
|
42
|
+
expected, base = bench{ (1..items).sum{ fib(30) } }
|
|
43
|
+
report "serial", base
|
|
44
|
+
|
|
45
|
+
[1, 2, 4, 8, 16].each do |n|
|
|
46
|
+
result, dt = bench do
|
|
47
|
+
stream(1..items).
|
|
48
|
+
pipe(lanes: n){ fib(30) }.
|
|
49
|
+
reduce(0){ |acc, v| acc + v }
|
|
50
|
+
end
|
|
51
|
+
raise "mismatch" unless result == expected
|
|
52
|
+
report "pipe(lanes: #{n})", dt, base
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Synthetic corpus for the text-processing demos.
|
|
57
|
+
WORDS = %w[Ractor pipeline port stream lane stage worker copy share value].freeze
|
|
58
|
+
|
|
59
|
+
def make_corpus(docs, lines_per_doc)
|
|
60
|
+
rng = Random.new(42)
|
|
61
|
+
Array.new(docs) do
|
|
62
|
+
Array.new(lines_per_doc) do
|
|
63
|
+
Array.new(8){ WORDS[rng.rand(WORDS.size)] }.join(" ")
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def word_tally(lines)
|
|
69
|
+
tally = Hash.new(0)
|
|
70
|
+
lines.each do |line|
|
|
71
|
+
line.scan(/\w+/){ |w| tally[w] += 1 }
|
|
72
|
+
end
|
|
73
|
+
tally
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
section "map-reduce: word ranking, 1 document = 1 message" do
|
|
77
|
+
corpus = make_corpus(160, 2_000)
|
|
78
|
+
|
|
79
|
+
serial_top, base = bench do
|
|
80
|
+
corpus.map{ word_tally(it) }.
|
|
81
|
+
reduce(Hash.new(0)){ |acc, t| t.each{ |w, n| acc[w] += n }; acc }.
|
|
82
|
+
max_by(3){ |_, n| n }
|
|
83
|
+
end
|
|
84
|
+
report "serial (#{corpus.size} docs)", base
|
|
85
|
+
|
|
86
|
+
parallel_top, dt = bench do
|
|
87
|
+
stream(corpus).
|
|
88
|
+
pipe(lanes: 8){ word_tally(it) }.
|
|
89
|
+
reduce(Hash.new(0)){ |acc, t| t.each{ |w, n| acc[w] += n }; acc }.
|
|
90
|
+
max_by(3){ |_, n| n }
|
|
91
|
+
end
|
|
92
|
+
report "pipe(lanes: 8) + reduce", dt, base
|
|
93
|
+
|
|
94
|
+
raise "mismatch" unless serial_top == parallel_top
|
|
95
|
+
puts " top words: " + serial_top.map{ |w, n| "#{w}(#{n})" }.join(", ")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
MW, MH, MITER = 78, 48, 3000
|
|
99
|
+
|
|
100
|
+
# Allocation-light row computation: allocation-heavy stage blocks scale
|
|
101
|
+
# worse across Ractors than pure computation.
|
|
102
|
+
def mandel_row(y)
|
|
103
|
+
ci = -1.0 + 2.0 * y / MH
|
|
104
|
+
row = String.new(capacity: MW)
|
|
105
|
+
x = 0
|
|
106
|
+
while x < MW
|
|
107
|
+
cr = -2.2 + 3.2 * x / MW
|
|
108
|
+
zr = zi = 0.0
|
|
109
|
+
n = 0
|
|
110
|
+
while n < MITER && zr * zr + zi * zi < 4.0
|
|
111
|
+
zr, zi = zr * zr - zi * zi + cr, 2 * zr * zi + ci
|
|
112
|
+
n += 1
|
|
113
|
+
end
|
|
114
|
+
row << (n == MITER ? "*" : " ")
|
|
115
|
+
x += 1
|
|
116
|
+
end
|
|
117
|
+
row
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
section "mandelbrot: rows in parallel, reassembled by row index" do
|
|
121
|
+
serial_pic, base = bench{ (0...MH).map{ mandel_row(it) } }
|
|
122
|
+
report "serial (#{MW}x#{MH}, iter=#{MITER})", base
|
|
123
|
+
|
|
124
|
+
# lanes: 8 is unordered, so carry the row index and reassemble.
|
|
125
|
+
parallel_pic, dt = bench do
|
|
126
|
+
stream(0...MH).
|
|
127
|
+
pipe(lanes: 8){ [it, mandel_row(it)] }.
|
|
128
|
+
reduce(Array.new(MH)){ |acc, (y, row)| acc[y] = row; acc }
|
|
129
|
+
end
|
|
130
|
+
report "pipe(lanes: 8)", dt, base
|
|
131
|
+
|
|
132
|
+
raise "mismatch" unless serial_pic == parallel_pic
|
|
133
|
+
puts parallel_pic.map{ |row| " |#{row}|" }
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
section "granularity: per-line vs per-chunk messages" do
|
|
137
|
+
# Fine-grained messages (1 line = 1 message) are dominated by
|
|
138
|
+
# copy/communication cost; chunking restores the speedup.
|
|
139
|
+
lines = make_corpus(1, 20_000).first
|
|
140
|
+
|
|
141
|
+
serial_count, base = bench{ lines.count{ it.include?("Ractor") } }
|
|
142
|
+
report "serial (#{lines.size} lines)", base
|
|
143
|
+
|
|
144
|
+
line_count, dt = bench do
|
|
145
|
+
stream(lines).
|
|
146
|
+
filter_pipe(lanes: 4){ it.include?("Ractor") }.
|
|
147
|
+
count
|
|
148
|
+
end
|
|
149
|
+
report "per-line filter_pipe(lanes: 4)", dt, base
|
|
150
|
+
|
|
151
|
+
chunk_count, dt = bench do
|
|
152
|
+
stream(lines.each_slice(1000)).
|
|
153
|
+
pipe(lanes: 4){ it.count{ |line| line.include?("Ractor") } }.
|
|
154
|
+
reduce(0){ |acc, n| acc + n }
|
|
155
|
+
end
|
|
156
|
+
report "1000-line chunks, pipe(lanes: 4)", dt, base
|
|
157
|
+
|
|
158
|
+
batch_count, dt = bench do
|
|
159
|
+
stream(lines, batch: 1000).
|
|
160
|
+
filter_pipe(lanes: 4){ it.include?("Ractor") }.
|
|
161
|
+
count
|
|
162
|
+
end
|
|
163
|
+
report "batch: 1000 (transparent)", dt, base
|
|
164
|
+
|
|
165
|
+
raise "mismatch" unless serial_count == line_count && serial_count == chunk_count && serial_count == batch_count
|
|
166
|
+
puts " matching lines: #{serial_count}"
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def busy(n) = (i = 0; i += 1 while i < n; i)
|
|
170
|
+
|
|
171
|
+
section "load balancing: one heavy element among light ones (lanes: 4)" do
|
|
172
|
+
# Demand-driven distribution: no new work is assigned to the worker
|
|
173
|
+
# that is stuck on the heavy element.
|
|
174
|
+
jobs = [40_000_000] + [4_000_000] * 31
|
|
175
|
+
|
|
176
|
+
_, base = bench{ jobs.each{ busy(it) } }
|
|
177
|
+
report "serial", base
|
|
178
|
+
|
|
179
|
+
_, dt = bench{ stream(jobs).pipe(lanes: 4){ busy(it) }.each{} }
|
|
180
|
+
report "pipe(lanes: 4)", dt, base
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
puts
|
|
184
|
+
puts "done."
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Collects the numbers quoted in README (min of 3 runs each) and prints
|
|
4
|
+
# them as a markdown table.
|
|
5
|
+
#
|
|
6
|
+
# ruby -I lib examples/readme_bench.rb
|
|
7
|
+
|
|
8
|
+
Warning[:experimental] = false
|
|
9
|
+
|
|
10
|
+
require "ractor/pipeline"
|
|
11
|
+
require "json"
|
|
12
|
+
|
|
13
|
+
include Ractor::Pipeline
|
|
14
|
+
|
|
15
|
+
LANES = [2, 4, 8, 16].freeze
|
|
16
|
+
|
|
17
|
+
def fib(n) = n < 2 ? n : fib(n - 1) + fib(n - 2)
|
|
18
|
+
def busy(n) = (i = 0; i += 1 while i < n; i)
|
|
19
|
+
|
|
20
|
+
def best(times = 5)
|
|
21
|
+
times.times.map { t = Time.now; yield; Time.now - t }.min
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
MW, MH, MITER = 78, 48, 3000
|
|
25
|
+
|
|
26
|
+
def mandel_row(y)
|
|
27
|
+
ci = -1.0 + 2.0 * y / MH
|
|
28
|
+
row = String.new(capacity: MW)
|
|
29
|
+
x = 0
|
|
30
|
+
while x < MW
|
|
31
|
+
cr = -2.2 + 3.2 * x / MW
|
|
32
|
+
zr = zi = 0.0
|
|
33
|
+
n = 0
|
|
34
|
+
while n < MITER && zr * zr + zi * zi < 4.0
|
|
35
|
+
zr, zi = zr * zr - zi * zi + cr, 2 * zr * zi + ci
|
|
36
|
+
n += 1
|
|
37
|
+
end
|
|
38
|
+
row << (n == MITER ? "*" : " ")
|
|
39
|
+
x += 1
|
|
40
|
+
end
|
|
41
|
+
row
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def agg(lines)
|
|
45
|
+
st = {}
|
|
46
|
+
lines.each do |l|
|
|
47
|
+
r = JSON.parse(l)
|
|
48
|
+
s = (st[r["path"]] ||= [0, 0, 0.0])
|
|
49
|
+
s[0] += 1
|
|
50
|
+
s[1] += 1 if r["status"] >= 500
|
|
51
|
+
s[2] += r["ms"]
|
|
52
|
+
end
|
|
53
|
+
st
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
puts RUBY_DESCRIPTION
|
|
57
|
+
|
|
58
|
+
rows = []
|
|
59
|
+
|
|
60
|
+
# 1. CPU-bound: 32 x fib(28)
|
|
61
|
+
items = 32
|
|
62
|
+
expected = (1..items).sum{ fib(28) }
|
|
63
|
+
base = best{ raise unless (1..items).sum{ fib(28) } == expected }
|
|
64
|
+
speedups = LANES.map do |n|
|
|
65
|
+
dt = best{ raise unless stream(1..items).pipe(lanes: n){ fib(28) }.reduce(0){ |a, v| a + v } == expected }
|
|
66
|
+
base / dt
|
|
67
|
+
end
|
|
68
|
+
rows << ["fib(28) x 32 (uniform)", base, speedups]
|
|
69
|
+
|
|
70
|
+
# 2. skewed load: 1 heavy + 31 light
|
|
71
|
+
jobs = [40_000_000] + [4_000_000] * 31
|
|
72
|
+
base = best{ jobs.each{ busy(it) } }
|
|
73
|
+
speedups = LANES.map do |n|
|
|
74
|
+
dt = best{ stream(jobs).pipe(lanes: n){ busy(it) }.each{} }
|
|
75
|
+
base / dt
|
|
76
|
+
end
|
|
77
|
+
rows << ["skewed load (1 heavy + 31 light)", base, speedups]
|
|
78
|
+
|
|
79
|
+
# 3. mandelbrot rows (uneven cost, ordered reassembly)
|
|
80
|
+
base = best{ (0...MH).map{ mandel_row(it) } }
|
|
81
|
+
speedups = LANES.map do |n|
|
|
82
|
+
dt = best{ stream(0...MH).pipe(lanes: n){ [it, mandel_row(it)] }.reduce(Array.new(MH)){ |a, (y, r)| a[y] = r; a } }
|
|
83
|
+
base / dt
|
|
84
|
+
end
|
|
85
|
+
rows << ["mandelbrot (48 rows, uneven)", base, speedups]
|
|
86
|
+
|
|
87
|
+
# 4. JSONL aggregation (allocation-bound), chunk-aggregate style
|
|
88
|
+
rng = Random.new(42)
|
|
89
|
+
paths = ["/", "/api/users", "/api/items", "/api/search", "/login", "/assets/app.js"]
|
|
90
|
+
lines = Array.new(400_000) do
|
|
91
|
+
%({"path":"#{paths[rng.rand(paths.size)]}","status":#{rng.rand(100) < 3 ? 500 : 200},"ms":#{(rng.rand * 300).round(1)}})
|
|
92
|
+
end
|
|
93
|
+
chunks = lines.each_slice(1000).to_a
|
|
94
|
+
base = best{ chunks.each{ agg(it) } }
|
|
95
|
+
speedups = LANES.map do |n|
|
|
96
|
+
dt = best do
|
|
97
|
+
stream(chunks).
|
|
98
|
+
pipe(lanes: n){ agg(it) }.
|
|
99
|
+
reduce({}){ |a, st| st.each{ |k, (q, e, m)| x = (a[k] ||= [0, 0, 0.0]); x[0] += q; x[1] += e; x[2] += m }; a }
|
|
100
|
+
end
|
|
101
|
+
base / dt
|
|
102
|
+
end
|
|
103
|
+
rows << ["JSONL aggregation (400k lines, alloc-bound)", base, speedups]
|
|
104
|
+
|
|
105
|
+
puts
|
|
106
|
+
puts "| workload (serial time) | " + LANES.map{ "lanes #{it}" }.join(" | ") + " |"
|
|
107
|
+
puts "|---|" + LANES.map{ "---" }.join("|") + "|"
|
|
108
|
+
rows.each do |name, base, speedups|
|
|
109
|
+
puts "| #{name} (#{"%.2fs" % base}) | " + speedups.map{ "x%.2f" % it }.join(" | ") + " |"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# 5. granularity (per-line vs batch)
|
|
113
|
+
gl = lines.first(100_000)
|
|
114
|
+
base = best{ gl.count{ it.include?(%("status":500)) } }
|
|
115
|
+
per_line = best{ stream(gl).filter_pipe(lanes: 4){ it.include?(%("status":500)) }.count }
|
|
116
|
+
batched = best{ stream(gl, batch: 1000).filter_pipe(lanes: 4){ it.include?(%("status":500)) }.count }
|
|
117
|
+
puts
|
|
118
|
+
printf "granularity: serial %.4fs / per-line %.3fs / batch1000 %.4fs\n", base, per_line, batched
|
|
119
|
+
|
|
120
|
+
# 6. throttling
|
|
121
|
+
class CountingSource
|
|
122
|
+
attr_reader :n
|
|
123
|
+
|
|
124
|
+
def initialize = @n = 0
|
|
125
|
+
def each = loop{ yield (@n += 1) }
|
|
126
|
+
end
|
|
127
|
+
cs = CountingSource.new
|
|
128
|
+
stream(cs).pipe(lanes: 2){ it }.first(3)
|
|
129
|
+
printf "throttle: source reads = %d (first(3), infinite source)\n", cs.n
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Compare Ractor::Pipeline with the parallel gem (process/thread based)
|
|
4
|
+
# on the map-shaped workloads from README.
|
|
5
|
+
#
|
|
6
|
+
# gem install parallel
|
|
7
|
+
# ruby -I lib examples/vs_parallel.rb
|
|
8
|
+
#
|
|
9
|
+
# Note the models differ: Parallel.map is a data-parallel map over a
|
|
10
|
+
# ready-made collection (fork/IPC per job set); Ractor::Pipeline is an
|
|
11
|
+
# in-process streaming topology. This compares only the overlap.
|
|
12
|
+
|
|
13
|
+
Warning[:experimental] = false
|
|
14
|
+
|
|
15
|
+
require "ractor/pipeline"
|
|
16
|
+
require "json"
|
|
17
|
+
begin
|
|
18
|
+
require "parallel"
|
|
19
|
+
rescue LoadError
|
|
20
|
+
abort "parallel gem not installed (gem install parallel)"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
include Ractor::Pipeline
|
|
24
|
+
|
|
25
|
+
def fib(n) = n < 2 ? n : fib(n - 1) + fib(n - 2)
|
|
26
|
+
def busy(n) = (i = 0; i += 1 while i < n; i)
|
|
27
|
+
|
|
28
|
+
def best(times = 3)
|
|
29
|
+
times.times.map { t = Time.now; yield; Time.now - t }.min
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def row(label, base, dt)
|
|
33
|
+
printf " %-32s %8.3fs (x%.2f)\n", label, dt, base / dt
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
puts RUBY_DESCRIPTION
|
|
37
|
+
puts "parallel #{Parallel::VERSION}"
|
|
38
|
+
|
|
39
|
+
section = ->(title){ puts; puts "== #{title}" }
|
|
40
|
+
|
|
41
|
+
# -- 1. uniform CPU: 32 x fib(28) ----------------------------------------
|
|
42
|
+
section.call "uniform CPU: 32 x fib(28)"
|
|
43
|
+
items = (1..32).to_a
|
|
44
|
+
expected = items.sum{ fib(28) }
|
|
45
|
+
base = best{ raise unless items.sum{ fib(28) } == expected }
|
|
46
|
+
printf " %-32s %8.3fs\n", "serial", base
|
|
47
|
+
[8, 16].each do |n|
|
|
48
|
+
dt = best{ raise unless stream(items).pipe(lanes: n){ fib(28) }.reduce(0){ |a, v| a + v } == expected }
|
|
49
|
+
row "Ractor::Pipeline lanes: #{n}", base, dt
|
|
50
|
+
end
|
|
51
|
+
[8, 16].each do |n|
|
|
52
|
+
dt = best{ raise unless Parallel.map(items, in_processes: n){ fib(28) }.sum == expected }
|
|
53
|
+
row "Parallel in_processes: #{n}", base, dt
|
|
54
|
+
end
|
|
55
|
+
dt = best{ raise unless Parallel.map(items, in_threads: 8){ fib(28) }.sum == expected }
|
|
56
|
+
row "Parallel in_threads: 8 (GVL)", base, dt
|
|
57
|
+
|
|
58
|
+
# -- 2. skewed load: 1 heavy + 31 light ----------------------------------
|
|
59
|
+
section.call "skewed load: 1 heavy (~0.3s) + 31 light (~30ms)"
|
|
60
|
+
jobs = [40_000_000] + [4_000_000] * 31
|
|
61
|
+
base = best{ jobs.each{ busy(it) } }
|
|
62
|
+
printf " %-32s %8.3fs\n", "serial", base
|
|
63
|
+
dt = best{ stream(jobs).pipe(lanes: 4){ busy(it) }.each{} }
|
|
64
|
+
row "Ractor::Pipeline lanes: 4", base, dt
|
|
65
|
+
dt = best{ Parallel.each(jobs, in_processes: 4){ busy(it) } }
|
|
66
|
+
row "Parallel in_processes: 4", base, dt
|
|
67
|
+
|
|
68
|
+
# -- 3. JSONL aggregation (allocation-bound) ------------------------------
|
|
69
|
+
section.call "JSONL aggregation: 400k lines, 1000-line chunks"
|
|
70
|
+
rng = Random.new(42)
|
|
71
|
+
paths = ["/", "/api/users", "/api/items", "/api/search", "/login", "/assets/app.js"]
|
|
72
|
+
lines = Array.new(400_000) do
|
|
73
|
+
%({"path":"#{paths[rng.rand(paths.size)]}","status":#{rng.rand(100) < 3 ? 500 : 200},"ms":#{(rng.rand * 300).round(1)}})
|
|
74
|
+
end
|
|
75
|
+
chunks = lines.each_slice(1000).to_a
|
|
76
|
+
|
|
77
|
+
def agg(lines)
|
|
78
|
+
st = {}
|
|
79
|
+
lines.each do |l|
|
|
80
|
+
r = JSON.parse(l)
|
|
81
|
+
s = (st[r["path"]] ||= [0, 0, 0.0])
|
|
82
|
+
s[0] += 1
|
|
83
|
+
s[1] += 1 if r["status"] >= 500
|
|
84
|
+
s[2] += r["ms"]
|
|
85
|
+
end
|
|
86
|
+
st
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def merge_all(stats_list)
|
|
90
|
+
stats_list.each_with_object({}) do |st, a|
|
|
91
|
+
st.each{ |k, (q, e, m)| x = (a[k] ||= [0, 0, 0.0]); x[0] += q; x[1] += e; x[2] += m }
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
base = best{ merge_all(chunks.map{ agg(it) }) }
|
|
96
|
+
printf " %-32s %8.3fs\n", "serial", base
|
|
97
|
+
dt = best do
|
|
98
|
+
stream(chunks).
|
|
99
|
+
pipe(lanes: 8){ agg(it) }.
|
|
100
|
+
reduce({}){ |a, st| st.each{ |k, (q, e, m)| x = (a[k] ||= [0, 0, 0.0]); x[0] += q; x[1] += e; x[2] += m }; a }
|
|
101
|
+
end
|
|
102
|
+
row "Ractor::Pipeline lanes: 8", base, dt
|
|
103
|
+
dt = best{ merge_all(Parallel.map(chunks, in_processes: 8){ agg(it) }) }
|
|
104
|
+
row "Parallel in_processes: 8", base, dt
|
|
105
|
+
dt = best{ merge_all(Parallel.map(chunks, in_threads: 8){ agg(it) }) }
|
|
106
|
+
row "Parallel in_threads: 8 (GVL)", base, dt
|
|
107
|
+
|
|
108
|
+
# -- 4. per-job overhead: tiny jobs ---------------------------------------
|
|
109
|
+
section.call "per-job overhead: 10k trivial jobs (it + 1)"
|
|
110
|
+
tiny = (1..10_000).to_a
|
|
111
|
+
base = best{ tiny.sum{ it + 1 } }
|
|
112
|
+
printf " %-32s %8.3fs\n", "serial", base
|
|
113
|
+
dt = best{ raise unless stream(tiny, batch: 500).pipe(lanes: 4){ it + 1 }.count == 10_000 }
|
|
114
|
+
row "Ractor::Pipeline lanes:4 batch:500", base, dt
|
|
115
|
+
dt = best{ raise unless Parallel.map(tiny, in_processes: 4){ it + 1 }.size == 10_000 }
|
|
116
|
+
row "Parallel in_processes: 4", base, dt
|