14

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();
}
Lukas Kalbertodt
  • 79,749
  • 26
  • 255
  • 305
Matthias
  • 8,018
  • 2
  • 27
  • 53
  • 5
    "I have a function where Rust's/llvm's optimization fails and leads to a panic" can you show it to us? – E_net4 May 23 '17 at 14:43
  • @E_net4: I've edited the question. However, I would be interested in the original question, even there would be a clever way to avoid problems with this specific function. – Matthias May 23 '17 at 14:50
  • as Matthias explains in [this comment](https://stackoverflow.com/questions/44137890/can-i-force-rust-to-not-optimize-a-single-function/44162451#comment75940210_44162451), the question is part of implementing an unsupported calling convention. – Jules Kerssemakers Jan 30 '18 at 14:37

2 Answers2

9

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!().

  • 1
    While containing useful information, this does not answer the question and would probably be better off as a comment on the original question. – Shepmaster May 24 '17 at 15:45
  • I would, but my measly 41 rep prevented me from doing so.. thanks for the feedback though! – Jules Kerssemakers May 25 '17 at 16:16
  • To clarify, I believe the poster may be suffering from an [XY-problem](http://xyproblem.info/). He is mixing 'normal' rust ( `let mut nr: u32 = 0;` ) with raw assembler (`asm!`) and something that looks like a function call (`swi_service_routine(nr);`). This mixing seems to be invalid in a `#[naked]` function, as my links explain. The debug-version _accidentally_ works, and the release optimisation exposes the incorrect usage of `#[naked]` In that case, the problem is not "how to not-optimise a single function", but "how to write this function _correctly_" – Jules Kerssemakers Jun 09 '17 at 12:20
  • 1
    @JulesKerssemakers: You are right, `#[naked]` functions are highly platform-specific and unsafe. However, you are wrong that a single `asm!` would help. As you can try for yourself, the optimiser even touches this assembler code. Thous, controlling the optimiser would help at least _a little_. The best would be if I could avoid naked functions at all, but unfortunately, Rust does not provide the needed calling conventions (yet?). So it is only a half XY-problem, but I checked already for the X-part (calling conventions). :-) – Matthias Jun 10 '17 at 10:43
  • Wow, I didn't realise that even raw asm is optimised.. This seems like it is getting into really experimental corners of rust. Have you tried asking on users.rustlang.org yet? Specifically [this thread](https://users.rust-lang.org/t/rust-for-embedded-development-where-we-are-and-whats-missing/10861?u=juleskers) deals with low-level stuff for microcontrollers, which is probably the most likely corner of rust where people know these things. – Jules Kerssemakers Jun 11 '17 at 11:48
-5

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

cargo optimize