https://redmine.ruby-lang.org/https://redmine.ruby-lang.org/favicon.ico?17113305112021-02-22T23:42:20ZRuby Issue Tracking SystemRuby master - Bug #13708: catastrophic slow compilation of defined-defined sequence has no chance to ^Chttps://redmine.ruby-lang.org/issues/13708?journal_id=905562021-02-22T23:42:20Zjeremyevans0 (Jeremy Evans)merch-redmine@jeremyevans.net
<ul><li><strong>Related to</strong> <i><a class="issue tracker-1 status-5 priority-4 priority-default closed" href="/issues/17649">Bug #17649</a>: `defined?` invokes method once for each syntactic element around it</i> added</li></ul> Ruby master - Bug #13708: catastrophic slow compilation of defined-defined sequence has no chance to ^Chttps://redmine.ruby-lang.org/issues/13708?journal_id=911462021-03-29T14:45:39Zjeremyevans (Jeremy Evans)code@jeremyevans.net
<ul><li><strong>Status</strong> changed from <i>Open</i> to <i>Closed</i></li></ul><p>Applied in changeset <a class="changeset" title="Make defined? cache the results of method calls Previously, defined? could result in many more m..." href="https://redmine.ruby-lang.org/projects/ruby-master/repository/git/revisions/7b3c5ab8a5825a2b960e639d257f0c8a69c4186c">git|7b3c5ab8a5825a2b960e639d257f0c8a69c4186c</a>.</p>
<hr>
<p>Make defined? cache the results of method calls</p>
<p>Previously, defined? could result in many more method calls than<br>
the code it was checking. <code>defined? a.b.c.d.e.f</code> generated 15 calls,<br>
with <code>a</code> called 5 times, <code>b</code> called 4 times, etc.. This was due to<br>
the fact that defined works in a recursive manner, but it previously<br>
did not cache results. So for <code>defined? a.b.c.d.e.f</code>, the logic was<br>
similar to</p>
<pre><code class="ruby syntaxhl" data-language="ruby"><span class="k">return</span> <span class="kp">nil</span> <span class="k">unless</span> <span class="k">defined?</span> <span class="n">a</span>
<span class="k">return</span> <span class="kp">nil</span> <span class="k">unless</span> <span class="k">defined?</span> <span class="n">a</span><span class="p">.</span><span class="nf">b</span>
<span class="k">return</span> <span class="kp">nil</span> <span class="k">unless</span> <span class="k">defined?</span> <span class="n">a</span><span class="p">.</span><span class="nf">b</span><span class="p">.</span><span class="nf">c</span>
<span class="k">return</span> <span class="kp">nil</span> <span class="k">unless</span> <span class="k">defined?</span> <span class="n">a</span><span class="p">.</span><span class="nf">b</span><span class="p">.</span><span class="nf">c</span><span class="p">.</span><span class="nf">d</span>
<span class="k">return</span> <span class="kp">nil</span> <span class="k">unless</span> <span class="k">defined?</span> <span class="n">a</span><span class="p">.</span><span class="nf">b</span><span class="p">.</span><span class="nf">c</span><span class="p">.</span><span class="nf">d</span><span class="p">.</span><span class="nf">e</span>
<span class="k">return</span> <span class="kp">nil</span> <span class="k">unless</span> <span class="k">defined?</span> <span class="n">a</span><span class="p">.</span><span class="nf">b</span><span class="p">.</span><span class="nf">c</span><span class="p">.</span><span class="nf">d</span><span class="p">.</span><span class="nf">e</span><span class="p">.</span><span class="nf">f</span>
<span class="s2">"method"</span>
</code></pre>
<p>With this change, the logic is similar to the following, without<br>
the creation of a local variable:</p>
<pre><code class="ruby syntaxhl" data-language="ruby"><span class="k">return</span> <span class="kp">nil</span> <span class="k">unless</span> <span class="k">defined?</span> <span class="n">a</span>
<span class="n">_</span> <span class="o">=</span> <span class="n">a</span>
<span class="k">return</span> <span class="kp">nil</span> <span class="k">unless</span> <span class="k">defined?</span> <span class="n">_</span><span class="p">.</span><span class="nf">b</span>
<span class="n">_</span> <span class="o">=</span> <span class="n">_</span><span class="p">.</span><span class="nf">b</span>
<span class="k">return</span> <span class="kp">nil</span> <span class="k">unless</span> <span class="k">defined?</span> <span class="n">_</span><span class="p">.</span><span class="nf">c</span>
<span class="n">_</span> <span class="o">=</span> <span class="n">_</span><span class="p">.</span><span class="nf">c</span>
<span class="k">return</span> <span class="kp">nil</span> <span class="k">unless</span> <span class="k">defined?</span> <span class="n">_</span><span class="p">.</span><span class="nf">d</span>
<span class="n">_</span> <span class="o">=</span> <span class="n">_</span><span class="p">.</span><span class="nf">d</span>
<span class="k">return</span> <span class="kp">nil</span> <span class="k">unless</span> <span class="k">defined?</span> <span class="n">_</span><span class="p">.</span><span class="nf">e</span>
<span class="n">_</span> <span class="o">=</span> <span class="n">_</span><span class="p">.</span><span class="nf">e</span>
<span class="k">return</span> <span class="kp">nil</span> <span class="k">unless</span> <span class="k">defined?</span> <span class="n">_</span><span class="p">.</span><span class="nf">f</span>
<span class="s2">"method"</span>
</code></pre>
<p>In addition to eliminating redundant method calls for defined<br>
statements, this greatly simplifies the instruction sequences by<br>
eliminating duplication. Previously:</p>
<pre><code>0000 putnil ( 1)[Li]
0001 putself
0002 defined func, :a, false
0006 branchunless 73
0008 putself
0009 opt_send_without_block <calldata!mid:a, argc:0, FCALL|VCALL|ARGS_SIMPLE>
0011 defined method, :b, false
0015 branchunless 73
0017 putself
0018 opt_send_without_block <calldata!mid:a, argc:0, FCALL|VCALL|ARGS_SIMPLE>
0020 opt_send_without_block <calldata!mid:b, argc:0, ARGS_SIMPLE>
0022 defined method, :c, false
0026 branchunless 73
0028 putself
0029 opt_send_without_block <calldata!mid:a, argc:0, FCALL|VCALL|ARGS_SIMPLE>
0031 opt_send_without_block <calldata!mid:b, argc:0, ARGS_SIMPLE>
0033 opt_send_without_block <calldata!mid:c, argc:0, ARGS_SIMPLE>
0035 defined method, :d, false
0039 branchunless 73
0041 putself
0042 opt_send_without_block <calldata!mid:a, argc:0, FCALL|VCALL|ARGS_SIMPLE>
0044 opt_send_without_block <calldata!mid:b, argc:0, ARGS_SIMPLE>
0046 opt_send_without_block <calldata!mid:c, argc:0, ARGS_SIMPLE>
0048 opt_send_without_block <calldata!mid:d, argc:0, ARGS_SIMPLE>
0050 defined method, :e, false
0054 branchunless 73
0056 putself
0057 opt_send_without_block <calldata!mid:a, argc:0, FCALL|VCALL|ARGS_SIMPLE>
0059 opt_send_without_block <calldata!mid:b, argc:0, ARGS_SIMPLE>
0061 opt_send_without_block <calldata!mid:c, argc:0, ARGS_SIMPLE>
0063 opt_send_without_block <calldata!mid:d, argc:0, ARGS_SIMPLE>
0065 opt_send_without_block <calldata!mid:e, argc:0, ARGS_SIMPLE>
0067 defined method, :f, true
0071 swap
0072 pop
0073 leave
</code></pre>
<p>After change:</p>
<pre><code>0000 putnil ( 1)[Li]
0001 putself
0002 dup
0003 defined func, :a, false
0007 branchunless 52
0009 opt_send_without_block <calldata!mid:a, argc:0, FCALL|VCALL|ARGS_SIMPLE>
0011 dup
0012 defined method, :b, false
0016 branchunless 52
0018 opt_send_without_block <calldata!mid:b, argc:0, ARGS_SIMPLE>
0020 dup
0021 defined method, :c, false
0025 branchunless 52
0027 opt_send_without_block <calldata!mid:c, argc:0, ARGS_SIMPLE>
0029 dup
0030 defined method, :d, false
0034 branchunless 52
0036 opt_send_without_block <calldata!mid:d, argc:0, ARGS_SIMPLE>
0038 dup
0039 defined method, :e, false
0043 branchunless 52
0045 opt_send_without_block <calldata!mid:e, argc:0, ARGS_SIMPLE>
0047 defined method, :f, true
0051 swap
0052 pop
0053 leave
</code></pre>
<p>This fixes issues where for pathological small examples, Ruby would generate<br>
huge instruction sequences.</p>
<p>Unfortunately, implementing this support is kind of a hack. This adds another<br>
parameter to compile_call for whether we should assume the receiver is already<br>
present on the stack, and has defined? set that parameter for the specific<br>
case where it is compiling a method call where the receiver is also a method<br>
call.</p>
<p>defined_expr0 also takes an additional parameter for whether it should leave<br>
the results of the method call on the stack. If that argument is true, in<br>
the case where the method isn't defined, we jump to the pop before the leave,<br>
so the extra result is not left on the stack. This requires space for an<br>
additional label, so lfinish now needs to be able to hold 3 labels.</p>
<p>Fixes [Bug <a class="issue tracker-1 status-5 priority-4 priority-default closed" title="Bug: `defined?` invokes method once for each syntactic element around it (Closed)" href="https://redmine.ruby-lang.org/issues/17649">#17649</a>]<br>
Fixes [Bug <a class="issue tracker-1 status-5 priority-4 priority-default closed" title="Bug: catastrophic slow compilation of defined-defined sequence has no chance to ^C (Closed)" href="https://redmine.ruby-lang.org/issues/13708">#13708</a>]</p>