4 ms·
> Is there much that can be done to improve this? I have a 50k+ lines of code project which usually recompiles after a change in ~2 seconds in release mode. T
by kouteiheika 7y ago
> Is there much that can be done to improve this?
I have a 50k+ lines of code project which usually recompiles after a change in ~2 seconds in release mode.
There are many tricks which can be used to improve compile times to the point that even on medium-sized projects the compile time is not an issue. But you need to keep a certain discipline to adhere to these.
1) Use LLD instead of the system linker.
2) Don't add dependencies willy-nilly. Especially for trivial stuff which you don't need pedal-to-the-metal optimized. (e.g. do you really need to add that 4000 lines long SIMD-optimized base64 encoder/decoder, or can you live with a naive 10-lines long version you can write yourself in a few minutes?)
3) Feature-flag gate dependencies/features not necessary during development. (e.g. do you actually need HTTPS support during development, or can you test your webapp on HTTP and only compile-in the TLS stack for production deployment?)
4) Avoid heavy dependencies. (e.g. there are some popular web frameworks for Rust which have over 100+ dependencies by default; if you pick such a framework then your compile times are obviously going to be very heavily affected)
5) Use dynamic dispatch (&dyn T and Box<dyn T>) instead static dispatch (impl T) when accepting generic arguments in cases where you don't need pedal-to-the-metal performance.
6) If you absolutely need to use static dispatch purely for ergonomics (and not because of the performance) then create two function - a dynamically dispatched private one which accepts a &dyn T and contains the actual functionality, and a public one which accepts impl T and is a one-line wrapper around the private one.
7) Don't use #[inline] annotations if you don't absolutely need them.
8) Split your project into multiple crates.
- leshow 7y ago> 6) If you absolutely need to use static dispatch purely for ergonomics (and not because of the performance) then create two function - a dynamically dispatched private one which accepts a &dyn T and contains the actual functionality, and a public one which accepts impl T and is a one-line wrapper around the private one. Would you really recommend this as common advice? It seems like not a good idea to me. If all you cared about was binary size and compilation speed, maybe, but not otherwise. Same with blanketly recommending use of &dyn T instead of <T>. There are other problems with dynamic dispatch in rust, namely it's kind of a pain if you need to `+ OtherTrait` with it.
- kouteiheika 7y agoYes I would. In general people tend to overuse static dispatch even when it's not really necessary. Of course the issue is a little bit more nuanced than "always use X unless Y" and there are tradeoffs in play here that need to be balanced. For example, if your function is really small - yeah, it's probably fine to just use static dispatch. If you're writing a generic data structure - you most likely also want it to be a statically dispatched Struct<T>, but with a healthy dose of #[cold] annotated non-generic functions for the cold paths. However, let's say that you have a function that accepts a filesystem path and loads a PNG from it - you do not want the PNG loading code to be 1) duplicated in every compilation unit (compilation time bloat), and 2) monomorphised three times just because you passed an `&str` once to it, a `String` another time, and a `PathBuf` yet another time (compilation time and executable size bloat), so you definitely do want dynamic dispatch here (at least under the hood with the two function trick). I do think the default should indeed be &dyn T and you should only go for impl T when you can actually clearly substantiate why you should use it, instead of the other way around which is the default now in the Rust ecosystem. (Which is how you end up with 20+ second edit-compile cycles one of the sibling comments mentioned.)
- eximius 7y agoCould a proc macro be written that uses dynamic dispatch in debug and static dispatch in release? That would be optimal for dev compilation speed and binary speed, at the cost of binary size. It seems like a pretty good tradeoff for most cases.
- kouteiheika 7y agoIIRC I think there was a crate with a procedural macro like that which did something kinda similar to this. However currently that isn't really optimal for quite a few reasons: 1) having a procedural macro by itself pulls in extra dependencies, 2) it introduces extra work for the compiler so it does negatively impact the compile times (the procedural macros don't operate directly on the AST, so they have to parse the token stream into an AST, process it, serialize it back into a raw token stream, and the compiler has to parse it again), 3) Rust's current procedural macro machinery doesn't yet support emitting proper error messages from a procedural macro, so if you'll get an error or a warning from a piece of code generated by a procedural macro it will just point to the #[name_of_the_macro] annotation instead of the actual location where the issue originated from.
- modernerd 7y agoThank you, this is very helpful. > 1. Use LLD instead of the system linker. A note for macOS users that unfortunately LLD is not supported reliably there yet: https://github.com/rust-lang/rust/issues/39915 https://github.com/rust-lang/rust/issues/39915 https://users.rust-lang.org/t/llvm-lld-linker-fails-on-mac-os-x/35078 https://users.rust-lang.org/t/llvm-lld-linker-fails-on-mac-o... From https://lld.llvm.org/ https://lld.llvm.org/ > The linker supports ELF (Unix), PE/COFF (Windows), Mach-O (macOS) and WebAssembly in descending order of completeness.