← Back to context

Comment by unscaled

6 months ago

I know very little about how rustc is implemented, but watching what kind of things make make Rust compile times slower, I tend to agree with you. The borrow checker rarely seems to be the culprit here. It tends to spike up exactly on the things you've mentioned: procedural macros use, generics use (monomorphization) and release builds (optimization).

There are other legitimate criticisms you can raise at the Rust borrow checker such as cognitive load and higher cost of refactoring, but the compilation speed argument is just baseless.

Procedural macros are not really _that_ slow themselves, the issue is more that they tend to generate enormous amount of code that will then have to be compiled, and _that_'s slow.

  • The issue, most commonly noted by devs, with proc macros is that it slows down the incremental compilation times, because proc macros are recomputed each time.

    • There's no reason proc macros invocations can't themselves be cached. For the few macros that are not idempotent (an idea that is itself absurd) an attribute like `#[proc_macro_non_cacheable]` should be available

  • Also the procedural macro library itself and all of its dependencies have to be compiled. Though this only really affects initial builds, as the library can be cached on subsequent ones.

  • Proc macros themselves are slow too. If you compile them in debug mode, they run slowly. If you compile them in release mode, they run faster but take longer to compile. This is especially noticeable with behemoth macros like serde that use the complicated syn parser.

    Compiling them in release mode does have an advantage if the proc macro is used a lot in your dep tree, since the faster invocations compensate for the increased compile time. Another option is shipping pre-compiled macros like the serde maintainer tried to do at one point, but there was sufficient (justified) backlash to shipping blobs in that case that it will probably never take off.

    Here's a comparison of using serde proc macros for (De)Serialize impls vs pre-generated impls: https://github.com/Arnavion/k8s-openapi/issues/4#issuecommen... In other words the amount of code that is compiled in the end is the same; the time difference is entirely because of proc macro invocation. 5m -> 3m for debug builds, 3.5m -> 3m for release builds. It's from 2018, but the situation is largely unchanged as of today.

  • yesn't they require you to compile a binary (or multiple ones when nested) before being able to compile your binary and depending on a lot of factors that can add quite a bunch of overhead especially for non-incremental non-release builds (and probably can be fixed by adding sand-boxing for reproducibility making most of them pure cache-able functions allowing distributed caching of both their binaries and output, like theoretically, not sure if rust will ever end up there).

    And the majority of procedural macros don't produce that much code and like you said their execution isn't the biggest problem.

    E.g. the recent article about a db system ending up with 30?min compiler times and then cutting them down to 4min was a case of auto generating a whole (very enormously huge) crate (no idea if proc-macros where involved, didn't really matter there anyway).

    So yeah, kinda what you said, proc macros can and should be improved, but rarely are they the root cause.