Introduction: Why Rust Performance Is Not Magic, But Discipline
Rust is deservedly considered one of the fastest systems programming languages. However, as with any other language, "raw" code does not always perform at its maximum potential. Many beginner developers, switching to Rust from Python or JavaScript, expect an instant speed boost but forget that optimization is a process, not a gift from the compiler.
This article is intended for those who have already written their first programs in Rust and want to understand how to get the most out of them. We will cover three key areas: profiling (to know what to fix), zero-cost abstractions (to not pay for what you don't use), and memory management (to avoid bottlenecks). All examples will be accompanied by code that you can run immediately in your environment.
1. Profiling: Measure Before You Optimize
The main rule of optimization: don't optimize blindly. Without profiling, you risk spending hours "speeding up" code that is already fast, while the real bottleneck remains unnoticed.
Profiling Tools
The Rust ecosystem has several excellent tools. To start, I recommend cargo-flamegraph — it visualizes where the program spends the most time. Installation:
cargo install flamegraphRunning profiling for your binary:
cargo flamegraph --bin my_appAfter execution, you will get an SVG file with a "flame graph". Wide "bars" show functions that consume the most CPU.
Example: Finding a Bottleneck
Suppose we have a function that sums the squares of numbers:
fn sum_of_squares(n: u64) -> u64 { let mut sum = 0; for i in 1..=n { sum += i * i; } sum}If the profiler shows that 90% of the time is spent on this function, we know where to look. If the bottleneck is I/O (e.g., reading a file), optimizing computations won't help.
2. Zero-Cost Abstractions: How the Compiler Helps (and Hinders)
One of Rust's main features is the concept of zero-cost abstractions. This means that high-level constructs (iterators, closures, traits) should not add overhead compared to manually writing loops. However, this only works with proper usage.
Iterators vs. Loops
Many beginners write code like this:
let v: Vec<i32> = (0..1000).collect();let mut result = Vec::new();for i in &v { result.push(i * 2);}This code can be rewritten using iterators, and the Rust compiler will optimize it to the same machine code:
let v: Vec<i32> = (0..1000).collect();let result: Vec<i32> = v.iter().map(|x| x * 2).collect();Moreover, if you use filter, map, and fold sequentially, the compiler can "fuse" them into a single pass — this is called loop fusion. Manual loops do not enable this optimization.
Box<dyn Trait> vs. Generics
Another common mistake is overusing trait objects (Box<dyn SomeTrait>). They cause dynamic dispatch, which adds overhead. If the number of variants is known at compile time, use generics (static dispatch):
// Bad: dynamic dispatchfn process(items: Vec<Box<dyn Processor>>) { for item in items { item.run(); }}
// Good: static dispatchfn process<T: Processor>(items: Vec<T>) { for item in items { item.run()