Zero-cost abstractions in Rust

Key takeaways:

  1. Zero-cost abstractions in Rust mean that high-level language constructs do not incur more runtime cost than equivalent low-level code.

  2. Rust’s compiler (rustc) performs extensive optimizations during compile-time, transforming high-level abstractions into efficient machine code.

  3. Both high-level iterator usage and low-level loop implementation compile to similar assembly code, showcasing the efficiency of Rust’s abstractions.

  4. The assembly code indicates use of SIMD (Single Instruction, Multiple Data) instructions, loop unrolling, and alignment checks for performance enhancement.

Zero-cost abstractions in Rust imply that whatever we do in a high-level language should not be more expensive than if we wrote it in a low-level language. The abstractions provided by Rust, such as iterators, smart pointers, and pattern matching, are designed to be as efficient as possible.

Significance

In system-level programming, performance is crucial. Rust’s zero-cost abstractions allow developers to write safe and efficient code without the overhead typically associated with high-level programming constructs.

Compile-time optimizations in Rust

Rust’s compiler, rustc, plays a pivotal role in achieving zero-cost abstractions. It performs extensive optimizations during compile-time, ensuring that high-level abstractions are reduced to the most efficient machine code possible.

Example

Consider Rust’s iterators. They provide a high-level way to loop over elements of a collection. In languages without zero-cost abstractions, iterators might introduce extra overhead. However, in Rust, iterators are often compiled down to the same machine code as a simple loop, making them just as efficient.

let numbers = vec![1, 2, 3, 4, 5];
let sum: i32 = numbers.iter().sum();
Rust iterations

In this example, the use of .iter().sum() is as efficient as manually iterating over the vector and summing the numbers.

Let’s compare two implementations of the same functionality: one using high-level abstractions and the other using low-level code. This will help to demonstrate Rust’s zero-cost abstractions.

fn sum_with_iterators(numbers: &[i32]) -> i32 {
numbers.iter().sum()
}
fn sum_with_loop(numbers: &[i32]) -> i32 {
let mut sum = 0;
for &number in numbers {
sum += number;
}
sum
}
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let sum_iterators = sum_with_iterators(&numbers);
println!("Sum with iterators: {}", sum_iterators);
let sum_loop = sum_with_loop(&numbers);
println!("Sum with loop: {}", sum_loop);
}

We can now verify that both implementations compile down to similar or identical assembly code, illustrating the zero-cost nature.

Observing the assembly

To view the assembly of each function, we will run:

cargo asm rust_demo::sum_with_iterators
cargo asm rust_demo::sum_with_loop
Assembly commands

This will give us the following assembly outputs:

rust_demo::sum_with_loop:
 test    rsi, rsi
 je      .LBB7_1
 lea     rax, [rsi, -, 1]
 movabs  rdx, 4611686018427387903
 and     rdx, rax
 xor     eax, eax
 mov     rcx, rdi
 cmp     rdx, 7
 jb      .LBB7_7
 inc     rdx
 mov     r8, rdx
 and     r8, -8
 lea     rcx, [rdi, +, 4*r8]
 pxor    xmm0, xmm0
 xor     eax, eax
 pxor    xmm1, xmm1
.LBB7_5:
 movdqu  xmm2, xmmword, ptr, [rdi, +, 4*rax]
 paddd   xmm0, xmm2
 movdqu  xmm2, xmmword, ptr, [rdi, +, 4*rax, +, 16]
 paddd   xmm1, xmm2
 add     rax, 8
 cmp     r8, rax
 jne     .LBB7_5
 paddd   xmm1, xmm0
 pshufd  xmm0, xmm1, 238
 paddd   xmm0, xmm1
 pshufd  xmm1, xmm0, 85
 paddd   xmm1, xmm0
 movd    eax, xmm1
 cmp     rdx, r8
 je      .LBB7_2
.LBB7_7:
 lea     rdx, [rdi, +, 4*rsi]
.LBB7_8:
 add     eax, dword, ptr, [rcx]
 add     rcx, 4
 cmp     rcx, rdx
 jne     .LBB7_8
.LBB7_2:
 ret
.LBB7_1:
 xor     eax, eax
 ret
rust_demo::sum_with_iterators:
 test    rsi, rsi
 je      .LBB6_1
 cmp     rsi, 8
 jae     .LBB6_4
 xor     eax, eax
 xor     ecx, ecx
 jmp     .LBB6_7
.LBB6_1:
 xor     eax, eax
 ret
.LBB6_4:
 mov     rcx, rsi
 and     rcx, -8
 pxor    xmm0, xmm0
 xor     eax, eax
 pxor    xmm1, xmm1
.LBB6_5:
 movdqu  xmm2, xmmword, ptr, [rdi, +, 4*rax]
 paddd   xmm0, xmm2
 movdqu  xmm2, xmmword, ptr, [rdi, +, 4*rax, +, 16]
 paddd   xmm1, xmm2
 add     rax, 8
 cmp     rcx, rax
 jne     .LBB6_5
 paddd   xmm1, xmm0
 pshufd  xmm0, xmm1, 238
 paddd   xmm0, xmm1
 pshufd  xmm1, xmm0, 85
 paddd   xmm1, xmm0
 movd    eax, xmm1
 cmp     rcx, rsi
 je      .LBB6_8
.LBB6_7:
 add     eax, dword, ptr, [rdi, +, 4*rcx]
 inc     rcx
 cmp     rsi, rcx
 jne     .LBB6_7
.LBB6_8:
 ret

From the generated assemblies, we can make a few observations:

  • Main loop structure: Both functions utilize SIMD instructions for vectorized addition of array elements (paddd for parallel addition, movdqu for moving data). The functions loop over the array elements in chunks (utilizing SIMD), adding them together in a vectorized manner.

  • Differences in loop handling: In sum_with_iterators, there’s a check for array size alignment with 8 (cmp rsi, 8 followed by jae .LBB6_4). This is for efficient vectorized processing. In sum_with_loop, similar logic appears with an additional computation involving rdx and rax for alignment checks.

  • Final summation: Both functions use a sequence of paddd, pshufd, and movd instructions to consolidate the sum from SIMD registers to a general-purpose register (eax). They handle any remaining elements (if the array size is not a perfect multiple for SIMD operations) after the main SIMD loop.

  • Loop unrolling: The assembly indicates loop unrolling in both functions, a common optimization technique in which multiple iterations of a loop are executed within a single loop iteration to reduce the loop overhead.

As we can see, both functions have been highly optimized by the Rust compiler and exhibit very similar assembly patterns, particularly in their use of SIMD instructions for efficient summation and handling of array data. The core difference lies in the initial setup and alignment checks, but the overall strategy for summing the array elements is remarkably similar. This demonstrates Rust’s zero-cost abstraction in action, where high-level constructs (like iterators) are compiled into low-level code that is as efficient as manually written loops.

Conclusion

Zero-cost abstractions in Rust exemplify how developers can achieve high performance without sacrificing code safety and readability. By ensuring that high-level constructs compile down to equally efficient low-level code, Rust empowers developers to write performant system-level applications. This capability, combined with the Rust compiler’s optimizations, reinforces the language’s suitability for applications where performance is crucial, allowing developers to focus on writing clear, safe, and efficient code.

Frequently asked questions

Haven’t found what you were looking for? Contact Us


Are Rust abstractions zero cost?

Yes, Rust abstractions are designed to be zero-cost, meaning they don’t incur additional runtime overhead compared to equivalent low-level code.


What is abstraction in Rust?

Abstraction in Rust refers to the practice of simplifying complex systems by exposing only the necessary details, allowing developers to use high-level constructs without managing all underlying complexities.


What is the difference between zero-cost abstractions and traditional abstractions?

Zero-cost abstractions eliminate runtime overhead and maintain performance, while traditional abstractions may introduce inefficiencies, such as dynamic dispatch or extra memory allocations.


Free Resources

Copyright ©2025 Educative, Inc. All rights reserved