14 ms·
How to speed up the Rust compiler in 2020
- ChrisSD 7y ago> ...giving incremental compilation speed-ups of up to 13% across many benchmarks. It also added a lot more comments to explain what is going on in that code, and removed multiple uses of unsafe. I wanted to emphasize that last point because there's sometimes a misconception that using `unsafe` always means speeding up the code. In truth you can have speed and safety. Where unsafe is needed is when it's necessary to break from Rust's memory model or to otherwise write code that Rust cannot reason about. I mention this because I've noticed even people familiar with Rust who sometimes imply `safe == slow` and `unsafe == fast`. At the extreme end you get some new users who think merely adding an `unsafe` block around code will provide a speed boost. It will not. In fact `unsafe` doesn't do anything itself. It simply allows calling some functions and performing a few other operations that would otherwise be forbidden. Sometimes this is indeed necessary but often it isn't.
- petr_tik 7y agoIt's great you emphasise it and I would generalise it even further. Any perf optimisation should be considered after reliably measuring (controlling for hot/cold cache, overall system load among others) its effect, instead of blindly adding unsafe everywhere. Similar to people who sprinkle their Cpp code with inline thinking it will force the compiler to inline. In fact, sometimes you can improve perf by adding an additional safe operation to your code! http://troubles.md/rust-optimization/#assert-conditions-beforehand http://troubles.md/rust-optimization/#assert-conditions-befo... The key takeaway from all perf optimisation articles to me is the methodology used for realiably verifying the effect of suggested changes. It's awesome to see the author include failed experiments.
- dirtydroog 7y agoIn modern C++, inline is used to prevent 'multiple-definitons' errors, not to actually inline the function.
- rmdashrfstar 7y agoThat’s terrifying. I will choose to avoid C++ projects if given the chance.
- coldpie 7y agoIt is shocking how often people suggest dubious code changes to "improve performance" with exactly zero benchmarks, or solely artificial benchmarks. Computers and compilers are far more complicated than your naive code analysis implies, you _must_ prove any performance changes with robust, real-world benchmarks, especially if your changes make the code more unsafe or harder to understand.
- hyperman1 7y agoWhen I was younger, I implemented the 8 queens problem on my 386, and micro optimized the hell out of it. Assembler, cycle counting, disable interrupts, etc... Didn't switch to protected mode, but it came close. Then I found a smarter algorithm, and my qbasic proof of concept was magnitudes faster. Ouch! This kid learned something about the nature of optimization that day. Now Im an old fart but even older farts than me stumble into this.
- acdha 7y agoThat’s a great lesson to have early in your career, too. My favorite example of that was a memory-bound app where a key function had been implemented in both C and x86 assembly. Someone tested and found that the assembler version had been slight performance loss since (IIRC) after the Pentium Pro, and since nobody had wanted to substantially change the assembly code going C-only meant they actually refactored the data access patterns and saw a substantial improvement.
- petr_tik 7y agoNot to excuse people, who provide no metrics or benchmarks for their "performance optimisations", I want to acknowledge that benchmarking is hard. I hope something like NixOS will enable people to write and publish "reproducible" (in quotes because precise numbers are impossible) benchmarks by enabling you to setup your OS, user space and compilation flags exactly like in the benchmark.
- OJFord 7y agoOh that would be really compelling. I only tried NixOS briefly a while ago, but I think I recall you can switch (actually I think literally `switch`) environment like that pretty easily and back again? So your benchmark environment could actually be pretty minimal, or if you were sharing a bug reproduction it could be devoid of unrelated packages/config in your usual environment.
- bjoli 7y agoOn a similar note: some people seem to think immutable is slower than mutable. I have seen this many times, especially in my language of choice (scheme). A lot of people coming from python or c write loops using (while ...) and mutation, which in most schemes (and in many dynamic languages) is a nice way to get boxing penalties. Instead, a recursive function is almost always preferred. I have since since long preferred recursion. Which (not strictly relevant) leads me to my biggest gripe (which is pretty small) with rust. I like rust. I really do. But a couple of times a week I have to use loop when tail recursion would have been a lot clearer. In debug builds , rust disables sibling recursion (a less general form of tail recursion) which means I have to either use a trampoline macro (simple and slow) or some kind of syntax rewriting macros (complex and fast). Neither seem like a good idea. Should I just accept the ugliness of loop+mutation or is there a nice alternative?
- jfkebwjsbx 7y agoRecursive functions should be avoided whenever reasonably possible, for several reasons (performance, safety, optimizer shenanigans...). Immutable may be slower or faster than mutable, depending on what one is writing and optimizer.
- bjoli 7y agoTail recursion is not slower. A properly tail recursive function can be transformed into a jump, and not mutating loop variables makes flow analysis less complex. Scheme implementations usually doesn't even bother doing type inference once you start using set!, and just implicitly boxes everything that you mutate. Of course you _can_ optimize it, but once you have a language with multi-shot continuations you are probably better off encouraging immutability.
- jfkebwjsbx 7y agoTail recursion is a trivial subset of recursion. If you are in that case, you are better off writing a loop and avoiding to rely on the optimizer. As for Scheme, that is a detail of Scheme, not general mutability vs. immutability.
- jfkebwjsbx 7y agoUnsafe does not imply fast, but safe does restrict what you can do. In other words, the optimal safe solution can be slower than the optimal unsafe one.
- wtetzner 7y agoOn the other hand, it's possible that safe code gives the compiler more guarantees, so it has more room to optimize. Whether your code is safe or unsafe can have a performance impact, but what the impact is depends on the specific situation.
- lidHanteyk 7y agoSo why is `unsafe` even in the language, then? It would seem to me that it simultaneously undermines safety guarantees, and it can't even ensure performance improvements.
- conradludgate 7y agoUnsafe can't ensure performance improvements, but you can sometimes get performance boosts using it. It should be a last resort if you have a bottle neck that is a result of memory restrictions. It's also how FFI works. If you're calling a c library, you cannot guarantee memory safety anymore since its outside of rusts domain, so it needs to be unsafe.
- majewsky 7y ago> Unsafe can't ensure performance improvements, but you can sometimes get performance boosts using it. Maybe to back this up with a concrete example, let's say I'm parsing something and I have a &[u8] (a string of bytes) which I have already verified contains only ASCII digits. I wanted to get the numerical value of that number. I don't want to write my own number parsing code, but std only has a parsing function for &str (UTF-8 strings), not for &[u8]. I could do let string = str::from_utf8(bytes); let value: u64 = string.parse()?; But then str::from_utf8 would iterate over the bytestring again to verify that it's valid UTF-8. This check is useless because I already know the string only contains ASCII digits. So in this case, I can improve performance with an unsafe block: //SAFETY: `bytes` was already proven to only contain ASCII digits let string = unsafe { str::from_utf8_unchecked(bytes) }; let value: u64 = string.parse()?; The performance gain comes not from unsafe per se, it comes from using a different function that skips the UTF-8 check. Since this function does not guarantee on its own that str's invariants are upheld, it's marked unsafe.
- dirtydroog 7y agoIsn't this a bit fragile though? It's totally fine as long as you own and understand the code. But what's to stop someone new and/or inexperienced from coming along and violating the 'already proven' claim? How will you know? Isn't there a risk of Rust code having "Don't ever change this" comments?
- k__ 7y agoI heard that C/C++ game engine devs would throw everything on the stack and do their own thing in terms of memory management. How does this approach play with Rust?
- est31 7y agoYou can do this in Rust as well, and it's in fact being done in the compiler: https://github.com/rust-lang/rust/search?p=2&q=TypedArena&unscoped_q=TypedArena https://github.com/rust-lang/rust/search?p=2&q=TypedArena&un... https://docs.rs/rustc-ap-arena/655.0.0/rustc_ap_arena/struct.TypedArena.html https://docs.rs/rustc-ap-arena/655.0.0/rustc_ap_arena/struct... In general, there is bad support in the standard for custom allocators, but there are crates that provide generic custom containers.
- jononor 7y agoNo problem at all with using the stack a lot? But it would be explicit in most cases. Much less chances of memory corruption issues than in C/C++ thanks to lifetime tracking. And much less copying due to default move semantics, and the assurances given by lifetime tracking.
- m12k 7y agoNot a Rust dev, but from what I understand, the lifetime tracking of Rust means you can safely give out references to stack allocated memory without worrying about corruption/segfaults/use-after-free errors or the like, because the compiler will catch it if that is possible, whereas in C++ you'd usually want to do defensive copying in situations like that. So in some cases, Rust actually allows you to fearlessly do things the most efficient way in a way you'd not be able to do in C++.
- MegaDeKay 7y agoReading the article made me wonder when the remove of unsafe was due to refactoring the code and when it was enabled by enhancements to the language since the code was originally written.
- remcob 7y agoAdding unsafe can even slowdown your code. The unsafe block prevents the compiler from making certain assumptions, which can prevent some optimizations from happening. 'unsafe' is like telling the compiler 'trust me on this', and the compiler (not fully understanding what is going on in there) wisely decides not mess with what you wrote.
- steveklabnik 7y agoAdding an unsafe block changes nothing about compiler assumptions. Some unsafe constructs do not have the same optimization potential as their safe counterparts.
- david_draco 7y agoI love that they include a "Failures" section. It is most interesting to learn from.
- yinyang_in 7y agoCan you share the link of Failures section, couldn't find in articles website.
- dwaltrip 7y agoIt's at the bottom of the post.
- mkj 7y ago"The size of rlibs on disk has also shrunk by roughly 15-20%." This is very welcome! The size of building a few rust projects on a laptop disk is fairly incredible sometimes. I guess cargo needs to learn clean up prior-to-latest deps directories too.
- Benmcdonald__ 7y agoA 'cargo clean' every once in a while helps
- jonny383 7y agoHow much slower is rust as opposed to C or C++ for production sized builds? Traditionally people have thrown more hardware at these kinds of problems. As someone who has yet to use rust for anything more than small hobby sized projects, I am naive about rustic in terms of speed
- PudgePacket 7y agoFrom what I've heard they're roughly comparable, some projects will swing one way, some the other. Rust has solid incremental compilation now though so you shouldn't be paying the price of a full rebuild often.
- DougBTX 7y agoI had a quick search, but finding examples where the same project has been implemented in C++ and Rust with reported compile times seems to be rare... There is a benchmark from way back in 2016 here: https://users.rust-lang.org/t/are-there-any-compilation-time-benchmarks-of-rust-vs-g-vs-clang/4895/14 https://users.rust-lang.org/t/are-there-any-compilation-time... Dev build was 2.91s for Rust vs 8.48s for C++, release build was 5.97s for Rust vs 9.79s for C++. Would need to find much more recent examples for the comparison to be worthwhile.
- slavik81 7y agoThat's a somewhat misleading comparison even for the time. All his C++ files could be compiled in parallel and recompiled incrementally, and his need for -flto to get good performance is really just due to how he structured his program. He seems to regard cpp files as the default location for functions and inlining as an optimization, but IMO it's the opposite. An empty function goes in the header, and every function starts off empty. You move it to a cpp file once it requires including a header. Moving a heavy function from a header to a cpp file is an action to optimize the compile time of your program. On the other hand, it could be worse. There are some monstrously slow template libraries out there, and he's not using any of them. His code isn't very optimized, but it's sane.
- steveklabnik 7y ago
- IshKebab 7y agoI have a question about LEB128. I use a similar format in my code, but with two changes: 1. It's big endian. 2. The "last byte" bit is inverted (so it's 00001 instead of 11110). I changed the last byte flag so that it is easy to count the number of values in an array - you just do this: let mut count = 0; for byte in bytes { count += byte >> 7; } And the reason I used big endian is because it makes decoding simpler. Encoding is more difficult but data is only ever encoded once, and is commonly decoded multiple times. let mut value = 0; for byte in bytes { value = (value << 7) | (byte & 0x7F); if value & 0x80 != 0 { return value; } } Vs the original implementation which has do to this basically: let mut value = 0; let mut shift = 0; for byte in bytes { value |= (byte & 0x7F) << shift; if value & 0x80 != 0 { return value; } } Here is the generated assembly in both cases: https://godbolt.org/z/6FbX_E https://godbolt.org/z/6FbX_E No idea if it would be faster than the new code but surely it is faster than the old code?
- KMag 7y agoYou should put all of those "has another byte" bits together, so you can look at just the first two bytes to figure out what to do, and then perform your load. In your most common case, you can perform an 8-bite MOVBE, an LZCNT/BSR, and if the number of leading 0s/1s < 8, just shift and mask to get your value. Have a look at how UTF-8 is encoded for a similar scheme. The space taken is exactly the same as LEB128, but your code is less branchy and needs fewer masks and shifts.
- IshKebab 7y ago> The space taken is exactly the same as LEB128 No because values under 128 (very common in my data) take 2 bytes instead of 1. Unless I'm misunderstanding you.
- KMag 7y agoYou are misunderstanding me. Values less than 128 take only one byte. The obvious implementation fast path is: 1. Check that the current position is at least a 8 bytes from the end of the buffer. Otherwise, go to the short buffer slow path. 2. Count the number of leading 1s (or 0s) using BSR/LZCNT. 3. If the number of leading 1s (or 0s) indicates more than 8 bytes are necessary, use the multi-word slow path. 4. Shift and mask out your value. 5. Increment your position pointer by the length of your varint. Values less than 128 will have no leading 1s (or no leading zeros, if your scheme uses leading zeroes instead). The shift value is 56 bits. Unless you have a 1-byte optimized path, your calculated mask value is (((uint64_t)-1LL) >> 57). The position pointer is incremented by one byte at the end of the decode.
- sysk 7y agoStupid question but could Rust skip monomorphization during development builds and use dynamic dispatch? I heard generics were often to blame for slow builds.
- Ygg2 7y agoIt's a tradeoff. Do you want your code to run as if it was written by hand (i.e. duplicated by hand)? Then monomorphization is best. For Rust, it makes a lot of sense to pay for some compile speed penalty to not have any performance penalty.
- crazyjncsu 7y ago> during development builds That's the key phrase in his statement. I'm not familiar with Rust, but if compilation speed is a major issue, and there are aspects of the compilation that are avoidable to trade-off for runtime performance, it seems to be a good idea to make those configurable per-build-type. Does Rust not offer this?
- steveklabnik 7y agoNot currently, but it's been discussed a few times. Nobody has tried to write a patch yet, that I'm aware of.
- deleted 7y ago[deleted]
- snippy 7y agoI've written an image processing tool in Rust, where running a large image downscaling test with a release build takes 2.0 seconds, and with a debug build it takes 18.7 seconds. So most of the time during development I compiled in release mode, because it was actually faster overall. For many applications debug builds are fast enough, but their runtime performance is already so slow, that I hope they don't get even slower.
- qweasd111 7y agolol
- vhbit 7y agowhen all of those changes will be in stable? will it be 1.44 or more likely 1.45?
- steveklabnik 7y agoDepends on the change. The earliest one looks to be in 1.41, the latest one landed two days ago, so should be 1.45.