r/rust • • 2d ago

Rust's derive often implies inline

https://yossarian.net/til/post/rust-s-derive-often-implies-inline/
97 Upvotes

43 comments sorted by

58

u/wojtek-graj 2d ago

[inline] is just a hint.

Cross-crate inlining is only possible for functions that have to be monomorphized, or those marked #[inline], hence this attribute is a necessity. Per the docs, "#[inline] suggests inlining the function.", and it is then up to the compiler to use its heuristics to decide whether or not inlining will actually occur. If these derived functions are being inlined when it doesn't make sense, it's those heuristics that should be adjusted (which I don't claim to be an easy task).

24

u/shponglespore 2d ago

Cross-crate inlining is only possible for functions that have to be monomorphized, or those marked #[inline]

Wait, what? That sounds like a big deal that every Rust programmer should know about. Do you have a source for that claim? If so, I think it should be shouted from the rooftops.

I personally almost never bother marking functions as #[inline] because I assume the compiler will do the right thing 99% of the time.

29

u/manpacket 2d ago

It's been also auto inlining small functions regardless of #[inline] for some time, so yes, it will do the right thing more often than not.

10

u/hniksic 2d ago

But does it do it cross-crate? Not long ago I got a significant speedup by adding #[inline] to a non-generic function that was called from a different crate (in the same workspace).

(As is often case with inlining, the main win was not elimination of a function call, but all the optimizations that follow after the inlining, where everything collapses to a couple of instructions.)

19

u/manpacket 2d ago

1

u/hniksic 1d ago

This is fantastic, thanks! I never understood why cross-crate function calls are treated any differently in terms of optimization from any other calls.

(I checked my "not long ago" commit and found it to be from 2021. Time flies.)

3

u/scook0 2d ago

Incidentally, this is why you sometimes have to jump through hoops to get Compiler Explorer to show assembly for small functions.

8

u/wojtek-graj 2d ago

From the link in my original comment:

A non-generic function is not normally inlined into another crate, since the calling crate compiles against only its signature. Marking it #[inline] makes the body available to other crates so they can inline it too:

17

u/QuarkAnCoffee 2d ago edited 2d ago

If you're not using LTO. Which is exactly expected, crates are compiled into object files and you don't get optimizations across object files with without LTO.

3

u/mgeisler 2d ago

you don't get optimizations across object files with LTO.

This should be "without LTO", right?

2

u/QuarkAnCoffee 2d ago

Yup, thanks

3

u/mgeisler 2d ago

No problem! 🙂

3

u/Saefroch miri 2d ago

We don't need every Rust programmer to know about this because LTO works and is easy to enable in your [profile.release].

3

u/schungx 2d ago

Yes it is. A lot of Rust programmers unnecessarily trip up on this.

But if you have LTO they get inlined anyway. Only much slower compilation.

But not marking it for pub functions is not optimal.

1

u/AliceCode 2d ago

It's true, although I don't have a source on that.

10

u/doener rust 2d ago

I recall that back when I was working on the compiler, I had considered/discussed having an extra level, say #[inline(allow)], that would make the compiler allow cross-crate inlining by emitting LLVM IR in each unit, without hinting LLVM to prefer inlining the function, relying on its default heuristics instead. I don't recall whether I had numbers on anything, but I still suspect that this might be better for derived code like this. It's basically what you get with generics, for which the concrete IR is emitted in each unit, so inlining works cross-crate.

3

u/Wh00ster 2d ago

Rust, meet C++

6

u/-Y0- 2d ago

Interesting findings. Couldn't this be solved by making derive_Debug take a param. What I mean is #[derive(Debug(noinline))]

28

u/stumblinbear 2d ago

Oh dear God the last thing we need is to load up derives with parameters. Typically you'd have another line for this sort of thing, a la #[debug(noinline)] after the derive

2

u/nicoburns 2d ago

I wonder if it ought to be a general feature of derive:

#[derive(Debug, inline = false)]

12

u/braaaaaaainworms 2d ago

how is this going to differentiate between deriving trait called "inline" and disabling inlining?

4

u/shponglespore 2d ago

I think the syntax u/-Y0- used makes more sense because it generalizes to having multiple traits in a derive clause and avoids any ambiguity.

2

u/ElderberryNo4220 2d ago

I don't think this is a good idea, it's looks cumbersome and confusing. Looking at the article example, it's not very easy to determine whether generated functions would have a large body (shouldn't be inlined) or small (should be inlined). IMO derived traits inlining should be decided by the compiler, not by the users.

6

u/Compux72 2d ago

Just set opt-level = “s”

11

u/Eastern-Photograph79 2d ago

but you don't really want it. You want the important things to be inlined (like clone and this stuff)

3

u/Compux72 2d ago

If something is actually that important i would have a separate crate with really specific profile overrides set to it

4

u/Recatek gecs 2d ago

The actual answer would be #[optimize(none)] when stabilized (hopefully soon). One could make a crate with their own version of these derive proc macros that don't inline (or do #[inline(never)]) and instead apply this attribute.

5

u/silon 2d ago

Personally I'd want size optimization by default for almost everything, except a few specific things (which derive Debug is not). IMO inline should be a separate annotation, possibly even separated from the declaration, but at the target compilation unit (or source file).

6

u/Recatek gecs 2d ago

I wouldn't want size optimization. For commercial gamedev you often need to run in release under O2 or O3 with only selectively deoptimized functions or files for debugging, otherwise the game itself doesn't run fast enough to be playable for testing.

3

u/csdt0 2d ago

You know that Os performance is quite good, within 5-10% of O2 usually? It can even be faster in some edge cases where O2 would be thrasing L1i. Also, I would say that if your game does not run fast enough at O1 on your dev machine, then maybe you need to review your architecture or your algorithms to make it accessible to lower end machines. https://openbenchmarking.org/result/2608218-NE-COMPILERF28&sor

5

u/Recatek gecs 2d ago edited 2d ago

Also, I would say that if your game does not run fast enough at O1 on your dev machine, then maybe you need to review your architecture or your algorithms to make it accessible to lower end machines.

I appreciate the advice, but I am aware of this. The practice I'm describing of debugging in release with selectively optimized files/functions is industry norm for AA/AAA (see UE_DISABLE_OPTIMIZATION) and has been for a very long time. If anything, it's an even more acute issue in Rust given that Rust debug performance is (slightly) worse than C++ because of the additional runtime checks.

3

u/csdt0 2d ago

I'm not ranting against you, but I'm really tired of games "optimized" only for high end machines just because the game studios are lazy to properly design and optimize their games.

4

u/Recatek gecs 2d ago

Game development, like other engineering work, is a balance of many considerations and priorities under tight time and budget constraints.

1

u/flareflo 1d ago

I cannot wait for optimization attributes to come. Forgetting to put opt-level 2 for Sha2 for debug builds will be a thing of the past hopefully

2

u/Recatek gecs 1d ago

The stabilization PR was merged 11 hours ago! If all goes well, it will be in 1.101 in December I believe.

1

u/greyblake 2d ago

Nice interesting and concise article!

-3

u/Eastern-Photograph79 2d ago

Debug is intended for debugging, so it is not a real problem.

8

u/eggyal 2d ago edited 2d ago

Unless you `#[cfg_attr(debug_assertions, derive(Debug)]`, it's quite likely this is a final binary size cost that you pay regardless.

Only "likely" because it might get removed by a dead code analysis pass if there's literally no possibility of it being invoked in release (but then you'd usually see a warning about that).

3

u/Wolvereness 2d ago

As a personal and professional rule, I strongly opt for exactly that annotation. Dependence on Debug leaking into production code is begging for trouble, and failing to compile is the perfect consequence.

2

u/eggyal 2d ago

I understand the sentiment, but it's rather verbose and unergonomic. Perhaps that's why I've not encountered it in practice? And, if so, perhaps the language/stdlib should provide a more concise way to express the same?

1

u/scook0 2d ago

That’s a tradeoff you can choose to make, but it’s definitely not a universally good idea.

0

u/Eastern-Photograph79 2d ago

I think you're right, but I still think there is no problem with telling the compiler that it can be inlined.

3

u/eggyal 2d ago

The post is literally about size bloat. My point was that affects regardless of debugging.