Project

General

Profile

Actions

Bug #18258

open

Ractor.shareable? can be slow and mutates internal object flags.

Added by ioquatix (Samuel Williams) about 2 months ago. Updated about 2 months ago.

Status:
Open
Priority:
Normal
Target version:
-
[ruby-core:105708]

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.

Updated by Eregon (Benoit Daloze) about 2 months ago

Yeah that's an alternative design.

Currently Ractor.shareable? semantics are "is it already shareable as in conceptually or not?".
(And the flag is just a cache for the "yes" case)
And it's not "is it already shareable as marked with Ractor.make_shareable or all instances of that class are shareable, or they are leaf frozen objects".

Note that String#freeze could only set the shareable flag if the String has no ivar, otherwise it's incorrect.

I'm not against that design, but it might be compatibility issue.

Anyway, I think no program should check Ractor.shareable?, probably only Ractor internals should check that, and if not shareable then raise an exception and therefore performance before that is not that big a deal.
So maybe one solution is removing Ractor.shareable?, and also in C-API, they seem only useful for Ractor internals, and maybe for debugging (maybe they could be only defined with ruby -d?).

Updated by ioquatix (Samuel Williams) about 2 months ago

I feel from a predictability POV, it would definitely be advantageous to set RB_FL_SHAREABLE in every case it's known ahead of time to avoid unexpected overheads. Even if Ractor.shareable? is not public interface, it's still used internally in many many places...

Updated by Eregon (Benoit Daloze) about 2 months ago

I think for the semantic model it could be easier with the proposed semantics in this issue: only always-shareable objects are shareable without Ractor.make_shareable/freeze for leaf objects with no ivars/deep_freeze/Immutable.
Right now it's probably confusing for users that some objects are magically shareable? if there were enough .freeze.
OTOH, it seems impossible that shareable? returns true except if the user already froze the instance and there are no other non-frozen objects referred in it.

Cons: it would require using Ractor.make_shareable even more, instead of being able to just call enough .freeze.
Also once we have Immutable, those objects should of course be shareable, yet Ractor.make_shareable wouldn't be called on them.

Actions

Also available in: Atom PDF