Skip to content

Latest commit

 

History

History
137 lines (84 loc) · 7.79 KB

File metadata and controls

137 lines (84 loc) · 7.79 KB

C++ vs Rust in Real Systems Patterns: What Wins, What Ties, What Actually Matters

If you read enough LinkedIn posts, you will eventually be told that Language X is "10x faster," "memory safe and therefore always faster," or "basically the new C but with fewer foot-guns." I wanted a more boring answer: what happens when you write idiomatic C++ and idiomatic Rust for the same systems patterns and actually measure them?

This article is a data-backed comparison across four foundational patterns in systems programming: RAII resource management, lock-free ring buffers, async I/O pipelines, and zero-copy string processing. I wrote the C++ and Rust implementations in ways a real practitioner would write them (not line-by-line translations), then benchmarked with standard tooling. The takeaway is not a crown or a knockout. It is closer to "the win margin is usually small, but the tradeoffs are real."

If you want the code and the scripts, the full repo is in CppRustComparison with all benchmarks and a reproducible runner.

Repro steps (short version):

git clone https://github.com/cschladetsch/CppRustComparison
cd CppRustComparison
python scripts/run_benchmarks.py --all

Methodology (or, How to Avoid Cheating)

Benchmarking is a minefield. The easy way to win is to pick a narrow case and "optimize" the other language into a slow, sad corner. I tried hard not to do that:

  • Each implementation is idiomatic for its language.
  • I used Google Benchmark for C++ and Criterion for Rust.
  • Release builds only, with strong optimization settings.
  • Warm-ups, multiple samples, and confidence intervals.
  • The same pattern and workload for each language.

In other words, this is "how a competent C++ dev and a competent Rust dev would solve it," not "let me port this by hand and call it fair."

Tiny snippet, C++ RAII:

auto buf = std::make_unique<ManagedBuffer>(size);
ScopeGuard cleanup([&] { buf->clear(); });

Tiny snippet, Rust RAII:

let mut buf = ManagedBuffer::new(size);
let _cleanup = ScopeGuard::new(|| buf.clear());

Pattern 1: RAII Resource Management

RAII is the bread and butter of C++ and the heart of Rust's ownership model. It is deterministic cleanup, but the mechanics differ:

  • C++ uses std::unique_ptr and std::shared_ptr with destructors.
  • Rust uses Drop, Box, and Arc.

What I saw

Allocation and deallocation were effectively identical. Both languages call the same allocator and the same OS. The interesting differences show up in reference counting and move semantics:

  • shared_ptr uses two atomics (strong + weak), while Arc uses one. That gives Rust a small edge on ref-counted hot paths.
  • Rust moves are a bitwise copy with no destructor, while C++ moves may run a destructor on the source. This shows up in tight loops but usually not in real-world workloads.

Practical takeaway: You do not pick a language based on raw RAII speed. But you can pick based on how often you are in a ref-counting hot path, and how much you value Rust's move semantics being enforced by the compiler rather than "be careful, please."

Pattern 2: Lock-Free Ring Buffers

Lock-free data structures are a good test of "do the languages generate the same instructions?" because most of the performance is in the atomics, not in the syntax.

Both implementations used comparable atomic operations with acquire-release semantics and cache-line padding. The results were as exciting as watching two identical cars drag race in a parking lot: they tie. When the CPU does the same thing, you get the same performance.

Where it matters

  • The biggest wins were architectural, not language-specific: cache-line alignment, false sharing avoidance, and avoiding unnecessary fences.
  • If you are losing here, it is usually because of data layout, not because Rust or C++ is "faster."

Practical takeaway: This is a tie. Choose based on ergonomics, safety, or ecosystem, not because you think the atomic instructions will be magic in one language.

Pattern 3: Async I/O Pipelines

Async is where the ecosystems really diverge. C++20 gives you coroutines, but you still need an executor. Rust gives you async/await plus tokio, which is a full runtime with a big set of batteries included.

What I saw

The raw coroutine suspend/resume overhead in C++ can be slightly lower. But runtime throughput is often better in Rust when you use tokio's work-stealing scheduler. In short:

  • C++ has lower per-suspend overhead.
  • Rust can scale better when you have lots of concurrent tasks.

Practical takeaway: C++ has the edge in fine-grained, tight coroutine mechanics. Rust tends to win in real-world async workloads because tokio is a tuned, production-grade runtime. You can build one in C++, but you will spend real time doing it.

Pattern 4: Zero-Copy String Processing

This is a pattern I see everywhere in systems code: parsing CSV, JSON, and logs without allocating. The usual building blocks:

  • C++: std::string_view, SSO (Small String Optimization), std::from_chars
  • Rust: &str, Cow<str>, and explicit slicing

What I saw

string_view and &str are basically identical (pointer + length). Zero-copy CSV parsing was a tie. The real difference is SSO:

  • C++ uses SSO for small strings, often 15-22 characters.
  • Rust's String does not use SSO.

On small string-heavy workloads, C++ can be 2-3x faster for operations that stay within SSO. That advantage disappears on larger strings.

Practical takeaway: If you are doing tons of small string work, C++ can be faster. If you are mostly working with borrowed slices or larger strings, it is a wash.

Build and Compile Times (Yes, This Is Also Performance)

Runtime speed is important, but "how fast I can iterate" matters too. I captured clean and incremental builds locally. These numbers are very machine- and toolchain-specific, but they give a useful flavor:

Build Clean build Incremental (touch 1 file) Notes
C++ (CMake Release) 22.94s 4.12s CMake configure took ~33.9s before the first build
Rust (cargo release + LTO) 13.56s 0.69s Incremental rebuild was a single crate recompile

Toolchain details: MSVC 19.44 (Visual Studio 2022), CMake 3.31.5, Rust 1.92.0. C++ was built as Release without LTO; Rust used --release with workspace LTO enabled.

Practical takeaway: Rust's incremental builds are excellent for localized changes. C++ can be fast too, but header fanout can explode compile times. Keep headers clean and your C++ build is fine; ignore them and you will age prematurely.

The Big Picture: Margins Are Small, Tradeoffs Are Real

Here is the boring-but-true summary:

  • Most performance differences are under 10%, often under 5%.
  • The biggest differences come from ecosystem and tooling, not instruction-level differences.
  • You will spend more time choosing an executor, a build system, and a deployment model than you will chasing a 3% micro-benchmark win.

So which should you choose?

I use this rule of thumb:

  • If you need absolute control over memory layout, inline assembly, or have a massive existing C++ codebase: C++.
  • If you want safety guarantees, a modern async ecosystem, and a compiler that stops you before you do something silly at 2 AM: Rust.
  • If you already have a team that is great at one language, you should not switch for a 5% benchmark delta. Switching costs are real and not just in the training budget.

The Honest Conclusion

Both languages can deliver systems-level performance. Rust tends to be safer by default, C++ tends to be more flexible by default. Neither one is "always faster." The real decision is about team skill, ecosystem, and the risk profile of your product.

If you want the code, the benchmarks, or to reproduce the results, everything is in the repo. No vibes, just measurements. (Ok, maybe a few vibes.)


Repo and benchmarks: CppRustComparison