{"id":3213,"date":"2019-10-11T10:01:52","date_gmt":"2019-10-10T23:01:52","guid":{"rendered":"http:\/\/blog.mozilla.org\/nnethercote\/?p=3213"},"modified":"2019-10-11T10:01:52","modified_gmt":"2019-10-10T23:01:52","slug":"how-to-speed-up-the-rust-compiler-some-more-in-2019","status":"publish","type":"post","link":"https:\/\/blog.mozilla.org\/nnethercote\/2019\/10\/11\/how-to-speed-up-the-rust-compiler-some-more-in-2019\/","title":{"rendered":"How to speed up the Rust compiler some more in 2019"},"content":{"rendered":"<p>In July I wrote about <a href=\"https:\/\/blog.mozilla.org\/nnethercote\/2019\/07\/17\/how-to-speed-up-the-rust-compiler-in-2019\/\">my efforts to speed up the Rust compiler in 2019<\/a>. I also described how <a href=\"https:\/\/blog.mozilla.org\/nnethercote\/2019\/07\/25\/the-rust-compiler-is-still-getting-faster\/\">the Rust compiler has gotten faster in 2019<\/a>, with compile time reductions of 20-50% on most benchmarks. Now that Q3 is finished it&#8217;s a good time to see how things have changed since then.<\/p>\n<h3>Speed improvements in Q3 2019<\/h3>\n<p>The following image shows changes in time taken to compile many of the <a href=\"https:\/\/github.com\/rust-lang-nursery\/rustc-perf\/blob\/master\/collector\/benchmarks\/README.md\">standard benchmarks<\/a> used on the <a href=\"https:\/\/perf.rust-lang.org\/\">Rust performance tracker<\/a>. It compares a revision of the the compiler from 2019-07-23 with a revision of the compiler from 2019-10-09.<\/p>\n<p><a href=\"http:\/\/blog.mozilla.org\/nnethercote\/2019\/10\/11\/how-to-speed-up-the-rust-compiler-some-more-in-2019\/walltime\/\" rel=\"attachment wp-att-3216\"><img decoding=\"async\" loading=\"lazy\" class=\"alignnone wp-image-3216 size-full\" src=\"https:\/\/blog.mozilla.org\/nnethercote\/files\/2019\/10\/walltime.png\" alt=\"Screenshot of Rust compiler benchmark improvements for Q3\" width=\"657\" height=\"1528\" srcset=\"https:\/\/blog.mozilla.org\/nnethercote\/files\/2019\/10\/walltime.png 657w, https:\/\/blog.mozilla.org\/nnethercote\/files\/2019\/10\/walltime-252x586.png 252w, https:\/\/blog.mozilla.org\/nnethercote\/files\/2019\/10\/walltime-600x1395.png 600w\" sizes=\"(max-width: 657px) 100vw, 657px\" \/><\/a><\/p>\n<p>These are the <a href=\"https:\/\/en.wiktionary.org\/wiki\/wall_time\">wall-time<\/a> results. There are three different build kinds measured for each one: a debug build, an optimized build, and a check build (which detects errors but doesn\u2019t generate code). For each build kind there is a mix of incremental and non-incremental runs done. The numbers for the individual runs aren\u2019t shown here but you can see them if you <a href=\"https:\/\/perf.rust-lang.org\/compare.html?start=2019-07-23&amp;end=ece4977138a8eda96c234982e482fb43f67f1bee&amp;stat=wall-time\">view the results directly on the site<\/a> and click around. (Note that the site has had some reliability issues lately. Apologies if you have difficulty with that link.) The \u201cavg\u201d column shows the average change for those runs. The \u201cmin\u201d and \u201cmax\u201d columns show the minimum and maximum changes among those same runs.<\/p>\n<p>There are a few regressions, mostly notably for the <code>ctfe-stress-2<\/code> benchmark, which is an artificial stress test of compile-time function evaluation and so isn&#8217;t too much of a concern. But there are many more improvements, including double-digit improvements for <code>clap-rs<\/code>, <code>inflate<\/code>, <code>unicode_normalization<\/code>, <code>keccak, wg-grammar<\/code>, <code>serde<\/code>, <code>deep-vector<\/code>, <code>script-servo<\/code>, and <code>style-servo<\/code>. There have been many interesting things going on.<\/p>\n<h3>memcpy<\/h3>\n<p>For a long time, profilers like Cachegrind and Callgrind have shown that 2-6% of the instructions executed by the Rust compiler occur in calls to <code>memcpy<\/code>. This <a href=\"https:\/\/github.com\/rust-lang\/rust\/issues\/64301\">seems high<\/a>! Curious about this, I modified DHAT to track calls to <code>memcpy<\/code>, much in the way it normally tracks calls to <code>malloc<\/code>.<\/p>\n<p>The results showed that most of the <code>memcpy<\/code> calls come from a relatively small number of code locations. Also, all the <code>memcpy<\/code> calls involved values that exceed 128 bytes. It turns out that <a href=\"https:\/\/github.com\/rust-lang\/rust\/pull\/64302#issuecomment-529840404\">LLVM will use inline code for copies of values that are 128 bytes or smaller<\/a>. (Inline code will generally be faster, but <code>memcpy<\/code> calls will be more compact above a certain copy size.)<\/p>\n<p>I was able to eliminate some of these <code>memcpy<\/code> calls in the following PRs.<\/p>\n<p><a href=\"https:\/\/github.com\/rust-lang\/rust\/pull\/64302\">#64302<\/a>: This PR shrank the <code>ObligationCauseCode<\/code> type from 56 bytes to 32 bytes by boxing two of its variants, speeding up many benchmarks by up to 2.6%. The benefit mostly came because the <code>PredicateObligation<\/code> type (which contains an <code>ObligationCauseCode<\/code>) shrank from 136 bytes to 112 bytes, which dropped it below the 128 byte <code>memcpy<\/code> threshold. I also tried reducing the size of <code>ObligationCauseCode<\/code> to 24 bytes by boxing two additional variants, but this had worse performance because more allocations were required.<\/p>\n<p><a href=\"https:\/\/github.com\/rust-lang\/rust\/pull\/64374\">#64374<\/a>: The compiler&#8217;s parser has this type:<\/p>\n<pre>pub type PResult&lt;'a, T&gt; = Result&lt;T, DiagnosticBuilder&lt;'a&gt;<\/pre>\n<p>It&#8217;s used as the return type for a lot of parsing functions. The <code>T<\/code> value is always small, but <code>DiagnosticBuilder<\/code> was 176 bytes, so <code>PResult<\/code> had a minimum size of\u00a0 184 bytes. And <code>DiagnosticBuilder<\/code> is only needed when there is a parsing error, so this was egregiously inefficient. This PR boxed <code>DiagnosticBuilder<\/code> so that <code>PResult<\/code> has a minimum size of 16 bytes, speeding up a number of benchmarks by up to 2.6%.<\/p>\n<p><a href=\"https:\/\/github.com\/rust-lang\/rust\/pull\/64394\">#64394<\/a>: This PR reduced the size of the <code>SubregionOrigin<\/code> type from 120 bytes to 32 bytes by boxing its largest variant, which sped up many benchmarks slightly (by less than 1%). If you are wondering why this type caused <code>memcpy<\/code> calls despite being less than 128 bytes, it&#8217;s because it is used in a <code>BTreeMap<\/code> and the tree nodes exceeded 128 bytes.<\/p>\n<h3>ObligationForest<\/h3>\n<p>One of the biggest causes of <code>memcpy<\/code> calls is within a data structure called <code>ObligationForest<\/code>, which represents a bunch of constraints (relating to type checking and trait resolution, I think) that take the form of a collection of N-ary trees. <code>ObligationForest<\/code> uses a single vector to store the tree nodes, and links between nodes are represented as numeric indices into that vector.<\/p>\n<p>Nodes are regularly removed from this vector by a function called <code>ObligationForest::compress<\/code>. This operation is challenging to implement efficiently because the vector can contain thousands of nodes and nodes are removed only a few at a time, and order must be preserved, so there is a lot of node shuffling that occurs. (The numeric indices of all remaining nodes must be updated appropriately afterwards, which further constrains things.) The shuffling requires lots of\u00a0 <code>swap<\/code> calls, and each one of those does three <code>memcpy<\/code> calls (<code>let tmp = a; a = b; b = tmp<\/code>, more or less). And each node is 176 bytes! While trying to get rid of these <code>memcpy<\/code> calls, I got very deep into <code>ObligationForest<\/code> and made the following PRs that aren&#8217;t related to the copying.<\/p>\n<p><a href=\"https:\/\/github.com\/rust-lang\/rust\/pull\/64420\">#64420<\/a>: This PR inlined a hot function, speeding up a few benchmarks by up to 2.8%. The function in question is indirectly recursive, and LLVM will normally refuse to inline such functions. But I was able to work around this by using a trick: creating two variants of the function, one marked with <code>#[inline(always)]<\/code> (for the hot call sites) and one marked with <code>#[inline(never)]<\/code> (for the cold call sites).<\/p>\n<p><a href=\"https:\/\/github.com\/rust-lang\/rust\/pull\/64500\">#64500<\/a>: This PR did a bunch of code clean-ups, some of which helped performance to the tune of up to 1.7%. The improvements came from factoring out some repeated expressions, and using iterators and <code>retain<\/code> instead of while loops in some places.<\/p>\n<p><a href=\"https:\/\/github.com\/rust-lang\/rust\/pull\/64545\">#64545<\/a>: This PR did various things, improving performance by up to 13.8%. The performance wins came from: combining a split parent\/descendants representation to avoid frequent chaining of iterators (chained iterators are inherently slower than non-chained iterators); adding a variant of the <code>shallow_resolve<\/code> function specialized for the calling pattern at a hot call site; and using explicit iteration instead of <code>Iterator::all<\/code>. (More about that last one below.)<\/p>\n<p><a href=\"https:\/\/github.com\/rust-lang\/rust\/pull\/64627\">#64627<\/a>: This PR also did various things, improving performance by up to 18.4%. The biggest improvements came from: changing some code that dealt with a vector to special-case the 0-element and 1-element cases, which dominated; and inlining an extremely hot function (using a variant of the abovementioned <code>#[inline(always)]<\/code> +\u00a0<code>#[inline(never)]<\/code> trick).<\/p>\n<p>These PRs account for most of the improvements to the following benchmarks: <code>inflate<\/code>,\u00a0<code>keccak<\/code>, <code>cranelift-codegen<\/code>, and <code>serde<\/code>. Parts of the <code>ObligationForest<\/code> code was so hot for these benchmarks (especially <code>inflate<\/code> and <code>keccak<\/code>) that it was worth micro-optimizing them to the nth degree. When I find hot code like this, there are always two approaches: (a) try to speed it up, or (b) try to avoid calling it. In this case I did (a), but I do wonder if the users of <code>ObligationForest<\/code> could be more efficient in how they use it.<\/p>\n<p>The above PRs are a nice success story, but I should also mention that I tried a ton of other micro-optimizations that didn&#8217;t work.<\/p>\n<ul>\n<li>I tried <code>drain_filter<\/code> in <code>compress<\/code>. It was slower.<\/li>\n<li>I tried several invasive changes to the data representation, all of which ended up slowing things down.<\/li>\n<li>I tried using <code>swap_and_remove<\/code> instead of <code>swap<\/code> in <code>compress<\/code>, This gave speed-ups, but changed the order that predicates are processed in, which changed the order and\/or contents of error messages produced in lots of tests. I was unable to tell if these error message changes were legitimate &#8212; some were simple, but some were not &#8212; so I abandoned all approaches that altered predicate order.<\/li>\n<li>I tried boxing <code>ObligationForest<\/code> nodes, to reduce the number of bytes copied in <code>compress<\/code>. It reduced the amount of copying, but was a net slowdown because it increased the number of allocations performed.<\/li>\n<li>I tried inlining some other functions, for no benefit.<\/li>\n<li>I used <code>unsafe<\/code> code to remove the <code>swap<\/code> calls, but the speed-up was only 1% in the best case and I wasn&#8217;t confident that my code was panic-safe, so I abandoned that effort.<\/li>\n<li>There were even a number of seemingly innocuous code clean-ups that I had to abandon because they hurt performance measurably. I think this is because the code is so hot in some benchmarks that even tiny changes can affect code generation adversely. (I generally use instruction counts rather than wall time to make these evaluations, because instruction counts have very low variance.)<\/li>\n<\/ul>\n<p>Amusingly enough, the <code>memcpy<\/code> calls in <code>compress<\/code> were what started all this, and despite the big wins, I didn&#8217;t manage to get rid of them!<\/p>\n<h3>Inlining and code bloat<\/h3>\n<p>I mentioned above that in #64545 I got an improvement by replacing a hot call to <code>Iterator::all<\/code> with explicit iteration. The reason I tried this was that I looked at the implementation of <code>Iterator::all<\/code> and saw that it was surprisingly complicated: it wrapped the given predicate in a closure that<br \/>\nreturned a <code>LoopState<\/code>, passed that closure to <code>try_for_each<\/code> which<br \/>\nwrapped the first closure in a second closure, and passed that second closure<br \/>\nto <code>try_fold<\/code> which did the actual iteration using the second<br \/>\nclosure. Phew!<\/p>\n<p>Just for kicks I tried replacing this complex implementation with the obvious, simple implementation, and got a small speed-up on <code>keccak<\/code>, which I was using for most of my performance testing. So I did the same thing for three similar <code>Iterator<\/code> methods (<code>any<\/code>, <code>find<\/code> and <code>find_map<\/code>), submitted <a href=\"https:\/\/github.com\/rust-lang\/rust\/pull\/64572\">#64572,<\/a> and did a CI perf run. The results were <a href=\"https:\/\/perf.rust-lang.org\/compare.html?start=528379121ceb5fca5382b4337be7ac064890ec8c&amp;end=5be48aab258ff8e464383502991afeacaee17b15\">surprising and extraordinary<\/a>: 1-5% reductions for many benchmarks, but double-digits for some, and 20-50% reductions for some <code>clap-rs<\/code> runs. Uh, what? Investigation showed that the reduction came from LLVM doing less work during code generation. These functions are all marked with <code>#[inline]<\/code> and so the simpler versions result in less code for LLVM to process. Sure enough, the big wins all came in <code>debug<\/code> and <code>opt<\/code> builds, with little effect on <code>check<\/code> builds.<\/p>\n<p>This surprised me greatly. There&#8217;s been a long-running theory that the LLVM IR produced by the Rust compiler&#8217;s front end is low quality, that LLVM takes a long time to optimize it, and more front-end optimization could speed up LLVM&#8217;s code generation. #64572 demonstrates a related, but much simpler prospect: we can speed up compilation by making commonly inlined library functions smaller. In hindsight, it makes sense that this would have an effect, but the size of the effect is nonetheless astounding to me.<\/p>\n<p>But there&#8217;s a trade-off. Sometimes a simpler, smaller function is slower. For the iterator methods there are some cases where that is true, so the library experts were unwilling to land #64572 as is. Fortunately, it was possible to obtain much of the potential compile time improvements without compromising runtime.<\/p>\n<ul>\n<li>In <a href=\"https:\/\/github.com\/rust-lang\/rust\/pull\/64600\">#64600<\/a>, scottmcm removed an aggressive specialization of <code>try_fold<\/code>\u00a0 for slices that had an unrolled loop that called the given closure four times. This got about 60% of the improvements of #64572.<\/li>\n<li>In <a href=\"https:\/\/github.com\/rust-lang\/rust\/pull\/64885\">#64885,<\/a> andjo403 simplified the four <code>Iterator<\/code> methods to call <code>try_fold<\/code> directly, removing one closure layer. This got about another 15% of the improvements of #64572.<\/li>\n<\/ul>\n<p>I had a related idea, which was to use simpler versions for debug builds and complex versions for opt builds. I tried three different ways of doing this.<\/p>\n<ul>\n<li>Use <code>if cfg!(debug_assertions)<\/code> within the method bodies.<\/li>\n<li>Have two versions of each method, one marked with <code>#[cfg(debug_assertions)]<\/code>, the other marked with <code>#[cfg(not(debug_assertions))]<\/code>.<\/li>\n<li>Mark each method with <code>#[cfg_attr(debug_assertions, inline)]<\/code> so that the methods are inlined only in optimized builds.<\/li>\n<\/ul>\n<p>None of these worked; they either had little effect or made things worse. I&#8217;m hazy on the details of how library functions get incorporated; maybe there&#8217;s another way to make this idea work.<\/p>\n<p>In a similar vein, Alex Crichton opened <a href=\"https:\/\/github.com\/rust-lang\/rust\/pull\/64846\">#64846<\/a>, which changes <code>hashbrown<\/code> (Rust&#8217;s hash table implementation) so it is less aggressive about inlining. This got some sizeable improvements on some benchmarks (up to 18% on <code>opt<\/code> builds of <code>cargo<\/code>) but also caused small regressions for a lot of other benchmarks. In this case, the balance between &#8220;slower hash tables&#8221; and &#8220;less code to compile&#8221; is delicate, and a final decision is yet to be made.<\/p>\n<p>Overall, this is an exciting new area of investigation for improving Rust compile times. We definitely want new <a href=\"https:\/\/github.com\/rust-lang\/measureme\/issues\/51\">tooling<\/a> to help identify which library functions are causing the most code bloat. Hopefully these functions can be tweaked so that compile times improve without hurting runtime performance much.<\/p>\n<h3>Miscellaneous<\/h3>\n<p>As well as all the above stuff, which all arose due to my investigations into <code>memcpy<\/code> calls, I had a few miscellaneous improvements that arose from normal profiling.<\/p>\n<p><a href=\"https:\/\/github.com\/rust-lang\/rust\/pull\/65089\">#65089<\/a>: In <a href=\"https:\/\/github.com\/rust-lang\/rust\/pull\/64673\">#64673<\/a>, simulacrum got up to 30% wins on the <code>unicode_normalization<\/code> benchmark by special-casing a type size computation that is extremely hot. (That benchmark is dominated by <a href=\"https:\/\/github.com\/rust-lang-nursery\/rustc-perf\/blob\/master\/collector\/benchmarks\/unicode_normalization\/src\/tables.rs\">large <code>match<\/code> expressions containing many integral patterns<\/a>.) Inspired by this, in this PR I made a few changes that moved the special case to a slightly earlier point that avoided even more unnecessary operations, for wins of up to 11% on that same benchmark.<\/p>\n<p><a href=\"https:\/\/github.com\/rust-lang\/rust\/pull\/64949\">#64949<\/a>:<span class=\"blob-code-inner blob-code-marker\" data-code-marker=\"-\"> The following pattern occurs in a few places in the compiler.<br \/>\n<\/span><\/p>\n<pre>let v = self.iter().map(|p| p.fold_with(folder)).collect::&lt;SmallVec&lt;[_; 8]&gt;&gt;()<\/pre>\n<p>I.e. we map some values into a <code>SmallVec<\/code>. A few of these places are very hot, and in most cases the number of elements produced is 0, 1, or 2. This PR changed those hot locations to handle one or more of the 0\/1\/2 cases directly without using iteration and <code>SmallVec::collect<\/code>, speeding up numerous benchmarks by up to 7.8%.<\/p>\n<p><a href=\"https:\/\/github.com\/rust-lang\/rust\/pull\/64801\">#64801<\/a>: This PR avoided a chained iterator in a hot location, speeding up the <code>wg-grammar<\/code> benchmark by up to 1.9%.<\/p>\n<p>Finally, in <a href=\"https:\/\/github.com\/rust-lang\/rust\/pull\/64112\">#64112<\/a> I tried making pipelinined compilation more aggressive by moving crate metadata writing before type checking and borrow checking. Unfortunately, it wasn&#8217;t much of a win, and it would slightly delay error message emission when compiling code with errors, so I abandoned the effort.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>In July I wrote about my efforts to speed up the Rust compiler in 2019. I also described how the Rust compiler has gotten faster in 2019, with compile time reductions of 20-50% on most benchmarks. Now that Q3 is finished it&#8217;s a good time to see how things have changed since then. Speed improvements [&hellip;]<\/p>\n","protected":false},"author":139,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[311,16179],"tags":[],"_links":{"self":[{"href":"https:\/\/blog.mozilla.org\/nnethercote\/wp-json\/wp\/v2\/posts\/3213"}],"collection":[{"href":"https:\/\/blog.mozilla.org\/nnethercote\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.mozilla.org\/nnethercote\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.mozilla.org\/nnethercote\/wp-json\/wp\/v2\/users\/139"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.mozilla.org\/nnethercote\/wp-json\/wp\/v2\/comments?post=3213"}],"version-history":[{"count":0,"href":"https:\/\/blog.mozilla.org\/nnethercote\/wp-json\/wp\/v2\/posts\/3213\/revisions"}],"wp:attachment":[{"href":"https:\/\/blog.mozilla.org\/nnethercote\/wp-json\/wp\/v2\/media?parent=3213"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.mozilla.org\/nnethercote\/wp-json\/wp\/v2\/categories?post=3213"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.mozilla.org\/nnethercote\/wp-json\/wp\/v2\/tags?post=3213"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}