DEV Community

Donald Johnson
Donald Johnson

Posted on

Comparing C, Go, and Rust for Simple I/O Performance

When you're working with a machine as powerful as an AMD Ryzen Threadripper PRO 5975WX, you expect things to move fast. With 32 cores, 64 virtual CPUs, and 251 GB of RAM, the bottleneck is rarely the hardware. It's usually the software.

This experiment pits three popular systems languages — C, Go, and Rust — against each other in a simple task: writing 100,000 lines to a file. The question is, which one handles this basic I/O task better? Each language has its own strengths: C is known for its raw speed and direct system access, Go simplifies concurrency, and Rust promises memory safety without compromising performance.

We'll take a look at how each of these languages performs by tracing their system calls, measuring their execution times, and identifying where they hit snags.

The Setup

For this experiment, each program runs on a machine running Ubuntu 22.04 LTS with the following specs:

  • CPU: AMD Ryzen Threadripper PRO 5975WX (32 Cores, 64 Threads)
  • Memory: 251 GB
  • Disk: Plenty of space, so that’s not the issue here.

Each language was tasked with writing 100,000 lines to a file, and we used strace to track system calls and time to measure how long it took.

The Results

Here’s how the numbers came out:

Language I/O Time System Call Time Number of write Calls
C 0.007426 seconds 0.001266 seconds 267
Go 0.230 seconds 0.140 seconds 3,102
Rust 5.47 seconds 1.35 seconds 300,001

C is absurdly fast, which is no surprise. Go does alright but isn't nearly as quick. Rust, on the other hand, struggles with performance — but the reason for that is interesting.

Why C Wins

C’s performance comes down to simplicity. It directly interacts with the system, and in this test, it only makes 267 system calls to write 100,000 lines. This means C is doing its job with minimal overhead, moving data straight to the file with very little in the way of interference. That’s why C is still the go-to language when performance is critical.

On your machine, with all those cores humming along and plenty of memory to buffer operations, C can fully leverage the hardware’s potential. You don’t need to worry about concurrency or memory safety — C just does the job as fast as the hardware allows.

Go’s Middle Ground

Go's performance is respectable but slower, and the main reason is its runtime overhead. Go uses futex system calls to manage goroutines, which are lightweight threads. In this test, 88% of Go’s system call time is spent on these futex calls. While futex makes Go powerful for concurrent tasks, it adds overhead for simple I/O tasks like this one.

Still, Go only makes 3,102 write calls, which is a lot more than C but far fewer than Rust. That’s thanks to Go’s buffered I/O, which reduces the number of times the program has to hit the system.

Go might not be as fast as C in raw I/O, but it’s designed for more complex, real-world applications where concurrency matters. On a system like yours, Go’s ability to handle multiple tasks at once would shine in a different type of workload.

Rust’s Bottleneck

Rust is the slowest in this test, and the problem is clear: it makes 300,001 write system calls. That's one for every line written, plus one for each newline character. That’s a huge overhead and explains why Rust takes so long compared to C and Go.

However, the issue here isn’t inherent to Rust — it’s that the code isn’t optimized. By default, Rust doesn’t use buffered I/O, which means every single write goes straight to the system. With some simple optimizations, Rust could easily close the performance gap.

Fixing Rust

Here’s how you can make Rust faster using a buffered writer:

use std::fs::File;
use std::io::{BufWriter, Write};
use std::time::Instant;

fn main() {
    let start = Instant::now();

    let file = File::create("output_rust.txt").expect("Could not create file");
    let mut writer = BufWriter::new(file);

    for i in 0..100000 {
        writeln!(writer, "Line {}", i).expect("Could not write to file");
    }

    let duration = start.elapsed();
    println!("Rust I/O Time: {:?}", duration);
}
Enter fullscreen mode Exit fullscreen mode

With this small change, Rust can dramatically reduce the number of system calls, leading to a significant performance boost. It’s a great example of how Rust’s safety features don’t have to come at the cost of speed, as long as you optimize your code for the task at hand.

Conclusion

If you need raw I/O speed, C is still the clear winner. Its direct approach to system calls and lack of runtime overhead make it the fastest option for tasks like this.

Go provides a good balance between ease of use and performance, especially in applications where concurrency is needed. In more complex systems where multiple tasks are running in parallel, Go could easily outperform C because of its goroutines.

Rust, while slow in this test, can be optimized with buffered I/O. Rust’s focus on safety makes it a great choice when you need both speed and memory protection, but you need to be mindful of how you handle I/O.

On your system, with its immense processing power and memory, all three languages can be optimized to perform extremely well. C’s performance shines with low-level tasks, Go thrives with concurrency, and Rust has the potential to match them both with the right optimizations.

Top comments (0)