Ruby Issue Tracking System: Issueshttps://bugs.ruby-lang.org/https://bugs.ruby-lang.org/favicon.ico?17113305112012-10-14T13:56:07ZRuby Issue Tracking System
Redmine Ruby master - Bug #7158 (Closed): require is slow in its bookkeeping; can make Rails startup 2.2x...https://bugs.ruby-lang.org/issues/71582012-10-14T13:56:07Zgregprice (Greg Price)price@mit.edu
<p>=begin<br>
Starting a large application in Ruby is slow. Most of the startup<br>
time is not spent in the actual work of loading files and running Ruby<br>
code, but in bookkeeping in the 'require' implementation. I've<br>
attached a patch series which makes that bookkeeping much faster.<br>
These patches speed up a large Rails application's startup by 2.2x,<br>
and a pure-'require' benchmark by 3.4x.</p>
<p>These patches fix two ways in which 'require' is slow. Both problems<br>
have been discussed before, but these patches solve the problems with<br>
less code and stricter compatibility than previous patches I've seen.</p>
<ul>
<li>
<p>Currently we iterate through $LOADED_FEATURES to see if anything<br>
matches the newly required feature. Further, each iteration<br>
iterates in turn through $LOAD_PATH. Xavier Shay spotted this<br>
problem last year and a series of patches were discussed<br>
(in Issue <a class="issue tracker-1 status-5 priority-4 priority-default closed" title="Bug: Performance bug (in require?) (Closed)" href="https://bugs.ruby-lang.org/issues/3924">#3924</a>) to add a Hash index alongside $LOADED_FEATURES,<br>
but for 1.9.3 none were merged; Masaya Tarui committed Revision r31875,<br>
which mitigated the problem. This series adds a Hash index,<br>
and keeps it up to date even if the user modifies $LOADED_FEATURES.<br>
This is worth a 40% speedup on one large Rails application,<br>
and 2.3x on a pure-'require' benchmark.</p>
</li>
<li>
<p>Currently each 'require' call runs through $LOAD_PATH and calls<br>
rb_file_expand_path() on each element. Yura Sokolov (funny_falcon)<br>
proposed caching this last December in Issue <a class="issue tracker-2 status-5 priority-4 priority-default closed" title="Feature: Cache expanded_load_path to reduce startup time (Closed)" href="https://bugs.ruby-lang.org/issues/5767">#5767</a>, but it wasn't<br>
merged. This series also caches $LOAD_PATH, and keeps the cache up<br>
to date with a different, less invasive technique. The cache takes<br>
34 lines of code, and is worth an additional 57% speedup in<br>
starting a Rails app and a 46% speedup in pure 'require'.</p>
</li>
</ul>
<p>== Staying Compatible</p>
<p>With both the $LOADED_FEATURES index and the $LOAD_PATH cache,</p>
<ul>
<li>
<p>we exactly preserve the semantics of the user modifying $LOAD_PATH<br>
or $LOADED_FEATURES;</p>
</li>
<li>
<p>both $LOAD_PATH and $LOADED_FEATURES remain ordinary Arrays, with<br>
no singleton methods;</p>
</li>
<li>
<p>we make just one semantic change: each element of $LOAD_PATH and<br>
$LOADED_FEATURES is made into a frozen string. This doesn't limit<br>
the flexibility Ruby offers to the programmer in any way; to alter<br>
an element of either array, one simply reassigns it to the new<br>
value. Further, normal path-munging code which only adds and<br>
removes elements shouldn't have to change at all.</p>
</li>
</ul>
<p>These patches use the following technique to keep the cache and the<br>
index up to date without modifying the methods of $LOADED_FEATURES or<br>
$LOAD_PATH: we take advantage of the sharing mechanism in the Array<br>
implementation to detect, in O(1) time, whether either array has been<br>
mutated. We cause $LOADED_FEATURES to be shared with an Array we keep<br>
privately in load.c; if anything modifies it, it will break the<br>
sharing and we will know to rebuild the index. Similarly for<br>
$LOAD_PATH.</p>
<p>== Benchmarks</p>
<p>First, on my company's Rails application, where $LOAD_PATH.size is 207<br>
and $LOADED_FEATURES.size is 2126. I measured the time taken by<br>
'bundle exec rails runner "p 1"'.</p>
<p>. Rails startup time,<br>
version best of 5 speedup<br>
v1_9_3_194 12.197s<br>
v1_9_3_194+index 8.688s 1.40x<br>
v1_9_3_194+index+cache 5.538s 2.20x</p>
<p>And now isolating the performance of 'require', by requiring<br>
16000 empty files.</p>
<p>version time, best of 5 speedup<br>
trunk (at r36920) 10.115s<br>
trunk+index 4.363s 2.32x<br>
trunk+index+cache 2.984s 3.39x</p>
<p>(The timings for the Rails application are based on the latest release<br>
rather than trunk because a number of gems failed to compile against<br>
trunk for me.)</p>
<p>== The Patches</p>
<p>I've attached four patches:</p>
<p>(1) Patch 1 changes no behavior at all. It adds comments and<br>
simplifies a bit of code to help in understanding why patch 3 is<br>
correct. 42 lines, most of them comments.</p>
<p>(2) Patch 2 adds a function to array.c which will help us tell when<br>
$LOAD_PATH or $LOADED_FEATURES has been modified. 17 lines.</p>
<p>(3) Patch 3 adds the $LOADED_FEATURES index. 150 lines.</p>
<p>(4) Patch 4 adds the $LOAD_PATH cache. 34 lines.</p>
<p>Reviews and comments welcome -- I'm sure there's something I could do<br>
to make these patches better. I hope we can get some form of them<br>
into trunk before the next release. My life has been happier since I<br>
switched to this version because commands in my Rails application all<br>
run faster now, and I want every Ruby programmer to be happier in the<br>
same way with 2.0 and ideally with 1.9.4.</p>
<p>=end</p>