DEV Community

Cover image for πŸ¦€ Rust Master Class - Chapter 26: Performance
Oludayo Adeoye
Oludayo Adeoye

Posted on

πŸ¦€ Rust Master Class - Chapter 26: Performance

πŸ¦€ Rust Master Class - Chapter 26: Performance


Marathon finish line. Didn't win, but beat my PB by 30 seconds. Those seconds came from a thousand small decisions. Performance isn't one big thing β€” it's a thousand small ones.


Measuring and improving performance in Rust involves a combination of accurate benchmarking to identify bottlenecks and the application of specific coding patterns to minimize memory allocations and maximize execution speed.

1. Measuring Performance: Benchmarking

To accurately measure code performance, the Criterion crate is the standard tool used in the Rust ecosystem. It provides detailed statistics and allows you to compare different implementations of the same logic .

  • Key Concept: Benchmarking should be done using a dedicated harness to account for CPU thermal throttling and background noise.
  • Configuration: You must add Criterion to your dev-dependencies in Cargo.toml and define a benchmark target .

Code Example (Comparing Sorting Algorithms):

// sorting_benchmark.rs
use criterion::{criterion_group, criterion_main, Criterion};
use learning_rust::{sort_algo_1, sort_algo_2};

fn sort_benchmark(c: &mut Criterion) {
    // Create a mutable variable
    let mut numbers = vec![3-10];
    // Compares the execution time of two different sorting implementations
    // Make an independent copy
    c.bench_function("sort_algo_1", |b| b.iter(|| sort_algo_1(&mut numbers.clone())));
    // Make an independent copy
    c.bench_function("sort_algo_2", |b| b.iter(|| sort_algo_2(&mut numbers.clone())));
}

criterion_group!(benches, sort_benchmark);
criterion_main!(benches);
Enter fullscreen mode Exit fullscreen mode

[Source: 172, 173]

2. Minimizing Memory Allocations

Unnecessary heap allocations are a primary source of performance degradation. Rust provides several ways to avoid them.

  • with_capacity: When creating a collection like a Vec, using with_capacity instead of new prevents the performance hit of multiple reallocations as the collection grows .
  • Avoiding Re-allocations with std::mem: Instead of cloning or creating new objects, use swap, take, or replace to move data out of a mutable location and put a default or new value in its place .

Code Example (Using swap):

use std::mem::swap;

fn main() {
    // Create a mutable variable
    let mut text = "the developer".to_string();
    // Create a mutable variable
    let mut copied_text = "developer".to_string();
    // Swaps the contents of text and copied_text without re-allocating memory
    swap(&mut text, &mut copied_text); 
}
Enter fullscreen mode Exit fullscreen mode

[Source: 12, 355]

3. Efficient Function Inputs and Coercion

To make functions more flexible and performant, use slices (&str or &[T]) rather than owned collections (&String or &Vec<T>).

  • Deref Coercion: Slices allow for deref coercion, meaning the function can accept both a slice and a reference to the owned type without extra allocation .
  • Performance Gain: This avoids unnecessary "double redirection" (e.g., a reference to a Box that contains a string) .

Code Example:

// More efficient: accepts &str (slice) and &String (via coercion)
fn vowels(word: &str) -> u8 { 
    // processing logic
    0
}

fn main() {
    // Create a new variable
    let s = String::from("Rust");
    vowels(&s);      // Works via coercion
    vowels("Rust");  // Works directly
}
Enter fullscreen mode Exit fullscreen mode

[Source: 53, 54, 100, 285]

4. Optimizing Data Structures

  • Boxing Large Enum Variants: The size of an enum is determined by its largest variant. If one variant is significantly larger than others, it "bloats" the entire enum . By wrapping the large variant in a Box, the enum only stores a pointer, significantly reducing its memory footprint on the stack .
  • Static vs. Dynamic Dispatch: Use Static Dispatch (Generics with Trait Bounds) whenever possible. The compiler generates specific code for each type used, which allows for inlining and is generally faster than Dynamic Dispatch (&dyn Trait), which relies on a vtable lookup at runtime [18-20].

Code Example (Static Dispatch):

trait Print { fn print(&self); }

// Faster: Resolved at compile time
fn static_display<T: Print>(value: T) {
    value.print();
}
Enter fullscreen mode Exit fullscreen mode

[Source: 33, 133, 283]

5. Efficient Programming Tips

  • Initialization: Use the Default trait and struct update syntax (..Default::default()) to initialize only the necessary fields of a struct efficiently .
  • Performance Lints: Utilize Rust’s performance lints to catch common sub-optimal patterns during development .
  • String Concatenation: Be mindful of ownership when concatenating strings to avoid unnecessary copies; the format! macro is often useful for combining data into a new string without taking ownership of the inputs .

πŸ“– Download the full PDF: https://drive.google.com/file/d/13Ui9FWMikWqjRVx4l3V6aYibXOn9ehHC/view?usp=sharing

Part 26 of the Rust Master Class series β€” STEM EdTech | Automation Consulting | Rust Tutoring

RustLang #Programming #LearnToCode #STEM #EdTech


πŸ“š Practice Resources

GitHub Repository: https://github.com/PacktPublishing/Rust-Programming-Master-Class-from-Beginner-to-Expert

Try it yourself: https://play.rust-lang.org/

Run the code from this chapter in the Rust playground, then clone the repo to continue your Rust journey!


Part 26 of the Rust Master Class series β€” STEM EdTech | Automation Consulting | Rust Tutoring

RustLang #Programming #LearnToCode #STEM #EdTech

Top comments (0)