Repository navigation
Erratic optimization of some complex code that may never actually panic #142691
Description
Activity
- addedneeds-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triagingThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
on Jun 18, 2025 I believe you are asking this question in search of a general understanding of the principles at play, instead of asking why this specific example turns out poorly.
And the answer is that closures do more work, notionally, so they run up against the problem that the number of inlining shall be three.
Reacted by Arnaud de BossoreilleThe inlining threshold for
opt-level=zis so small that even some functions that look like trivial wrappers don't inline (because they are just barely nontrivial enough to LLVM).Inlining is very fundamental to compiler optimizations because there are only very limited interprocedural optimizations. Most interprocedural reasoning is done by inlining first.
So without looking at the
--emit=llvm-irI'm pretty sure you are dancing around theopt-level=zthreshold for an important call.opt-level=sis known to be smaller on many cases.std::mem::transmute::<Vec, Vec>(ManuallyDrop::into_inner(manually_drop))
Note that this is unsound,
Vec<T>does not guarantee its field order and can choose a different one for eachT.Tbeingrepr(transparent)aroundUdoes not prevent that. I'd expect this to blow up when built with-Zbuild-stdand-Zrandomize-layout(though it might not, randomization has limitations).I guess the optimization bug might be worse, then, as we could be optimizing it into
ret.I looked at Vec's fields and T only feeds into
PhantomData, we're not randomizing that at the moment. So for now this shouldn't trigger UB, even with randomization. It's only unsound because we don't guarantee that.Yes, I am basically trying to determine patterns I could avoid, knowing they would not be as optimized as I would like. Like "avoid closures when you can, even when they are aimed at deduplicating code". But maybe even this doesn't make full sense.
the number of inlining shall be three
I love this video and "I've actually been trying to make the optimizer optimize this code for the past 4 years. And it doesn't".
I was a bit afraid of this answer though: I take if you are a compiler guru then you know why and you can spend many years failing at optimizing the code, and if you're not (hello 👋) then you can ask, investigate, and in the best case you can become a compiler guru on the long term (back to first case 😄).
we could be optimizing it into ret.
Yeah, I was not even expecting that much optimization, but a dozen of instructions is already not that bad. People should be more aware that compilers can do wonders.
Don't focus too much on the
opt-levelvalue, I also saw differences with the default release optimization level (see real use case). I understand everything good comes from proper inlining in the first place (I had that feeling too).I fully understand the potential unsoundness. Out of curiosity, where did you find the information "we're not randomizing that at the moment"? I'm really interested is seeing the corresponding code.
Also, naive question... imagine the fields order could be subject to adjustments by the compiler, what would be the chance that a
Vec<T>and aVec<U>,TandUhaving same size and alignment, would have their fields in a different order (apart from the case where they are genuinely random)?Out of curiosity, where did you find the information "we're not randomizing that at the moment"?
I wrote the relevant changes 😅, #133088. Basically phantomdata has no fields and we currently primarily feed fields into the randomization seeds, and only a small amount of type information and I think the
Tof a phantomdata currently shouldn't contribute to any of that.Also, naive question... imagine the fields order could be subject to adjustments by the compiler, what would be the chance that a Vec and a Vec, T and U having same size and alignment, would have their fields in a different order
Currently there's no reason to since the niches of T and U aren't relevant, only the alignment niches of the pointer might be. But something like #139719 might add some limited randomization in the future without requiring opt-in. Though even then that'd still require feeding it more type information.
Yes, I am basically trying to determine patterns I could avoid, knowing they would not be as optimized as I would like. Like "avoid closures when you can, even when they are aimed at deduplicating code". But maybe even this doesn't make full sense.
It's not a very sharp-edged answer. I would say it's more
- closures that do no capturing should optimize identically to simple functions, but due to some nuances in the compiler, they actually don't. you shouldn't be able to notice this often, as we try to "decay" closures into functions when we can.
- closures that do any capturing are fundamentally different animals from functions. this means they will have different optimization properties than something that passes state as arguments.
- this often works out in the favor of functions that pass state as arguments, but not always, so if you are having trouble with a capturing closure that is not optimizing despite seeming "easy", then try to turn it into a non-capturing closure or a simple function that passes state as arguments.
- I have definitely noticed at least one case where a closure, whether capturing or non-capturing, simply optimizes better than a function despite (or rather because of) all of the above caveats. 🤷♀
OK, thanks all of you for your insights. I think I shall close this issue now.
P.S. if there is any way to make my code less potentially unsound, please reach out, I'm interested 😄.
- addedC-discussionCategory: Discussion or questions that doesn't represent real issues.Category: Discussion or questions that doesn't represent real issues.and removedC-bugCategory: This is a bug.Category: This is a bug.needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triagingThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
on Jul 3, 2025
Hi, I am working with code similar to this:
This code is complex but if you analyze it deeply it basically takes a vector and returns it "as is" due to the specific closure passed to
try_convert_vec_in_place. This is intended.And I am trying to ensure the generated assembly is optimal in that case. For that I am using
cargo-show-asm.If I run
RUSTFLAGS="-C opt-level=z" cargo asm should_convert_vec_with_noopI see a lot of panic handling code remaining in the assembly.Now I transform the
clean_on_errorclosure into a function with arguments like this:And I call it like this:
clean_on_error::<T, U>(slice, first_moved, first_ttt);where needed.If I rerun cargo asm, this time I get an extremely optimized version:
Question is, why, sometimes and for unclear reasons, does it not detect that the code can never enter the error branches and not detect it can be highly optimized? And are there cases the compiler should/could detect so that the optimization is less dependent on small details of implementation?
In my real project I tried with a closure taking all data as arguments, it worked like the function, but as soon as one variable is referenced by the closure the code becomes sub-optimal.
Also in my real project I did not need
opt-level=z, the default release optimization was enough, it's when I wanted to reduce the size of the test case that I needed it.For reference, my real case is at arnodb/truc@024330f but the commit might not exist any more in the future. It is basically the same as the above but embedded in some test code where the NOOP is even more complex.
Meta
rustc --version --verbose:Same behaviour with
betaandnightlyat the time of writing.