Inlining in Rust: Understanding the Compiler’s Role

Drashti Shah
4 min readApr 10, 2023

--

What is inlining?

  • Inlining is an optimisation technique in Rust that can improve the performance of your code.
  • Inlining replaces a function call with the function’s body, eliminating the overhead of a function call.
  • It can lead to faster code execution and cache usage improvements, but may also increase code size.

Compiler’s role in inlining

The compiler is like an expert in code optimisation, and it’s responsible for making the final decision on inlining functions. Here’s why the compiler is trusted with this task.

  • Function size: The compiler knows if a function is big or small. If a big function is inlined everywhere, it can make the overall code too large, which can slow things down. The compiler carefully checks the function size before deciding to inline it.
  • How often a function is called: The compiler can figure out if a function is called a lot (a “hot” function) or not so much (a “cold” function). Inlining hot functions can make the program run faster, but inlining cold functions might not help much.
  • Target device: The compiler also knows about the device the code will run on and its specific features, like memory size and processor type. This information helps the compiler decide if inlining a function will help the code run better on that device.
  • Optimisation level: When compiling code, you can choose different optimisation levels. A high level means the compiler will try its best to make the code run fast, while a low level might focus on keeping the code small. The compiler takes this into account when deciding whether to inline a function or not.

By letting the compiler make the final call on inlining, it can use its expertise to find the right balance between making the code run faster and avoiding potential problems, like making the code too big or increasing compile time.

Inlining attributes in Rust

  • #[inline]: Suggests that the compiler should consider inlining the function.
  • #[inline(always)]: Stronger suggestion to always inline the function, but the compiler can still reject the request.
  • #[inline(never)]: Tells the compiler not to inline the function.
// A small, frequently called function
#[inline(always)]
fn small_function() {
// ... function body
}

// A large, less frequently called function
#[inline(never)]
fn large_function() {
// ... function body
}

Guidelines for inlining

When deciding whether to inline a function or not, consider the following rules.

Small and Simple Functions

Inline: Small and simple functions, such as getter and setter methods or arithmetic operations, are great candidates for inlining. Inlining these functions can provide performance benefits without introducing significant code bloat.

Don’t inline: If the small function has a low frequency of calls, you might not gain much from inlining it. In this case, you can leave it to the compiler’s discretion.

Large and Complex Functions

Don’t inline: Generally, it’s best to avoid inlining large and complex functions, as the potential code bloat and compilation time increase can outweigh the performance benefits. The compiler might also decide not to inline it, even if you use #[inline(always)].

Hot Call Sites

Inline: If a function is called frequently at a specific call site (a “hot” call site), but not as often at other call sites, you can split the function into two versions: one that’s inlined and another that’s not. The inlined version can be called at the hot call site, while the non-inlined version can be called at the colder call sites. This way, you can benefit from inlining without increasing code bloat at all call sites.

Don’t inline: If the hot call site is not performance-critical or if inlining the function would introduce significant code bloat or compilation time, it might be better not to inline it.

Library Functions

Inline: When developing a library, you can use #[inline] to hint to the compiler that certain functions might benefit from inlining. This allows the compiler to make the final decision when the library is used in a specific application.

Don’t inline: If a library function is not performance-critical or is too large and complex, you might decide not to provide an inline hint.

Remember that these rules are guidelines, and the best approach can vary depending on your specific use case. Always profile and analyse your code’s performance to determine the most effective strategy for inlining.

Potential drawbacks

Inlining can provide significant performance improvements, but it’s not without its downsides.

  • Code bloat: Inlining can lead to an increase in the size of the binary due to duplicating the function code at each call site. This code bloat can negatively impact cache locality and increase the memory footprint of your program.
  • Compilation time: Aggressive inlining can increase the compilation time, as the compiler needs to analyse and optimise the inlined code at each call site.
  • Inlining limits: The Rust compiler has certain thresholds and heuristics to decide whether or not to inline a function. If a function is too large or complex, it may not be inlined even if marked with #[inline(always)]. This means that, sometimes, the compiler might not be able to inline a function as you intended.
  • Harder to debug: Inlined functions can make debugging more difficult, as the function’s code is now integrated into the calling code. This can lead to less clear stack traces and make it harder to identify the exact location of an issue.

Conclusion

Understanding inlining in Rust and the Rust compiler’s decision-making process is important to optimise your code effectively. Remember that the compiler is designed to make intelligent decisions about inlining, and providing appropriate hints can help the compiler make better choices for your code’s performance.

--

--

Drashti Shah

ESG & climate data scientist by day. Aspiring computational biologist and game designer by night. A Rust fan who believes in an "open career".