Can I force Rust to not optimize a single function?

5.3k views Asked by At

I have a function where Rust's/LLVM's optimization fails and leads to a panic (in the release version), while the unoptimized code (debug version) just works fine. If I compare the generated assembly code, I can not even grasp an idea what the optimizer tries to accomplish. (A reason might be that this very function uses inline assembler.)

Is there any way to tell Rust to leave certain functions alone during the optimisation, or do I have to switch off all optimizations?

Here is the specific function:

#[naked]
pub extern "C" fn dispatch_svc(){
    Cpu::save_context();
    let mut nr: u32 = 0;
    unsafe {
        asm!("ldr r0, [lr, #-4]
              bic $0, r0, #0xff000000":"=r"(nr)::"r0":"volatile")
    };
    swi_service_routine(nr);
    Cpu::restore_context_and_return();
}
2

There are 2 answers

2
jesusbv - user3085938 On

If you are using cargo, you can tell it not to optimize anything at all, or by levels

cargo optimize

5
Jules Kerssemakers On

No you cannot.
Rusts' compilation units (the smallest unit the compiler, and thus the optimizer) operates on, are the entire crate.

Your only workaround would be to compile this function in an individual crate, compile it, and then include it as a pre-compiled dependency. (Normal rust-dependencies are compiled at the optimisation level of the depender)

However: Specifying a different optimisation level for this single function will not solve your problem! Sure, it may work today, but can break again each time the compiler (or optimisation flags) change. Given the function names in your example (Cpu::save_context/restore_context_and_return) the underlying problem you seem to be solving requires adding a proper calling convention to rustc.

TL;DR: naked functions are deeply unsafe (My respect, you're a braver person than I am!). The only reliable way to use them is to write only one single asm!() block as the entire function body, nothing else.

Mixing asm!, normal Rust and function calls like you are doing is effectively Undefined Behaviour (in the scary C/Nasal-Demon sense of the term) No amount of optimisation-tweaking will change this.

2022-04 update: since originally answering this, a lot has happened in around naked functions.
A minimal "constrained" subset of naked functions (see RFC #2972]1) is slated for stabilisation in 1.60 There are also compiler errors to "reject unsupported naked functions", that would trigger for the examples provided here.


Naked functions are still unstable until the Rust authors "get it right". As you have discovered, there are many subtle problems with this.
Tracking issue for stabilisation here, superseded in 2022 by the more-limited tracker for "constrained" naked functions.

In the naked-fn RFC, under "Motivation", we find:

Because the compiler depends on a function prologue and epilogue to maintain storage for local variable bindings, it is generally unsafe to write anything but inline assembly inside a naked function. The LLVM language reference describes this feature as having "very system-specific consequences", which the programmer must be aware of.

(emphasis mine)

A little bit lower in the RFC, under unresolved questions, we learn that this is not just a problem for Rust. Other languages also experience problems with this feature:

.. most compilers supporting similar features either require or strongly recommend that authors write only inline assembly inside naked functions to ensure no code is generated that assumes a particular stack layout.

The reason is that all compilers make a LOT of assumptions about how functions are called (keywords: "Caller-Saved Registers", "Callee-saved registers", "Calling convention", "Red zone") . Naked functions don't obey these assumptions, and thus any code a compiler generates is highly likely to be wrong. The "solution" is to not let the compiler generate anything, i.e. write the entire function by hand in assembly.

As such, the way you are mixing 'normal' code (let mut nr: u32 = 0; ), function calls (swi_service_routine(nr);) and raw assembler in a naked function is unspecified behaviour. (Yes, such a thing exists in Rust, but only in Unstable).

Naked functions cause enough problems that they deserve their own label in the Rust bugtracker. In one of the A-naked issues, we find this comment, by knowledgeable user Tari (among others, author of llvm-sys. He explains:

The actual correctness of non-asm code in naked functions depends on the optimizer and code generator, which in general we cannot make any guarantees about what it will do.

There is also talk about requiring unsafe for naked functions, as they break many of Rust's normal assumptions. The fact that they don't require this yet in all cases is an open bug
2022 update: Closed on 2022-01-21 by the new deny-by-default lints to Reject unsupported naked functions (#93153)


So, the proper solution to your "optimisation problem" is to stop relying on optimisation at all. Instead, write only a single asm!() block.

For your Cpu::save_context() / Cpu::restore_context_and_return() pair: I can understand the desire for code-reuse. To get it, change those into a macro that inserts the relevant asm!(...). A concatenation of asm!(...); asm!(...); asm!(...); should be equivalent to a single asm!().