Issue #21953 has been updated by tikkss (Tsutomu Katsube). File 1000.html added File 10000.html added File 100000.html added Eregon (Benoit Daloze) wrote in #note-2:
Did you measure the difference in memory usage? I'd expect it to be small because each Ruby::Box has pretty much a copy of all classes, i.e. there is very little memory shared between boxes.
Thanks for your reply. No, I didn't. But I took this opportunity to give it a try. I increased the number of classes with three methods to 1,000, 10,000 and 100,000 and measured the RSS for both `spawn` and `Ractor` + `Ruby::Box` (I used `spawn` for portability). To summarize, `Ractor` + `Ruby::Box` had lower RSS in every case. However, as the number of classes increased, the difference gradually narrowed. The script used for the measurements is as follows: ```rb # spawn.rb monitor_pid = spawn(Gem.ruby, "monitor.rb", Process.pid.to_s) sleep 0.1 # Wait a moment until the monitor process starts up n_workers = 4 N_CLASSES = ARGV[0] pids = n_workers.times.collect do spawn(Gem.ruby, "worker.rb", N_CLASSES) end pids.each do |pid| Process.waitpid(pid) end Process.kill("TERM", monitor_pid) ``` ```rb # monitor.rb pid = Process.pid ppid = ARGV[0] # Sum RSS (KB) for PPID and children (excludes self for accuracy). IO.popen("while ps -o pid=,ppid=,rss= | awk '$1 != #{pid} && ($1 == #{ppid} || $2 == #{ppid}) {sum += $3} END {print sum}'; do :; done") do |io| loop do puts io.gets end end ``` ```rb # worker.rb N_CLASSES = ARGV[0].to_i N_CLASSES.times do |index| Object.const_set("X" + index.to_s, Class.new do def foo end def bar end def baz end end) end sleep 5 # Wait a moment until the other workers finish their processing ``` ```rb # ractor-ruby-box.rb monitor_pid = spawn(Gem.ruby, "monitor.rb", Process.pid.to_s) sleep 0.1 # Wait a moment until the monitor process starts up n_workers = 4 N_CLASSES = ARGV[0] workers = n_workers.times.collect do Ractor.new do local_box = Ruby::Box.new local_box.eval <<~RUBY #{N_CLASSES}.times do |index| Object.const_set("X" + index.to_s, Class.new do def foo end def bar end def baz end end) end sleep 5 # Wait a moment until the other workers finish their processing RUBY end end workers.each(&:join) Process.kill("TERM", monitor_pid) ``` To run the measurements, executes the following commands for each case: ```console $ ruby -v spawn.rb ruby 4.1.0dev (2026-04-30T09:44:32Z master f037b47af9) +PRISM [x86_64-darwin24] (snip) ``` ```console $ RUBY_BOX=1 ruby ractor-ruby-box.rb ruby 4.1.0dev (2026-04-30T09:44:32Z master f037b47af9) +PRISM [x86_64-darwin24] (snip) ``` The results summarizing the maximum RSS are as follows (in CSV format): ```csv TYPE,N_CLASSES,RSS(KB) spawn,1000,72632 spawn,10000,116240 spawn,100000,623692 ractor-ruby-box,1000,21392 ractor-ruby-box,10000,63136 ractor-ruby-box,100000,526572 ``` Please refer to the attached file for a graphical summary of the results. ---------------------------------------- Feature #21953: Allow accessing unshareable objects within a Ractor-local Ruby Box https://bugs.ruby-lang.org/issues/21953#change-117149 * Author: tikkss (Tsutomu Katsube) * Status: Open ---------------------------------------- ### Status Currently, non-main ractors prohibit access to the following objects to prevent data races: * Global variables * Class variables * Unshareable class instance variables * Unshareable constants ### Proposal I would like to propose that allow reading/writing unshareable objects inside a Ruby Box created in a non-main ractor: ```ruby # lib/x.rb class X # can write unshareable objects XXX = "1" @@cvar = "1" @ivar = "1" class << self def cvar; @@cvar; end def ivar; @ivar; end end end # can read unshareable objects $LOAD_PATH # => returns $LOAD_PATH includes lib directory X.cvar # => "1" X.ivar # => "1" X::XXX # => "1" # main.rb Ractor.new do local_box = Ruby::Box.new local_box.eval <<~RUBY base_dir = File.expand_path(File.dirname(__FILE__)) lib_dir = File.join(base_dir, "lib") # can write unshareable objects $LOAD_PATH.unshift(lib_dir) RUBY local_box.require("x") x = local_box::X.new end.join ``` Ruby Box can isolate global/class variables, class/module definitions from other boxes. A Ruby Box created inside a non-main ractor cannot be accessed from other ractor. If that is the case, wouldn’t it be fine to access unshareable objects inside that box? So I would like to propose that allow reading/writing unshareable objects inside a Ruby Box created in a non-main ractor. ### Background We are working on implementing a Ractor based parallel test runner for the test-unit gem. Ractor is a great for parallel processing. However, many existing libraries still rely on class variables or class instance variables for configuration. Ideally, we should reduce the use of class variables or class instance variables. Currently, we tried fixing several non-shareable objects, but we could not resolve all of them yet. We will continue working on this issue, but we are also exploring other approaches. This is the idea begind this proposal. I think the work needed to make objects shareable when running exisiting libraries with Ractor can be reduced. ### FAQ Q: Can we create a Ruby Box inside a non-main ractor? A: Yes: ```ruby Ractor.new {Ruby::Box.new}.join ``` Q: Is a Ruby Box created in a non-main ractor truly inaccessible from other ractor? A: No. I'm not sure if it's intentional, but a Ruby Box is a shareable object. Also, it can be accessed from the main Ractor by using `Ractor#value`: ```ruby Ractor.shareable?(Ruby::Box.new) # => true ``` ```ruby Ractor.new {Ruby::Box.new}.value # => #<Ruby::Box:3,user,optional> ``` To implement this proposal, Ruby Box may need to be an unshareable object, and passing it with `Ractor#value` may need to be disallowed. ---Files-------------------------------- 1000.html (1.01 KB) 10000.html (1.02 KB) 100000.html (1.02 KB) -- https://bugs.ruby-lang.org/