0

I have a crate that generate code with macros using quote and proc_macro2::TokenStream. When the size of the generated code increase a little, the compilation time explodes exponentially, and the disk usage of my SSD stays at 100% for a few minutes.

What advice should I follow to keep the compilation time reasonable?

For example, when the code generates one "element", it takes 15 seconds, but for 5 elements, it takes 15 MINUTES, 60 times more.

During most of the 15 minutes, it only uses the CPU (a lot). But at the end, for a few minutes, the disk usage is 100%.

What could explain the unreasonable disk usage by rustc when the code produce by the macro grows?

It is reproducible on two different computers, one with Windows 11, the other Ubuntu 22. Both with an SSD, and largely enough ram (so it can't be a swap problem).

I've set-up two branches to reproduce: macro1 and macro2 in https://github.com/tdelmas/floats

The diff between the two: https://github.com/tdelmas/floats/compare/macro1...macro2

/!\ DO NOT open the branch macro2 on vscode, between the background build that takes 15 minutes (with 100% disk usage on the end) and rust-analyzer, it will be hard to work.

To reproduce:

(all those steps were done on a terminal, with vscode - and rust-analyzer - closed)

The slow macro is only called in the code that generate test, so we can pre-build the app (and the dependencies) with cargo build first and then build the app with tests (that needs that macro to be built) with cargo test --no-run (anyway the tests are fast, less than a second)

# initialisation
git checkout https://github.com/tdelmas/floats
cd floats

Then we can prepare the build for macro1 with:

git switch macro1
# to remove any previous build
cargo clean
# dowload and build deps, the build without the impacted feature
cargo build
# build with the problematic macro
cargo test --no-run

That last command took 14 seconds on my Windows and 15 on my other computer with Linux (Ubuntu). (slow, but reasonable)

To compare with the other branch macro2:

git switch macro2
# to remove any previous build
cargo clean
# dowload and build deps, the build without the impacted feature
cargo build
# build with the problematic macro
cargo test --no-run

And that last command, that build with the macro that is problematic, with took 15 MINUTES on my Windows and 8 minutes on my Linux.

The code between the two branches should not change in complexity, only in size. If we assume that most of the time is spent in the code where the diff is, then the build should take at most 5 times more, not 60.

Or did I make a catastrophic mistake in my code?

I use in this version:

output.extend(quote! {
  #add
  #sub
  #mul
  #div
  #rem
});

Where #add, #sub, ... are generated before with (simplifying): let add = quote! { ... }, but the previous version had one call to output.extend per var, and was as slow (I changed it to limit the number of call to output.extend, but that had apparently no impact.).

I can probably avoid the problem by generating less code with macros and adding methods on my objects to run more code at runtime is I don't understand why the build time explode, but I would really prefer not to and understand what's happening here.

More context:

The problematic macro is called here: https://github.com/tdelmas/floats/blob/macro1/typed_floats/src/lib.rs#L106

The call to #add and 4 others is in a double loop (of 12 elements each, from get_specifications). They all generate a similar code by calling test_op_self_rhs (which calls test_op_checks). None of those function contains any loop. They just concatenate quotes!.

These two loops generate all combinations of NonNaN,NonNaNFinite, NonZeroNonNaN, NonZeroNonNaNFinite, Positive,PositiveFinite, StrictlyPositive, StrictlyPositiveFinite, Negative,NegativeFinite, StrictlyNegative, StrictlyNegativeFinite to generate the tests that:

  • verify that the output types of the operations (like +) is not too strict
  • verify that the output type is as strict as possible

The functions XXX_result like add_result take two times as parameters, and the list of all types, and determine the resulting type if we add two number of those types. (if the result of that function is None it means that it may be NaN thus none of those types fit and f64 must be the return type for that operation)

(Cf https://crates.io/crates/typed_floats)

Crosspost: https://users.rust-lang.org/t/rust-macro-extremely-slow-and-with-high-disk-usage/96281

Tom
  • 4,666
  • 2
  • 29
  • 48
  • did you confuse SO with githib issue tracker ? – Stargateur Jul 01 '23 at 10:05
  • You claim "large enough RAM" but did you actually verify that the compiler isn't using all of it? – Thomas Jul 01 '23 at 10:19
  • 1
    @Stargateur no, I didn't. No need to be this rude. I'm the author of that (typed_floats) crate, I'm trying to improve it, and I have a performance problem when writing rust macros, and I'm asking guidance for that. Is that performance problem a bug? If not, what should I do to improve the code and avoid that performance problem. If it's a bug, how can I get around it and check if it is a known or if I should report it. – Tom Jul 01 '23 at 10:19
  • @Thomas Yes I did chech: during the build, around 8gb of ram was used, of the 16 and 32 my computer have, so more than half was free. – Tom Jul 01 '23 at 10:22
  • Is the extra running time caused by the macro itself, or by compiling the generated code? Did you try to inspect the _output_ of the macro? Maybe it's doing something you didn't intend. – Thomas Jul 01 '23 at 10:24
  • @Thomas good questions, but I don't know yet how to check that, will try. – Tom Jul 01 '23 at 10:26
  • SO question can't require to read a whole project to answer, if you can't put all code nessesary into the question it's not a good SO question, it's a Q&A not a forum, I think you will have way more help on user rust forum or reddit rust. "No need to be this rude." damm NO FUN ALLOWED here as always. your post could clearly be a github issue seriously. Just look, what is the question of this question ? "Rust macro extremely slow (increase expenentially) and with high disk usage" is not a question... and again it's a Q&A site.... I didn't down vote cause this is quality but it's not a question. – Stargateur Jul 01 '23 at 10:31
  • @Stargateur I thought my question was clear and at the very top : "What advice should I follow to keep the compilation time reasonable?". You're right, it may be an issue for github, but where? My project definitively, but that's unhelpful. the crate quote? proc-macro2? the rust project? Also, I'm learning rust, so I'm asking here for advice how to solve the compilation time. Is it a problem with my way of using those crates? Is it a global problem with macros? Is it an obvious bug in my code? How can I investigate it? – Tom Jul 01 '23 at 10:38
  • I think the post you did on user forum is where I would start. Then I think you could create an issue on quote since that "less" official than proc-macro2 anyway that the same creator that wrote the two dtolnay. But I don't know if dtlonay will help you since your problem is complex it would take a lot of time. honestly proc macro and quote are kind of very advanced even for me as a rust veteran. the number of people who can help you is very limited. – Stargateur Jul 01 '23 at 10:44
  • @Stargateur Thank you. I didn't know if my problem was complex or not, thus my question. As my code generate simple boilerplate, I assumed I had missed something obvious and used the proc-macro2 API wrong, or that the way I use it was suboptimal. Also, I cross-posted here because I anticipate to offer a bounty if my problem needed it. – Tom Jul 01 '23 at 10:54
  • @Thomas thank you for your pointers, with more digging in that direction, I have been able to pinpoint the culprit: passing from 29k lignes of code to 57k (without macros) multiply by 30 the compilation time by 30 and generate a 13GB file. I will generate less code... – Tom Jul 01 '23 at 12:12

1 Answers1

1

To generate the code created by the macros, the command cargo +nightly rustc --profile=check -- -Zunpretty=expanded can be used. (the nightly toolchain must be installed.

Saving that output in a file allows, with very few modifications, to have the same code without using macros.

Regarding https://github.com/tdelmas/floats/compare/macro1...macro2 :

When compiling a project containing the first code, cargo build takes ~15 seconds. With the second code, it takes ~15 minutes.

The problem doesn't come from the usage of macros, but from the compilation time of the generated code.

Also, with the second code, you can notice the compiler generated a file target/debug/incremental/***/query-cache.bin of 13 GB which most probably explains the high disk usage at the end of the compilation.

So until the compilation improve, the solution is to generate less code. (the problem still persists with cargo 1.72.0-nightly (49b6d9e17 2023-06-09))

Also, turning off incremental compilation fix the high disk usage and reduce a little the compilation time: https://doc.rust-lang.org/cargo/reference/profiles.html#incremental

Tom
  • 4,666
  • 2
  • 29
  • 48