Introduction
In the world of Rust programming, we often need to choose between dynamic dispatch and static dispatch (achieved through generics and monomorphism). This choice is not always about performance, but rather a trade-off between code design and flexibility. This article will take you deep into the implementation principles and use cases of these two methods, and help you make more informed choices through practical benchmarking.
What are Dynamic Dispatch and Static Dispatch?
In simple terms:
- Dynamic Dispatch: Code is generated at runtime
- Static Dispatch: Code is generated at compile time
In Rust, these two methods are implemented through <span>dyn Trait</span> and generics <span><T: Trait></span>.
Basic Usage
First, let’s define a simple trait as an example:
trait Animal {
fn make_sound(&self);
}
Implementation of Dynamic Dispatch
Using the <span>dyn</span> keyword to create trait objects, which can be used in struct fields, function parameters, or return values:
// Using dynamic dispatch in a struct
struct Zoo {
// animals can be different types of animals (tiger, lion, etc.)
pub animals: Vec<Box<dyn Animal>>,
}
impl Zoo {
fn make_sounds(&self) {
for animal in self.animals.iter() {
animal.make_sound();
}
}
}
// Using dynamic dispatch in a function
fn make_sounds(animals: &[Box<dyn Animal>]) {
for animal in animals.iter() {
animal.make_sound();
}
}
Implementation of Static Dispatch (Generics)
Using generic type parameters and trait bounds:
// Using generics in a struct
struct ZooWithOneAnimalType<T: Animal> {
// All animals must be of the same type, either all tigers or all lions
pub animals: Vec<T>,
}
impl<T> ZooWithOneAnimalType<T>
where
T: Animal,
{
fn make_sounds(&self) {
for animal in self.animals.iter() {
animal.make_sound();
}
}
}
// Using generics in a function
fn make_sounds_one_animal_type<T: Animal>(animals: &Vec<T>) {
for animal in animals.iter() {
animal.make_sound();
}
}
How to Choose? Key Considerations
From the implementations above, the most important consideration is:
Do we need to handle multiple types that implement the same trait simultaneously?
-
When using generics (static dispatch), the generic parameter can only be replaced with one specific type at any given time. For example, the
<span>animals</span>array in<span>ZooWithOneAnimalType</span>must either be all tigers or all lions. -
In contrast, with dynamic dispatch, we can include multiple different
<span>Animal</span>concrete types in the same<span>animals</span>vector.
Advantages of dynamic dispatch include:
- Need to store different types of objects in a collection
- Need to dynamically add different types of objects at runtime
- Want to return any type that implements a specific trait from a function without defining the return type in advance
Performance Comparison
While dynamic dispatch is generally considered slower, in practical applications, the performance difference is often not a decisive factor. Let’s verify this through benchmarking:
First, define two animal types:
struct Tiger;
impl Animal for Tiger {
fn make_sound(&self) {
println!("I am a tiger")
}
}
struct Lion;
impl Animal for Lion {
fn make_sound(&self) {
println!("I am a lion")
}
}
Then use Criterion for benchmarking:
fn dynamic_zoo() {
// Create a zoo with 100 animals, alternating between Tiger and Lion
let animals: Vec<Box<dyn Animal>> = (1..=100)
.map(|i| {
if i % 2 == 0 {
Box::new(Tiger) as Box<dyn Animal>
} else {
Box::new(Lion) as Box<dyn Animal>
}
})
.collect();
let zoo = Zoo {
animals: animals,
};
zoo.make_sounds();
}
fn static_zoo() {
// Create a zoo with 100 lions
let lion_cafe = ZooWithOneAnimalType {
animals: (1..=100).map(|_| Lion).collect(),
};
lion_cafe.make_sounds();
}
Benchmark results (100 measurements):
dynamic time: [125.48 µs 127.90 µs 130.31 µs]
static time: [119.58 µs 120.89 µs 122.23 µs]
As we can see, while static dispatch is indeed faster, the difference is only a few microseconds. In most practical applications, this difference is negligible.
Complete Code Example
Here is a complete code example that you can try:
trait Animal {
fn make_sound(&self);
}
struct Tiger;
impl Animal for Tiger {
fn make_sound(&self) {
println!("I am a tiger")
}
}
struct Lion;
impl Animal for Lion {
fn make_sound(&self) {
println!("I am a lion")
}
}
// Dynamic dispatch - struct
struct Zoo {
// Can contain various animals (tigers, lions, etc.)
pub animals: Vec<Box<dyn Animal>>,
}
impl Zoo {
fn make_sounds(&self) {
for animal in self.animals.iter() {
animal.make_sound();
}
}
}
// Dynamic dispatch - function
fn make_sounds(animals: &[Box<dyn Animal>]) {
for animal in animals.iter() {
animal.make_sound();
}
}
// Static dispatch (generics) - struct
struct ZooWithOneAnimalType<T: Animal> {
// All animals must be of the same type
pub animals: Vec<T>,
}
impl<T> ZooWithOneAnimalType<T>
where
T: Animal,
{
fn make_sounds(&self) {
for animal in self.animals.iter() {
animal.make_sound();
}
}
}
// Static dispatch (generics) - function
fn make_sounds_one_animal_type<T: Animal>(animals: &Vec<T>) {
for animal in animals.iter() {
animal.make_sound();
}
}
fn main() {
// Create a dynamic dispatch zoo
let mut zoo = Zoo {
animals: Vec::new(),
};
// Can add different types of animals
zoo.animals.push(Box::new(Tiger));
zoo.animals.push(Box::new(Lion));
zoo.make_sounds();
// Create a static dispatch zoo
let tiger_zoo = ZooWithOneAnimalType {
animals: vec![Tiger, Tiger, Tiger],
};
tiger_zoo.make_sounds();
// Another static dispatch zoo, but with different types
let lion_zoo = ZooWithOneAnimalType {
animals: vec![Lion, Lion],
};
lion_zoo.make_sounds();
}
Analysis of Practical Application Scenarios
Scenarios Suitable for Dynamic Dispatch
-
Plugin Systems: When you need to dynamically load and use different plugin implementations at runtime.
-
GUI Components: Different types of UI components coexist in the same container.
-
Game Entities: Different entities in a game (players, enemies, NPCs) all implement the same interface.
// Example of game entities
trait GameEntity {
fn update(&mut self, delta_time: f32);
fn render(&self);
}
struct Player { /* ... */ }
struct Enemy { /* ... */ }
struct NPC { /* ... */ }
// Implement GameEntity trait
impl GameEntity for Player { /* ... */ }
impl GameEntity for Enemy { /* ... */ }
impl GameEntity for NPC { /* ... */ }
// The game world can contain any type of entity
struct GameWorld {
entities: Vec<Box<dyn GameEntity>>,
}
impl GameWorld {
fn update(&mut self, delta_time: f32) {
for entity in &mut self.entities {
entity.update(delta_time);
}
}
fn render(&self) {
for entity in &self.entities {
entity.render();
}
}
}
Scenarios Suitable for Static Dispatch
-
Generic Data Structures: Such as custom collection types that need to work with various types.
-
Algorithm Implementations: When applying the same algorithm to different types.
-
Performance-Critical Paths: When dealing with a large amount of data and needing to optimize performance to the maximum.
// Example of a generic sorting function
fn sort<T: Ord>(mut collection: Vec<T>) -> Vec<T> {
collection.sort();
collection
}
// Can be used for any comparable type
let sorted_ints = sort(vec![3, 1, 4, 1, 5]);
let sorted_strings = sort(vec!["hello", "world", "rust"]);
Conclusion
When choosing between dynamic dispatch and static dispatch in Rust, the decision should primarily be based on the following considerations:
-
Flexibility Requirements: If you need to handle multiple different types in the same collection, dynamic dispatch is the better choice.
-
Runtime Behavior: If you need to determine the object type at runtime, dynamic dispatch is more suitable.
-
Compile-Time Information: If all types are known at compile time, static dispatch is usually the more concise choice.
-
Performance Considerations: In scenarios with extreme performance requirements, static dispatch may be superior, but in most cases, this difference is negligible.
Most importantly, do not make decisions solely based on minor performance differences. The choice should be based on code design, flexibility, and maintainability to find the solution that best fits your specific needs.
As mentioned at the beginning of the article: performance is not always the deciding factor! Performance differences at the microsecond level are often imperceptible to users, while code flexibility, maintainability, and design rationality are often more important.
References
- Rust: Dyn (Dynamic Dispatch) vs Generics (Monomorphism / Static Dispatch): https://medium.com/@itsuki.enjoy/rust-dyn-dynamic-dispatch-vs-generics-monomorphism-91ecafb5e324
Book Recommendations
The second edition of “The Rust Programming Language” is an authoritative learning resource written by the Rust core development team and translated by members of the Chinese Rust community. It is suitable for all software developers who wish to evaluate, get started, improve, and study the Rust language, and is regarded as essential reading for Rust development work.
This book introduces the basic concepts of Rust language to unique practical tools, covering advanced concepts such as ownership, traits, lifetimes, safety guarantees, as well as practical tools like pattern matching, error handling, package management, functional features, and concurrency mechanisms. The book includes three complete project development case studies, guiding readers to develop Rust practical projects from scratch.
Notably, this book has been updated to the Rust 2021 version, meeting the systematic learning needs of beginners and serving as a reference guide for experienced developers, making it the best entry point for building solid Rust skills.
Recommended Reading
-
Rust: The Performance King Sweeping C/C++/Go?
-
A C++ Perspective from Rust Developers: Revealing Pros and Cons
-
Rust vs Zig: The Emerging Systems Programming Language Battle
-
Essential Design Patterns for Asynchronous Programming in Rust: Enhancing Your Code’s Performance and Maintainability