https://bugs.ruby-lang.org/https://bugs.ruby-lang.org/favicon.ico?17113305112020-10-21T22:56:07ZRuby Issue Tracking SystemRuby master - Feature #17278: On-demand sharing of constants for Ractorhttps://bugs.ruby-lang.org/issues/17278?journal_id=881012020-10-21T22:56:07Zko1 (Koichi Sasada)
<ul></ul><p>If a non-main ractor is accessing <code>H</code> with immutable operation (such as <code>.each</code>), modifying <code>H</code> by main ractor will cause thread-safety issue.</p> Ruby master - Feature #17278: On-demand sharing of constants for Ractorhttps://bugs.ruby-lang.org/issues/17278?journal_id=881032020-10-21T23:37:36ZDan0042 (Daniel DeLorme)
<ul></ul><p>ko1 (Koichi Sasada) wrote in <a href="#note-1">#note-1</a>:</p>
<blockquote>
<p>If a non-main ractor is accessing <code>H</code> with immutable operation (such as <code>.each</code>), modifying <code>H</code> by main ractor will cause thread-safety issue.</p>
</blockquote>
<p>If a non-main ractor is accessing <code>H</code>, it will cause <code>H</code> to become frozen, so modifying <code>H</code> by main ractor will be impossible.</p> Ruby master - Feature #17278: On-demand sharing of constants for Ractorhttps://bugs.ruby-lang.org/issues/17278?journal_id=881072020-10-22T01:50:09Zko1 (Koichi Sasada)
<ul></ul><p>Oh, I see. I missed the process:</p>
<pre><code>[with ractor lock]
if all objects recursively are still marked with FL_AUTOSHARE
make_shareable (state C)
</code></pre>
<p>I understand this proposal <code>make_sharable</code> lazily, until it is needed.<br>
I never consider about <code>rb_check_frozen()</code>. Good point.</p>
<p>Other points:</p>
<ul>
<li>is it acceptable runtime overhead (memory + traversing)? Not so heavy, but not 0. I heard there is a development rule that constants should be frozen. And if it is true, we only need to provide a convenient way to freeze (=~ make sharable) objects.</li>
<li>it can delay the bug detection, if mutation is not occur frequently (it violates early bug detection)</li>
<li>I'm not sure all objects calls <code>rb_check_frozen()</code> before mutating their state,,, but maybe such objects can't apply <code>make_shareable</code> so no problem... maybe.</li>
</ul> Ruby master - Feature #17278: On-demand sharing of constants for Ractorhttps://bugs.ruby-lang.org/issues/17278?journal_id=881082020-10-22T03:11:29ZDan0042 (Daniel DeLorme)
<ul></ul><p>ko1 (Koichi Sasada) wrote in <a href="#note-3">#note-3</a>:</p>
<blockquote>
<ul>
<li>is it acceptable runtime overhead (memory + traversing)? Not so heavy, but not 0.</li>
</ul>
</blockquote>
<p>The deep-freezing traversal has to occur at some point. Eagerly when the constant is assigned or lazily when it's accessed by a ractor; it's the same cost. But if it's lazy we ensure this cost is only incurred for the minimum possible number of objects, unlike the <code>shareable_constant_value</code> pragma. So there may be a little gain there. The FL_AUTOSHARE marking pass (#2) is indeed an extra cost, although unlike <code>deep_freeze</code> it doesn't need to call user-defined methods. So yeah, overhead would be not so heavy but not 0.</p>
<blockquote>
<p>I heard there is a development rule that constants should be frozen.</p>
</blockquote>
<p>It's the first time I hear that, but yes it looks like <a href="https://www.rubydoc.info/github/bbatsov/RuboCop/RuboCop/Cop/Style/MutableConstant" class="external">rubocop</a> has a rule like that. But not everyone uses rubocop. In my code I would consider that an anti-pattern; littering <code>.freeze</code> everywhere just <em>feels</em> too ugly to me.</p>
<blockquote>
<ul>
<li>it can delay the bug detection, if mutation is not occur frequently (it violates early bug detection)</li>
</ul>
</blockquote>
<p>Yes, but this is the "secondary error" effect I described above. If you detect the bug early and fix it by making the constant frozen, you still have a delayed bug waiting for you if mutation doesn't occur frequently. Unless you have the discipline to search your code for modifying operations on every constant that you make frozen.</p>
<blockquote>
<ul>
<li>I'm not sure all objects calls <code>rb_check_frozen()</code> before mutating their state</li>
</ul>
</blockquote>
<p>I can't think of a case where it's ok to mutate the state of a frozen object (apart from Queue, and even then it feels weird). That would have to be a bug right?</p>
<blockquote>
<p>I understand this proposal <code>make_sharable</code> lazily, until it is needed.</p>
</blockquote>
<p>Exactly; since you understand, would you be ok with adding this to the Developers Meeting agenda? I would like to leave this decision to you, as the Ractor developer.</p> Ruby master - Feature #17278: On-demand sharing of constants for Ractorhttps://bugs.ruby-lang.org/issues/17278?journal_id=881552020-10-25T13:21:56ZEregon (Benoit Daloze)
<ul></ul><p>A performance concern is this will make the first access to any constant need the process-global lock (or a more fine-grained lock, but then it will increase footprint).<br>
That's not negligible, especially when considering to reuse JIT'ed code, where the JIT'ed code would have to handle that special first access and have an extra check and branch for it.</p>
<p>Also, and maybe more clearly, it would require every single Ruby constant read in the main Ractor to check that FL_AUTOSHARE flag, whether or not Ractors are used.</p> Ruby master - Feature #17278: On-demand sharing of constants for Ractorhttps://bugs.ruby-lang.org/issues/17278?journal_id=881562020-10-25T13:33:27ZEregon (Benoit Daloze)
<ul></ul><p>Eregon (Benoit Daloze) wrote in <a href="#note-5">#note-5</a>:</p>
<blockquote>
<p>Also, and maybe more clearly, it would require every single Ruby constant read in the main Ractor to check that FL_AUTOSHARE flag, whether or not Ractors are used.</p>
</blockquote>
<p>Actually not this one, based on the design above, I misunderstood.</p>
<p>The first concern seems still valid, except it's on the first rb_check_frozen() and not the first access.</p>
<p>The list of all constants values until the first Ractor is created could be quite some footprint overhead, and it has to be weak of course.<br>
Maybe starting in step 3 avoids the needs for that list?</p> Ruby master - Feature #17278: On-demand sharing of constants for Ractorhttps://bugs.ruby-lang.org/issues/17278?journal_id=881712020-10-26T03:15:39ZDan0042 (Daniel DeLorme)
<ul></ul><p>Eregon (Benoit Daloze) wrote in <a href="#note-5">#note-5</a>:</p>
<blockquote>
<p>A performance concern is this will make the first <code>rb_check_frozen()</code> to any constant need the process-global lock (or a more fine-grained lock, but then it will increase footprint).<br>
That's not negligible, especially when considering to reuse JIT'ed code, where the JIT'ed code would have to handle that special first access and have an extra check and branch for it.</p>
</blockquote>
<p>I would consider this cost negligible, because in normal code one does not modify constants, therefore <code>rb_check_frozen()</code> would only be called in the <em>exceptional</em> case that a constant is modified, and additionally the ractor lock would only occur on the <em>first</em> modification of such a constant. I can't speak about the performance implications regarding JIT'ed code, however it seems no worse to me than the case of trying to modify a frozen object.</p>
<blockquote>
<p>The list of all constants values until the first Ractor is created could be quite some footprint overhead, and it has to be weak of course.</p>
</blockquote>
<p>If the footprint overhead is too big that would be a problem, but it's quite hard to gauge how big it can realistically get. Searching for constant assignment in 582 popular gems and excluding irrelevant types such as Class, Integer and Symbol, I found 7012 constants. That would cost 55kB of footprint. Including an additional 195 aws-sdk-* gems I reach 39644 (!) constant assignments. But how many constant assignments can we find in a real-world app including dependencies?</p>
<blockquote>
<p>Maybe starting in step 3 avoids the needs for that list?</p>
</blockquote>
<p>I had two reasons for introducing that list</p>
<ol>
<li>
<p>If ractors are not used, we can avoid <em>entirely</em> the cost of marking all those constants with FL_AUTOSHARE. On the other hand that would mean the list is never released. That's not so great, but as with most performance issues it's all about the cpu/memory tradeoff. Maybe just limit the list length?</p>
</li>
<li>
<p>Make it possible to modify a constant and still have it usable in ractors:</p>
<pre><code class="ruby syntaxhl" data-language="ruby"><span class="no">A</span> <span class="o">=</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span><span class="mi">2</span><span class="p">]</span> <span class="c1">#without the list, this is marked FL_AUTOSHARE</span>
<span class="no">A</span> <span class="o"><<</span> <span class="mi">3</span> <span class="c1">#and so this modification causes it to become non-shared</span>
<span class="no">Ractor</span><span class="p">.</span><span class="nf">new</span> <span class="k">do</span>
<span class="nb">p</span> <span class="no">A</span> <span class="c1">#so here it cannot be used</span>
<span class="k">end</span><span class="p">.</span><span class="nf">take</span>
</code></pre>
</li>
</ol> Ruby master - Feature #17278: On-demand sharing of constants for Ractorhttps://bugs.ruby-lang.org/issues/17278?journal_id=881972020-10-26T09:04:13Zmatz (Yukihiro Matsumoto)matz@ruby.or.jp
<ul><li><strong>Status</strong> changed from <i>Open</i> to <i>Feedback</i></li></ul><p>I understand the benefit of the proposal but the mechanism behind is too complex and can cause unexpected result with concurrency (e.g. race conditions). It's the code (design) smell. We need more time to investigate further.</p>
<p>Matz.</p> Ruby master - Feature #17278: On-demand sharing of constants for Ractorhttps://bugs.ruby-lang.org/issues/17278?journal_id=882062020-10-26T15:24:37ZDan0042 (Daniel DeLorme)
<ul></ul><p>The mechanism's <em>implementation</em> may be a little complex but I think <em>usage</em> is quite simple. In the vast majority of cases the developer doesn't need to think about anything; existing constants will just work with ractors. It's like GC vs manual memory management; GC is more complex but it's worth it, right?</p>
<p>As for race conditions, I think the only one possible is that if an error occurs, whether it occurs in the main ractor or the non-main ractor can be non-deterministic. I can understand that seems like a design smell, but imho it's a much worse smell if developers have to <em>opt-in</em> to Ractor compatibility by <em>manually</em> freezing their constants. That's going to make Ractor adoption much harder than it needs to be; simple gems that <em>could</em> be used in a Ractor will need to be patched simply because they use non-frozen constants.</p> Ruby master - Feature #17278: On-demand sharing of constants for Ractorhttps://bugs.ruby-lang.org/issues/17278?journal_id=882122020-10-26T16:21:53Zko1 (Koichi Sasada)
<ul></ul><p>I also +1 for Matz because of non-deterministic behavior.</p> Ruby master - Feature #17278: On-demand sharing of constants for Ractorhttps://bugs.ruby-lang.org/issues/17278?journal_id=885672020-11-17T19:04:37ZDan0042 (Daniel DeLorme)
<ul></ul><p>In <a class="issue tracker-2 status-5 priority-4 priority-default closed" title="Feature: Ractor::LVar to provide ractor-local storage (Closed)" href="https://bugs.ruby-lang.org/issues/17323#note-5">#17323#note-5</a>, ko1 mentioned there is a possibility to provide "fork" model. So I tried thinking if it could apply here.</p>
<p>We can imagine that accessing an auto-shareable constant<br>
a) from non-main ractor: is made shareable<br>
b) from main ractor: is made shareable, and then a deep-dup copy is made and set aside for use for main ractor only</p>
<p>In this case it's no longer non-deterministic, but there are other tradeoffs. Memory usage is double. Most importantly, the constant may have a diverging value in the main ractor. Let's say you have <code>COUNTERS = Hash.new(0)</code> and the counters are only incremented in the main ractor; but from the perspective of the non-main ractors the counters would always be zero. I would find this <em>very</em> unintuitive, and likely very hard to debug.</p>
<p>I think the forking model could work for class variables because it's less surprising if a <em>variable</em> has a different value in different contexts.</p>
<p>In the end I remain convinced the original model I proposed is best. To a certain extent, non-deterministic behavior is a normal part of parallelism. For example in a producer/consumer architecture, if 2 producers generate each 1M 'A's and 1M 'B's, the consumer will see them in a non-deterministic order. No one would claim that's a problem. It's the same thing for this; the <em>order</em> may be non-deterministic (iif there's a race condition) but the end result is identical: an error.</p> Ruby master - Feature #17278: On-demand sharing of constants for Ractorhttps://bugs.ruby-lang.org/issues/17278?journal_id=892502020-12-16T18:38:33Zmarcandre (Marc-Andre Lafortune)marcandre-ruby-core@marc-andre.ca
<ul><li><strong>Related to</strong> <i><a class="issue tracker-2 status-5 priority-4 priority-default closed" href="/issues/17273">Feature #17273</a>: shareable_constant_value pragma</i> added</li></ul> Ruby master - Feature #17278: On-demand sharing of constants for Ractorhttps://bugs.ruby-lang.org/issues/17278?journal_id=902232021-02-01T16:24:14ZDan0042 (Daniel DeLorme)
<ul><li><strong>Description</strong> updated (<a title="View differences" href="/journals/90223/diff?detail_id=59103">diff</a>)</li></ul><p>I realized the list of ractor-reachable objects in step 1 was unnecessary; even without, it's possible to modify a constant and still have it usable in ractors. So just for the record I'm amending the proposal. (In case it's ever reconsidered maybe?)</p>