Skip to content

Commit 39c0c9d

Browse files
Add Ractor microbenchmarks and harness
These can be run as follows: ruby -Iharness-ractor/harness.rb benchmarks-ractor/json_parse_float/benchmark.rb Co-Authored-By: Luke Gruber <[email protected]>
1 parent b429d1a commit 39c0c9d

File tree

9 files changed

+254
-0
lines changed

9 files changed

+254
-0
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
require_relative "../../harness/loader"
2+
3+
run_benchmark(5) do |num_rs, ractor_args|
4+
output = File.open("/dev/null", "wb")
5+
input = File.open("/dev/zero", "rb")
6+
100_000.times do
7+
output.write(input.read(10))
8+
end
9+
end
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
source "https://rubygems.org"
2+
gem "json", "2.13.2"
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
GEM
2+
remote: https://rubygems.org/
3+
specs:
4+
json (2.13.2)
5+
6+
PLATFORMS
7+
arm64-darwin-23
8+
ruby
9+
10+
DEPENDENCIES
11+
json (= 2.13.2)
12+
13+
BUNDLED WITH
14+
2.7.0
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
require_relative "../../harness/loader"
2+
3+
Dir.chdir(__dir__)
4+
use_gemfile
5+
require "json"
6+
puts "json v#{JSON::VERSION}"
7+
8+
ELEMENTS = 100_000
9+
list = ELEMENTS.times.map do
10+
{
11+
rand => rand,
12+
rand => rand,
13+
rand => rand,
14+
rand => rand,
15+
rand => rand,
16+
rand => rand,
17+
rand => rand,
18+
rand => rand,
19+
rand => rand,
20+
rand => rand,
21+
}.to_json
22+
end
23+
Ractor.make_shareable(list)
24+
25+
# Work is divided between ractors
26+
run_benchmark(5, ractor_args: [list]) do |num_rs, list|
27+
# num_rs: 1,list: 100_000
28+
# num_rs: 2 list: 50_000
29+
# num_rs: 4 list: 25_000
30+
if num_rs.zero?
31+
num = list.size
32+
else
33+
num = list.size / num_rs
34+
end
35+
list.each_with_index do |json, idx|
36+
break if idx >= num
37+
JSON.parse(json)
38+
end
39+
end
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
source "https://rubygems.org"
2+
gem "json", "2.13.2"
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
GEM
2+
remote: https://rubygems.org/
3+
specs:
4+
json (2.13.2)
5+
6+
PLATFORMS
7+
arm64-darwin-23
8+
ruby
9+
10+
DEPENDENCIES
11+
json (= 2.13.2)
12+
13+
BUNDLED WITH
14+
2.7.0
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
require_relative "../../harness/loader"
2+
3+
Dir.chdir(__dir__)
4+
use_gemfile
5+
require "json"
6+
puts "json v#{JSON::VERSION}"
7+
8+
ELEMENTS = 300_000
9+
list = ELEMENTS.times.map do |i|
10+
{
11+
"string #{i}" => "value #{i}",
12+
"string #{i}" => "value #{i}",
13+
"string #{i}" => "value #{i}",
14+
"string #{i}" => "value #{i}",
15+
"string #{i}" => "value #{i}",
16+
"string #{i}" => "value #{i}",
17+
"string #{i}" => "value #{i}",
18+
"string #{i}" => "value #{i}",
19+
"string #{i}" => "value #{i}",
20+
"string #{i}" => "value #{i}",
21+
"string #{i}" => "value #{i}",
22+
"string #{i}" => "value #{i}",
23+
"string #{i}" => "value #{i}",
24+
"string #{i}" => "value #{i}",
25+
"string #{i}" => "value #{i}",
26+
"string #{i}" => "value #{i}",
27+
"string #{i}" => "value #{i}",
28+
"string #{i}" => "value #{i}",
29+
"string #{i}" => "value #{i}",
30+
"string #{i}" => "value #{i}",
31+
}.to_json
32+
end
33+
Ractor.make_shareable(list)
34+
35+
# Work is divided between ractors
36+
run_benchmark(5, ractor_args: [list]) do |num_rs, list|
37+
# num_rs: 1,list: 100_000
38+
# num_rs: 2 list: 50_000
39+
# num_rs: 4 list: 25_000
40+
if num_rs.zero?
41+
num = list.size
42+
else
43+
num = list.size / num_rs
44+
end
45+
list.each_with_index do |json, idx|
46+
break if idx >= num
47+
JSON.parse(json)
48+
end
49+
end

benchmarks.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,3 +160,14 @@ throw:
160160
desc: microbenchmark for the throw instruction and stack unwinding.
161161
category: micro
162162
single_file: true
163+
#
164+
# Ractor-only benchmarks
165+
#
166+
ractor/gvl_release_acquire:
167+
desc: microbenchmark designed to test how fast the gvl can be acquired and released between ractors.
168+
ractor/json_parse_float:
169+
desc: test the performance of parsing multiple lists of json floats with ractors.
170+
ractor/json_parse_string:
171+
desc: test the performance of parsing multiple lists of strings with ractors.
172+
ractor/optcarrot:
173+
desc: The NES emulator optcarrot, refactored to run inside multiple ractors.

harness-ractor/harness.rb

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# frozen_string_literal: true
2+
require_relative '../harness/harness-common'
3+
4+
Warning[:experimental] = false
5+
ENV["YJIT_BENCH_RACTOR_HARNESS"] = "1"
6+
7+
default_ractors = [
8+
0, # without ractor
9+
1, 2, 4, 6, 8#, 12, 16, 32
10+
]
11+
if rs = ENV["YJIT_BENCH_RACTORS"]
12+
rs = rs.split(",").map(&:to_i) # If you want to include 0, you have to specify
13+
rs = rs.sort.uniq
14+
if rs.any?
15+
ractors = rs
16+
end
17+
end
18+
RACTORS = (ractors || default_ractors).freeze
19+
20+
unless Ractor.method_defined?(:join)
21+
class Ractor
22+
def join
23+
take
24+
self
25+
end
26+
alias value take
27+
end
28+
end
29+
30+
def use_ractor_gemfile(filename)
31+
filename = File.expand_path("Gemfile_#{filename}.rb", "benchmarks/ractor/gemfiles")
32+
raise "Gemfile #{filename} doesn't exist" unless File.exist?(filename)
33+
use_inline_gemfile do
34+
gem "fiddle" # for maxrss
35+
instance_eval File.read(filename), filename, 1
36+
end
37+
end
38+
39+
MAX_ITERS = Integer(ENV.fetch("MAX_BENCH_ITRS", 5))
40+
41+
def run_benchmark(num_itrs_hint, ractor_args: [], &block)
42+
warmup_itrs = Integer(ENV.fetch('WARMUP_ITRS', 5))
43+
bench_itrs = Integer(ENV.fetch('MIN_BENCH_ITRS', num_itrs_hint))
44+
if bench_itrs > MAX_ITERS
45+
bench_itrs = MAX_ITERS
46+
end
47+
# { num_ractors => [itr_in_ms, ...] }
48+
stats = Hash.new { |h,k| h[k] = [] }
49+
50+
header = "r: itr: time"
51+
puts header
52+
53+
i = 0
54+
while i < warmup_itrs
55+
args = if ractor_args.empty?
56+
[]
57+
else
58+
ractor_deep_dup(ractor_args)
59+
end
60+
block.call *([0] + args)
61+
i += 1
62+
end
63+
64+
blk = Ractor.make_shareable(block)
65+
RACTORS.each do |rs|
66+
num_itrs = 0
67+
while num_itrs < bench_itrs
68+
before = Process.clock_gettime(Process::CLOCK_MONOTONIC)
69+
if rs.zero?
70+
block.call *([rs] + ractor_deep_dup(ractor_args))
71+
else
72+
rs_list = []
73+
rs.times do
74+
rs_list << Ractor.new(*([rs] + ractor_args), &block) # ractor_args are copied
75+
end
76+
while rs_list.any?
77+
r, _obj = Ractor.select(*rs_list)
78+
rs_list.delete(r)
79+
end
80+
end
81+
num_itrs += 1
82+
time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - before
83+
time_ms = (1000 * time).to_i
84+
itr_str = "%-3s %4s %6s" % ["#{rs}", "##{num_itrs}:", "#{time_ms}ms"]
85+
stats[rs] << time_ms
86+
puts itr_str
87+
end
88+
end
89+
return_results([], stats.values.flatten)
90+
end
91+
92+
# NOTE: we use `ractor_deep_dup` instead of `Ractor.make_shareable(copy: true)` for the case of
93+
# sending args to the block without a ractor because the arguments passed to `run_benchmark` are
94+
# sometimes modified, and we want to allow that because it improves compatibility. We don't want
95+
# it to be deeply frozen.
96+
def ractor_deep_dup(args)
97+
if Array === args
98+
ret = []
99+
args.each do |el|
100+
ret << ractor_deep_dup(el)
101+
end
102+
ret
103+
elsif Hash === args
104+
ret = {}
105+
args.each do |k,v|
106+
ret[ractor_deep_dup(k)] = ractor_deep_dup(v)
107+
end
108+
ret
109+
else
110+
args.dup
111+
end
112+
end
113+
114+
Ractor.make_shareable(self) # until we get Ractor.shareable_proc

0 commit comments

Comments
 (0)