Unused dependency code - can compiler performance be improved by doing less work?

It often happens, in real world Rust applications, to only leverage a very small subset of the API surface/functionality offered by a dependency.
This is often the case for both direct and transitive dependencies.

Based on my (very) limited understanding of cargo, I am under the impression that cargo/rustc will do the same amount of compilation work for a dependency regardless of the actual needs of the crate pulling in that dependency.

Feature flags are a common "solution" to this problem - mark functions or sub-modules in a crate as optional to reduce its compilation footprint.
Feature flags are necessarily coarse-grained and their ergonomics are often not ideal - e.g. your project fails to compile because you are using a function gated behind a feature flag that has not been enabled; the compiler is not aware of that function existence, because it's behind a disabled feature flag, and you are left puzzled looking at docs.rs trying to understand why things don't work. Beginners are particularly impacted.

I wonder - could cargo work backwards? Start from the root of the dependency tree, determine which symbols it is using/importing from its dependencies and then leverage that information to skip as much code as possible when compiling those dependencies.
Given my very limited knowledge in this area, I ask this question mostly out of curiosity/as a way to learn more about the assumptions and the design of the compiler.
I never saw this idea in many of the conversations I have been following around Rust's compilation speed (e.g. this or this) which leads me to believe it is flawed in some fundamental way - curious to learn why!

3 Likes

I cannot tell whether this will reduce compile times overall, but this idea does have some inherent problems. The main one is probably that code that isn't referenced directly still participates in type checking with traits; e.g. if we have a constraint that some type implements some trait, we need to check all impls of the trait to see if they match.

Also, unused should still trigger compile time errors so we have to typecheck it.

1 Like

I cannot tell whether this will reduce compile times overall

It'd be interesting to be able to analyze a crate to determine what % of the symbols exported by its dependencies are actually being used (either directly or transitively). That would give a theoretical upper bound on the potential gains.

The main one is probably that code that isn't referenced directly still participates in type checking with traits; e.g. if we have a constraint that some type implements some trait, we need to check all impls of the trait to see if they match.

Just to understand better: are you referring to functions using trait bounds like fn(variable: impl dep_y::TraitX) which is then invoked on a specific type and we need to determine if that type implements dep_y::TraitX?

Also, unused should still trigger compile time errors so we have to typecheck it.

What unused are you referring to here?

Yes.

Oops, typo. I meant everything: types, functions, impls, traits...

Just to understand better: are you referring to functions using trait bounds like fn(variable: impl dep_y::TraitX) which is then invoked on a specific type and we need to determine if that type implements dep_y::TraitX ?

Yes

That would only require checking the constraints on the impl TraitX block though, right? E.g.

impl TraitX for T
where
   T: SomeOtherTrait
{
// [...]
}

the actual methods and their implementations can be skipped when trying to find if there is a matching trait implementation.

Yes, however:

  1. This is not so cheap either.
  2. See my second point.

See my second point.

I assume you are referring to:

Also, unused should still trigger compile time errors so we have to typecheck it. Oops, typo. I meant everything: types, functions, impls, traits...

When compiling a binary/library, doesn't the compiler only emit unused warnings for local symbols that have not been used? How does that apply to dependencies?

If you are only talking about local unused code, that already isn't codegened anyway.

Not warnings, imagine the following code:

fn foo() -> u32 { "" }

The compiler should emit an error even if this function is not exported.

Ok, I see where you are coming from @chrefr - the underlying assumption is that the compiler has to verify that each dependency compiles as a standalone crate, including the portions that are not being leveraged by the binary we are trying to build.

If you are only talking about local unused code, that already isn't codegened anyway.

My focus/curiosity is mostly on avoiding work for remote unused code @bjorn3 - e.g. my binary is only using 3% of the exposed functionality of the regex crate, is it possible to avoid paying its full price in terms of compilation work?

Not codegening any functions until the end when it is shown they are used has been an idea toyed around (keyword: mir-only rlibs). An experimental implementation has been shown to improve from scratch compilation time in some cases, but also to regress it in other cases. To make it fast when changing code, using incremental compilation is a must have as otherwise everything would have to be codegened and optimized every time. In addition it breaks using --emit obj for compiling individual object files for each crate, which some build systems need. By the way there has been a dicussion on the rust-lang zulip about the future of incremental compilation which is probably relevant to this: rust-lang

8 Likes

Thanks for the link to the discussion - I now realise I am basically inquiring about the same strategy that Josh proposed in that Zulip topic.

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.