In my post about compile times earlier this year, I wondered if we might get some benefit from having a more lazy compiler. At the time, I had a hunch that a lot of what is compiled when we build a dependency wasn’t actually used.
I did a quick test today with a project with a single dependency, which is far from being a decent sample set. Still, I wanted to put what I found here for discussion. It’s, at the very least, interesting to see.
The transformation I tried was pretty simple: take an external dependency, move it into your code as a module, and fix up the references. At this point, it’ll build in the same amount of time as it did before. Makes sense. It’s the same amount of code, after all.
Next, I noticed that I had over 40 unused function/struct messages at this point.
Eg:
warning: struct is never used: `WavIntoSamples`
--> src/main.rs:915:1
|
915 | / pub struct WavIntoSamples<R, S> {
916 | | reader: WavReader<R>,
917 | | phantom_sample: marker::PhantomData<S>,
918 | | }
| |_^
warning: method is never used: `read_wave_header`
--> src/main.rs:924:5
|
924 | / fn read_wave_header(reader: &mut R) -> Result<u32> {
925 | | // Every WAVE file starts with the four bytes 'RIFF' and a file length.
926 | | // TODO: the old approach of having a slice on the stack and reading
927 | | // into it is more cumbersome, but also avoids a heap allocation. Is
... |
941 | | Ok(file_len)
942 | | }
| |_____^
My theory in the original post was that we should be lazy when we compile dependencies, meaning we shouldn’t be pulling in functions or structs if we didn’t need to. While this is important for responsiveness in IDEs, I thought it might also improve compile times.
I used these warnings to help me find dead code, which I removed. Here are some stats:
Before:
- Debug: 1.0 secs
- Release: 1.19 secs
- Lines of code (incl. comments): 2335
After:
- Debug: 0.69 secs
- Release: 0.94 secs
- Lines of code (incl. comments): 595
Note that my application is still the same. I just removed all the code I didn’t need to build it.
In the end, my app only needed about a quarter to a third of the code in the dependency. A quick look at why points to a few areas. All test functions are pretty trivially ignored. There was some functionality the test cases needed that could be safely removed as well once the test functions were removed. Also, my crate only needed some of the exported functionality. In particular, the crate I was depending on, Hound, works with a variety of .wav files, but I only needed one kind. All the code that supports the other formats could be safely ignored. Same for reading .wav files. Since my code was focused on writing instead of reading, the reading functionality wasn’t necessary.
I recognise this is an apples to oranges comparison. Today, the Rust compiler is thorough to check all dependent crates for any issues. My question is: does it need to? If you only use less a third of your dependency’s code, do you really need to pay for the compilation time of the other 2/3rds? Maybe there are tricks we can use in the compiler to get some of that time back.
Not sure how much this translates for other projects and their use of dependencies, but I think it’s worth a look.