The empire strikes back — C++ enters the fray. C++ vs Rust vs C# vs Go
Part — 2 of the performance comparison in Kubernetes
Introduction
This is part-2 of the performance comparison series. Read that before reading this. I have made some changes to the benchmarking process based on readers feedback.
Disclaimer
Please note that I am no expert in the C++. I am just interested in this understanding how each language performs under stress. Henc, if you think you can improve the performance of the C++ program, you are most welcome to fork my repo and improve upon it.
Prerequisites
This article assumes that you have a basic understanding of C++, Rust, C#, Go, Docker, Terraform, Postgres and Kubernetes.
Changes from Part-1
Based on feedback from you awesome readers, I have made the following changes to the code base.
- Added C++ — Drogon
- Bumped Go to 1.20 from 1.19 and slightly updated the fibonacci generation code
- Changed the Go’s framework from Gin to Atreugo. I have set prefork to true. For more info on PreFork, read the atreugo documentation here.
- Bumped Rust to 1.71 from 1.65
- Removed swagger endpoint from the C# project
- Changed the Fibonacci input number from a constant (40) to a random number between 5 to 40
Link to the source code.
Performance monitoring
Configuration
Every container will have the following configuration:
Requests: memory of 512 Mi and cpu of 500m
Limits: memory of 1024 Mi and cpu of 1000m
Initial Observation
Without any load the base running application benchmarks are showcased below.
CPU% — No Load
Memory Usage — No Load
The yellow line represents C++, red line represents Rust, Blue line represents Go and the Green line represents C#.
In idle state rust’s CPU usage is slightly higher than Go and Go is slightly higher than C# and C++. C#’s memory usage when idle is slightly higher than Go, and Go’s memory usage is slightly higher than C++. Rust’s memory usage is very minimal.
Load for GET Request
We are going to use an excellent tool called bombardier.
First we will hit all 4 services with 1 million requests from 50 connections and limit the rate to with 100 request per second.
Command for doing so is:
bombardier -n 1000000 -m GET -c 50 -r 100 <URL>
CPU% — Under Load
C# has the highest CPU usage of the lot averaging around 5%. Go, Rust and C++ has minimal usage with C++ CPU usage being the least.
Memory Usage — Under Load
C# memory usage is certainly higher in this scenario, followed by Go. But C++ and Rust’s memory usage hasn’t changed.
Let us look at latencies.
p99 — Under Load
p99 (99% of requests being served):
C++ — 4.97 ms C# — 4.96 ms, Go — 4.96 ms, Rust — 4.95 ms
Rust has a slight edge over others.
Bombardier stats:
C++:
Rust:
C#:
Go:
C# seems to be have the highest throughput.
Load for POST Request with limits
Now let us hit all 3 services with 1 million requests from 50 connections and limit the rate to with 100 request per second. But this time we will use a single connection making 1 request every second.
Bombardier command is
bombardier -n 1000000 -m POST -c 1 -r 1 <URL>
C++, Go and C# seem to fluctuate in CPU usage.
C# has the highest memory usage.
p99 — POST Load
Latency is where things get interesting. Go’s latency is trying to keep up with C# it isn’t consistent. Rust seems to be shining as it seems have half the latency of C#.
Bombardier stats:
C++:
Rust:
C#:
Go:
C# seems to have the highest throughput.
Conclusion
From this benchmark, I am able to understand that my implementation of C++ is not efficient and Rust has consistent performance and is almost always faster than C# and Go while C++ seems to be a slightly fluctuating which I would mostly allude to the way how I have implemented the code. While C++ is an amazingly powerful tool, it goes to show that it requires a level of mastery to really wield that power.