DEV Community

Cover image for What Makes Rust Stand Out?
dev.to staff for The DEV Team

Posted on

What Makes Rust Stand Out?

As a Rust enthusiast, what advice would you give to developers considering learning Rust and incorporating it into their tech stack?

Follow the DEVteam for more discussions and online camaraderie!

Top comments (3)

Collapse
 
wiseai profile image
Mahmoud Harmouch • Edited

Oh, you're asking about what sets Rust apart from other programming languages? Well, lemme tell you, Rust truly has some remarkable features that make it stand out as a unique language. It's not just a mere programming language; it's a work of art in the world of software development. Let me tear it down a lil bit.

A. C-like Speed in execution and Memory Allocation

Rust boasts an impressive performance similar to that of C, allowing developers to write code that executes efficiently and maximizes hardware resources. Let's compare the Rust code for generating the Fibonacci sequence with the equivalent Java and C code.

  • Rust Code for Fibonacci Sequence:
use std::hint::black_box;
use std::time::Instant;

fn fibonacci(n: usize) -> Vec<u64> {
    let mut fibonacci: Vec<u64> = Vec::with_capacity(n);  // 👈 memory allocation on the heap
    fibonacci.push(0);
    fibonacci.push(1);
    for i in 2..n {
        let next: u64 = fibonacci[i - 1] + fibonacci[i - 2];
        fibonacci.push(next);
    }
    fibonacci
}

fn main() {
    let mut total: f64 = 0.0;
    const N: usize = 10_000_000;

    for _ in 1..=20 {   // 👈 20 iterations
        let start: Instant = Instant::now();
        black_box(fibonacci(black_box(N)));  // 👈 black_box is used to remove noisy noise
        let elapsed: f64 = start.elapsed().as_secs_f64();
        total += elapsed;
    }

    let avg_time: f64 = total / 20.0; // 👈 average
    println!("Average time taken: {} s", avg_time);
}

// cmd: $ cargo run --release
// Output: Average time taken: 0.03736314269999994 s
Enter fullscreen mode Exit fullscreen mode
  • Java Code for Fibonacci Sequence:
import java.util.ArrayList;
import java.util.List;

public class Fibonacci {
    public static List<Long> fibonacci(int n) {
        List<Long> fibonacci = new ArrayList<>(n); // 👈 ArrayList is intentional here, comparing heap allocation speed.
        fibonacci.add(0L);
        fibonacci.add(1L);
        for (int i = 2; i < n; i++) {
            long next = fibonacci.get(i - 1) + fibonacci.get(i - 2);
            fibonacci.add(next);
        }
        return fibonacci;
    }

    public static void main(String[] args) {
        double total = 0.0;
        final int N = 10_000_000;

        for (int count = 1; count <= 24; count++) {
            long startTime = System.nanoTime();
            List<Long> result = fibonacci(N);
            long endTime = System.nanoTime();
            double elapsed = (endTime - startTime) / 1_000_000_000.0;
            if (count > 4) { // 👈 Exclude JIT
                total += elapsed;
            }
        }

        double avgTime = total / 20.0; // 👈 24 - 5 + 1 = 20
        System.out.println("Average time taken: " + avgTime + " s");
    }
}

// cmd: $ javac Fibonacci.java && java Fibonacci
// Output: Average time taken: 0.121429435499999 s
Enter fullscreen mode Exit fullscreen mode
  • C Code for Fibonacci Sequence:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

long* fibonacci(int n) {
    long* fibonacci = (long*)malloc(n * sizeof(long)); // 👈 memory allocation on the heap.
    fibonacci[0] = 0;
    fibonacci[1] = 1;
    for (int i = 2; i < n; i++) {
        fibonacci[i] = fibonacci[i - 1] + fibonacci[i - 2];
    }
    return fibonacci;
}

int main() {
    double total = 0.0;
    const int N = 10000000;

    for (int count = 1; count <= 20; count++) {
        clock_t start = clock();
        long* result = fibonacci(N);
        clock_t end = clock();
        double elapsed = (double)(end - start) / CLOCKS_PER_SEC;
        total += elapsed;
        free(result); // Remember to free the dynamically allocated memory
    }

    double avg_time = total / 20.0;
    printf("Average time taken: %f s\n", avg_time);
    return 0;
}

// cmd: $ gcc -O3 -march=native -mtune=native -Wall file_name.c -o file_name && ./file_name
// Output: Average time taken: 0.033067 s
Enter fullscreen mode Exit fullscreen mode

Comparison:

  • Memory Management:

    • Rust: Rust's ownership system ensures memory safety and avoids memory leaks without relying on garbage collection.
    • Java: Java employs a garbage collector to manage memory, which can lead to occasional pauses during garbage collection.
    • C: C requires explicit memory management using malloc and free, making it more prone to memory leaks and segmentation faults if not handled carefully.
  • Performance:

    • Rust: Rust is known for its focus on performance and low-level control, which can result in efficient code execution, especially in CPU-bound tasks.
    • Java: Java's Just-In-Time (JIT) compilation and optimization may provide good performance in long-running applications, but it might have an initial warm-up phase.
    • C: C is a low-level language and can offer excellent performance as well, but it lacks Rust's memory safety guarantees.

Rust, Java, and C can handle the Fibonacci sequence generation, but Rust's concise syntax and focus on performance may make it an attractive choice for certain computational tasks. Java's garbage collection and runtime optimizations may be advantageous for long-running server applications, but not always the case.

B. Threads

  • Rust:
use std::thread;
use std::sync::{Arc, Mutex};

fn main() {
    let mut handles = vec![];
    let num_threads = 5;
    let counters = Arc::new(Mutex::new(vec![0; num_threads]));

    for t in 0..num_threads {
        let counters_ref = Arc::clone(&counters);
        let handle = thread::spawn(move || {
            for _ in 0..100_000 {
                // 👇 Each thread acquires a lock on the mutex
                let mut counters = counters_ref.lock().unwrap();
                counters[t] += 1;
            }
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    let counters = counters.lock().unwrap();
    let total_counter: usize = counters.iter().sum();

    println!("Final counter value: {}", total_counter);
}

// cmd: $ cargo run --release
// Output: Final counter value: 500000
Enter fullscreen mode Exit fullscreen mode
  • Python:
import threading

def thread_function(counters, t):
    for _ in range(100_000):
        with counters_lock:
            counters[t] += 1

num_threads = 5
counters = [0] * num_threads
counters_lock = threading.Lock()

threads = []

for t in range(num_threads):
    thread = threading.Thread(target=thread_function, args=(counters, t))
    threads.append(thread)
    thread.start()

for thread in threads:
    thread.join()

total_counter = sum(counters)

print(f"Final counter value: {total_counter}")

# cmd: $ python file_name.py
# Output: Final counter value: 500000
Enter fullscreen mode Exit fullscreen mode

In both the Rust and Python examples, we have five threads, and each thread increments the corresponding counter 100,000 times.

Comparison :

  • Safety and Memory Management:

    • Rust: Rust's memory safety and ownership model prevent data races at compile-time. The compiler enforces that shared data is safely accessed, and it is not possible to have data races in a correct Rust program.
    • Python: Python's threading module does not prevent data races or guarantee memory safety. Python's Global Interpreter Lock (GIL) allows only one thread to execute Python bytecode at a time, which avoids parallelism with CPU-bound tasks but still allows data races with custom C extensions or I/O-bound tasks.
  • Performance:

    • Rust: Rust's explicit memory management and lack of a Global Interpreter Lock enable it to take full advantage of multi-core processors and achieve true parallelism, resulting in potentially better performance in CPU-bound tasks.
    • Python: Due to the GIL, Python threads are more suitable for I/O-bound tasks that may involve waiting for external resources rather than heavy CPU-bound computations. The GIL can hinder Python's performance in CPU-bound tasks.

Rust's unique advantage here is its aka "fearless concurrency". Rust enforces strict rules at compile-time to prevent data races and other concurrency bugs. In contrast, many other languages, like Python, rely on the programmer's diligence and can be more prone to concurrency-related issues.

C. Option Handling

Rust example:

fn divide(a: f64, b: f64) -> Option<f64> {
    if b == 0.0 {
        None
    } else {
        Some(a / b)
    }
}

fn main() {
    let result = divide(10.0, 2.0);
    match result {
        Some(res) => println!("Result: {}", res),
        None => println!("Error: Cannot divide by zero"),
    }
}

// cmd: $ cargo run --release
// Output: Result: 5
Enter fullscreen mode Exit fullscreen mode

Comparison to other languages (e.g., Java):

public class OptionHandling {
    public static Double divide(double a, double b) {
        if (b == 0.0) {
            return null;
        } else {
            return a / b;
        }
    }

    public static void main(String[] args) {
        Double result = divide(10.0, 2.0);
        if (result != null) {
            System.out.println("Result: " + result);
        } else {
            System.out.println("Error: Cannot divide by zero");
        }
    }
}

// cmd: javac OptionHandling.java && java OptionHandling
// Output: Result: 5
Enter fullscreen mode Exit fullscreen mode

Rust's Option type forces the developer to handle potential None values explicitly, promoting safer code. In contrast, languages like Java use null values, leading to potential null pointer exceptions if not handled properly.

D. Maturity Beyond Its Years

Though Rust may seem relatively young, it has rapidly gained traction and established itself as a mature language in a remarkably short time. Developers worldwide, me included, have embraced it for a wide range of applications, from data science and embedded systems to web development. Its rapid adoption is a testament to its robustness and capability to tackle diverse programming challenges.

E. Expansive and Dynamic Ecosystem

Rust polars
Rust owes its success, in part, to its vibrant and extensive ecosystem. The community-driven open-source libraries available for Rust surpass those of many other languages in terms of performance and functionality. Take, for instance, the library Polars – it's a perfect illustration of how Rust's ecosystem offers cutting-edge solutions that are a staggering ~14 times faster than other alternatives, like Pandas. This collaborative and innovative ecosystem keeps Rust at the forefront of modern development.


So, you see, Rust shines bright among the rest, and that's what makes it so popular with developers. Now, enough of my rambling – hope you've got a clearer picture of why Rust rocks, rant over!

Collapse
 
baenencalin profile image
Calin Baenen • Edited

An easy to use dependencymanager and testing tool, cargo;
an easy to understand manifest format, contained in Cargo.toml;
a straightforward crate anatomy;
Java- or C++ like, but, over-all, just as good (if not better) than C;
... and it's very easy to understand.

Learn more about Rust here.

Collapse
 
philipjohnbasile profile image
Philip John Basile

It’s a great change of scenery language. I’m feeling the exhaustion of JavaScript framework burnout and this is like a nice bowl of Chicken Soup.