Feature #18258
closedRactor.shareable? can be slow and mutates internal object flags.
Description
On my computer, even with a relatively small object graph,Ractor.shareable?
can be quite slow (around 1-2ms). The following example creates an object graph with ~40k objects as an example, and on my computer takes around 20ms to execute Ractor.shareable?
. Because the object cannot be marked as RB_FL_SHAREABLE
because it contains mutable state, every time we check Ractor.shareable?
it will perform the same object traversal which is the slow path.
require 'benchmark'
class Borked
def freeze
end
end
class Nested
def initialize(count, top = true)
if count > 0
@nested = count.times.map{Nested.new(count - 1, false).freeze}.freeze
end
if top
@borked = Borked.new
end
end
attr :nested
attr :borked
end
def test(n)
puts "Creating nested object of size N=#{n}"
nested = Nested.new(n).freeze
shareable = false
result = Benchmark.measure do
shareable = Ractor.shareable?(nested)
end
pp result: result, shareable: shareable
end
test(8)
I propose we change Ractor.shareable?
to only check RB_FL_SHAREABLE
which gives (1) predictable and fast performance in every case and (2) avoids mutating internal object flags when performing what looks like a read-only operation.
I respect that one way of looking at Ractor.shareable?
is as a cache for object state. But this kind of cache can lead to unpredictable performance.
As a result, something like String#freeze
would not create objects that can be shared with Ractor. However, I believe we can mitigate this by tweaking String#freeze
to also set RB_FL_SHAREABLE
if possible. I believe we should apply this to more objects. It will lead to more predictable performance for Ruby.
Since there are few real-world examples of Ractor, it's hard to find real world example of the problem. However, I believe such an issue will prevent Ractor usage as even relatively small object graphs (~1000 objects) can cause 1-2ms of latency, and this particular operation does not release the GVL either which means it stalls the entire VM.
This issue came from discussion regarding https://bugs.ruby-lang.org/issues/18035 where we are considering using RB_FL_SHAREABLE
as a flag for immutability. By fixing this issue, we make it easier to implement model for immutability because we don't need to introduce new flags and can instead reuse existing flags.