Referencing, Deep Cloning, and Shallow Cloning in Rust

Hi, fellow Rustaceans!

Referencing, Deep Cloning, and Shallow Cloning in Rust

Hi, fellow Rustaceans!

Rust offers distinct data handling methods: referencing, deep cloning, and shallow cloning. Each method has its specific use cases, performance implications, and mechanics.

Let’s explore some of these approaches to help us make informed decisions in our coding practices.

1. Referencing

Referencing in Rust is a way to access data without owning it. It’s achieved through either immutable or mutable references and is central to Rust’s memory safety guarantees.

Immutable References (&T)

Immutable references allow read-only access to data. Multiple immutable references can coexist, but they cannot coexist with a mutable reference to the same data.

Example:

fn main() { 
    let data = vec![1, 2, 3]; 
    let ref1 = &data; 
    let ref2 = &data; 
    println!("ref1: {:?}, ref2: {:?}", ref1, ref2); 
}

Mutable References (&mut T)

Mutable references allow modifying the data they reference. Only one mutable reference to a particular piece of data is allowed at a time.

Example:

fn main() { 
    let mut data = vec![1, 2, 3]; 
    let ref_to_data = &mut data; 
    ref_to_data.push(4); 
    println!("{:?}", ref_to_data); 
}

Performance Implications

Referencing is a lightweight operation in Rust. It does not involve any data copying, making it highly efficient in terms of performance and memory usage.

2. Deep Cloning (clone)

Deep cloning creates an entirely new instance of the data, including all nested data structures.

Mechanics

When .clone() is called on a data structure, Rust recursively copies all fields, creating a completely independent object. The Clone trait defines the cloning logic.

Example:

#[derive(Clone, Debug)] 
struct CustomData { 
    values: Vec<i32>, 
} 
 
fn main() { 
    let original = CustomData { values: vec![1, 2, 3] }; 
    let deep_clone = original.clone(); 
    println!("Original: {:?}", original); 
    println!("Deep Clone: {:?}", deep_clone); 
}

Performance Implications

Deep cloning can be expensive, especially for large or complex data structures. It involves additional memory allocation and data copying.

3. Shallow Cloning

Shallow cloning in Rust typically involves smart pointers like Rc (Reference Counted) or Arc (Atomic Reference Counted). It creates a new pointer to the same data, increasing the reference count but not deeply copying the data.

Mechanics

  • Rc<T>: Used for single-threaded scenarios. It enables multiple owners of the same data.
  • Arc<T>: Thread-safe version of Rc<T>, suitable for multi-threaded contexts.

Example:

use std::rc::Rc; 
 
fn main() { 
    let original = Rc::new(vec![1, 2, 3]); 
    let shallow_clone = original.clone(); // Increases reference count 
    println!("Original: {:?}", original); 
    println!("Shallow Clone: {:?}", shallow_clone); 
}

Performance Implications

Shallow cloning is more efficient than deep cloning as it avoids data duplication. However, it adds overhead for reference counting and is not suitable when independent data manipulation is needed.

Advanced Referencing

Beyond basic usage, references can be leveraged in more complex scenarios like lifetimes and trait objects.

Lifetimes

Lifetimes ensure that references are valid for as long as they are used. Advanced use cases might involve specifying lifetimes explicitly to manage complex data relationships.

Example:

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { 
    if x.len() > y.len() { x } else { y } 
}

Trait Objects

Trait objects allow for dynamic polymorphism using references. This is useful when you want to operate on different types that implement the same trait.

Example:

trait Speak { 
    fn speak(&self); 
} 
 
fn make_some_noise(speaker: &dyn Speak) { 
    speaker.speak(); 
}

Advanced Deep Cloning

Deep cloning can be customized using the Clone trait. This is particularly useful when dealing with complex data structures where only parts of the structure need to be deeply cloned.

Customizing Clone

Implementing Clone manually allows you to specify exactly how deep cloning should behave.

Example:

#[derive(Debug)] 
struct DeepComplex { 
    data: Vec<i32>, 
} 
 
impl Clone for DeepComplex { 
    fn clone(&self) -> Self { 
        DeepComplex { data: self.data.clone() } // Custom clone logic 
    } 
}

Advanced Shallow Cloning

With Rc and Arc, advanced patterns like interior mutability can be implemented.

Interior Mutability

Using RefCell with Rc or Mutex/RwLock with Arc allows for mutability within an otherwise immutable data structure.

Example with Rc and RefCell:

use std::rc::Rc; 
use std::cell::RefCell; 
 
let shared_data = Rc::new(RefCell::new(5)); 
let shared_data_clone = Rc::clone(&shared_data); 
*shared_data_clone.borrow_mut() += 1;

Performance Implications in Practice

To demonstrate the performance impacts of referencing, deep cloning, and shallow cloning in Rust, we’ll create examples that compare these methods in scenarios involving large data structures. These examples will highlight the time taken for each operation, providing a clear comparison of their performance characteristics.

For our demonstration, we’ll use a large vector of integers. We’ll define a function to process this vector, which will be used in each scenario to ensure a consistent workload.

fn process_data(data: &Vec<i32>) { 
    // Simulate some processing 
    let sum: i32 = data.iter().sum(); 
    println!("Sum of data: {}", sum); 
} 
 
fn main() { 
    let large_data = vec![0; 10_000_000]; // Large vector with 10 million elements 
    // We'll use this large_data in the following scenarios 
}

Scenario 1 — Referencing

In this scenario, we’ll pass the large vector by reference to the process_data function.

// Continuing from the main function 
 
let start = std::time::Instant::now(); 
 
process_data(&large_data); 
 
let duration = start.elapsed(); 
 
println!("Time taken with referencing: {:?}", duration);

Scenario 2 — Deep Cloning

Here, we’ll clone the large vector and then pass the cloned vector to the process_data function.

// Continuing from the main function 
 
let start = std::time::Instant::now(); 
 
let cloned_data = large_data.clone(); 
 
process_data(&cloned_data); 
 
let duration = start.elapsed(); 
 
println!("Time taken with deep cloning: {:?}", duration);

Scenario 3 — Shallow Cloning

For shallow cloning, we’ll use an Rc (Reference Counted) pointer. We'll clone the Rc pointer and pass it to a modified version of process_data that accepts an Rc reference.

use std::rc::Rc; 
 
fn process_data_rc(data: &Rc<Vec<i32>>) { 
    // Similar processing as before 
    let sum: i32 = data.iter().sum(); 
    println!("Sum of data: {}", sum); 
} 
 
// Continuing from the main function 
let large_data_rc = Rc::new(large_data); 
 
let shallow_clone = large_data_rc.clone(); 
 
let start = std::time::Instant::now(); 
 
process_data_rc(&shallow_clone); 
 
let duration = start.elapsed(); 
 
println!("Time taken with shallow cloning: {:?}", duration);

When you run this code, you should expect to see the following trends:

  • Referencing will likely be the fastest, as it simply passes a reference without any data copying.
  • Deep Cloning will be significantly slower, especially with a large data structure, due to the time taken to copy all the data.
  • Shallow Cloning with Rc should be faster than deep cloning but slightly slower than referencing due to the overhead of reference counting.

This exercise helps with some insight into the performance implications of each method, reinforcing the importance of choosing the right approach based on the specific requirements of your application.

Choosing the Right Approach

The decision between referencing, deep cloning, and shallow cloning often comes down to:

  • Data Ownership: Who needs to own the data, and for how long?
  • Data Size: Is the data large enough that cloning would be costly?
  • Concurrency: Is the data being accessed from multiple threads?
  • Mutability Requirements: Does the data need to be modified, and if so, how frequently and by whom?

🚀 Explore a Wealth of Resources in Software Development and More by Luis Soares

📚 Learning Hub: Expand your knowledge in various tech domains, including Rust, Software Development, Cloud Computing, Cyber Security, Blockchain, and Linux, through my extensive resource collection:

  • Hands-On Tutorials with GitHub Repos: Gain practical skills across different technologies with step-by-step tutorials, complemented by dedicated GitHub repositories. Access Tutorials
  • In-Depth Guides & Articles: Deep dive into core concepts of Rust, Software Development, Cloud Computing, and more, with detailed guides and articles filled with practical examples. Read More
  • E-Books Collection: Enhance your understanding of various tech fields with a series of free e-Books, including titles like “Mastering Rust Ownership” and “Application Security Guide” Download eBook
  • Project Showcases: Discover a range of fully functional projects across different domains, such as an API Gateway, Blockchain Network, Cyber Security Tools, Cloud Services, and more. View Projects
  • LinkedIn Newsletter: Stay ahead in the fast-evolving tech landscape with regular updates and insights on Rust, Software Development, and emerging technologies by subscribing to my newsletter on LinkedIn. Subscribe Here

🔗 Connect with Me:

  • Medium: Read my articles on Medium and give claps if you find them helpful. It motivates me to keep writing and sharing Rust content. Follow on Medium
  • Personal Blog: Discover more on my personal blog, a hub for all my Rust-related content. Visit Blog
  • LinkedIn: Join my professional network for more insightful discussions and updates. Connect on LinkedIn
  • Twitter: Follow me on Twitter for quick updates and thoughts on Rust programming. Follow on Twitter

Wanna talk? Leave a comment or drop me a message!

All the best,

Luis Soares
luis.soares@linux.com

Senior Software Engineer | Cloud Engineer | SRE | Tech Lead | Rust | Golang | Java | ML AI & Statistics | Web3 & Blockchain

Read more