Introduction
After my previous benchmark comparing .NET 9 and Go was well received, I knew I had to revisit this battle one more time before.NET 10 is released. The performance improvements Microsoft did on .Net 9 was mindblowing which they further improved in .Net 10. So I decided that before the official .Net 10 is released I will do one more performance benchmark with .Net 9.
Executive Summary
π Go wins overall by 13.2% in total execution time, but the story is more nuanced:
- Go dominates in mathematical operations (+32.9%)
- .NET 9 dominates in collection processing (+66.7%)
- It’s a split decision with each language excelling in different areas
The gap has narrowed significantly, making this one of the closest comparisons yet.
The Setup: No Bias, Pure Performance
To ensure absolute fairness, I created identical benchmark applications in both languages:
- Same algorithms, same logic, same operations
- Same data structures and memory patterns
- Same hardware
- Same conditions (clean environment, no background processes)
Benchmark Categories:
- Prime Number Calculation (CPU-intensive)
- String Manipulation (Memory allocation)
- Mathematical Operations (Floating-point performance)
- Collection Processing (Memory management)
The Code: Identical Implementations
.NET 9 Implementation
using System.Diagnostics;
Console.WriteLine("Starting benchmarks...");
var stopwatch = Stopwatch.StartNew();
// Run all benchmarks
PrimeBenchmark();
StringManipulationBenchmark();
MathematicalBenchmark();
CollectionBenchmark();
stopwatch.Stop();
Console.WriteLine($"All benchmarks completed in {stopwatch.ElapsedMilliseconds}ms");
return;
static void PrimeBenchmark()
{
Console.WriteLine("\n=== Prime Number Benchmark ===");
var stopwatch = Stopwatch.StartNew();
var count = 0;
for (var i = 2; i <= 1000000; i++)
{
if (IsPrime(i))
{
count++;
}
}
stopwatch.Stop();
Console.WriteLine($"Found {count} primes in {stopwatch.ElapsedMilliseconds}ms");
}
static bool IsPrime(int number)
{
if (number <= 1)
return false;
if (number == 2)
return true;
if (number % 2 == 0)
return false;
var boundary = (int)Math.Floor(Math.Sqrt(number));
for (var i = 3; i <= boundary; i += 2)
{
if (number % i == 0)
return false;
}
return true;
}
static void StringManipulationBenchmark()
{
Console.WriteLine("\n=== String Manipulation Benchmark ===");
var stopwatch = Stopwatch.StartNew();
var result = "";
for (var i = 0; i < 100000; i++)
{
result += i.ToString();
if (result.Length > 10000)
{
result = result[..Math.Min(result.Length, 5000)];
}
}
stopwatch.Stop();
Console.WriteLine($"String manipulation completed in {stopwatch.ElapsedMilliseconds}ms");
}
static void MathematicalBenchmark()
{
Console.WriteLine("\n=== Mathematical Operations Benchmark ===");
var stopwatch = Stopwatch.StartNew();
double sum = 0;
for (var i = 0; i < 10000000; i++)
{
sum += Math.Sqrt(i) * Math.Sin(i) + Math.Cos(i) / Math.Sqrt(i + 1);
}
stopwatch.Stop();
Console.WriteLine($"Mathematical operations completed in {stopwatch.ElapsedMilliseconds}ms");
Console.WriteLine($"Sum: {sum}");
}
static void CollectionBenchmark()
{
Console.WriteLine("\n=== Collection Operations Benchmark ===");
var stopwatch = Stopwatch.StartNew();
// Create list with 1,000,000 items
var list = new List<int>();
for (var i = 0; i < 1000000; i++)
{
list.Add(i);
}
// Filter even numbers (manual loop - same as Go)
var evenNumbers = new List<int>();
foreach (var x in list)
{
if (x % 2 == 0)
{
evenNumbers.Add(x);
}
}
// Square each number (manual loop - same as Go)
// Use long to prevent overflow, same as Go will use int64
var squaredNumbers = new List<long>();
foreach (var x in evenNumbers)
{
squaredNumbers.Add((long)x * x);
}
// Sum (manual loop - same as Go)
long total = 0;
foreach (var x in squaredNumbers)
{
total += x;
}
stopwatch.Stop();
Console.WriteLine($"Collection operations completed in {stopwatch.ElapsedMilliseconds}ms");
Console.WriteLine($"Total: {total}");
Console.WriteLine($"List size: {list.Count}, Even numbers: {evenNumbers.Count}, Squared numbers: {squaredNumbers.Count}");
}
Go 1.22.2 Implementation
package main
import (
"fmt"
"math"
"time"
)
func main() {
fmt.Println("Starting benchmarks...")
start := time.Now()
// Run all benchmarks
primeBenchmark()
stringManipulationBenchmark()
mathematicalBenchmark()
collectionBenchmark()
elapsed := time.Since(start)
fmt.Printf("All benchmarks completed in %v\n", elapsed)
}
func primeBenchmark() {
fmt.Println("\n=== Prime Number Benchmark ===")
start := time.Now()
count := 0
for i := 2; i <= 1000000; i++ {
if isPrime(i) {
count++
}
}
elapsed := time.Since(start)
fmt.Printf("Found %d primes in %v\n", count, elapsed)
}
func isPrime(number int) bool {
if number <= 1 {
return false
}
if number == 2 {
return true
}
if number%2 == 0 {
return false
}
boundary := int(math.Floor(math.Sqrt(float64(number))))
for i := 3; i <= boundary; i += 2 {
if number%i == 0 {
return false
}
}
return true
}
func stringManipulationBenchmark() {
fmt.Println("\n=== String Manipulation Benchmark ===")
start := time.Now()
result := ""
for i := 0; i < 100000; i++ {
result += fmt.Sprintf("%d", i)
if len(result) > 10000 {
if len(result) > 5000 {
result = result[:5000]
}
}
}
elapsed := time.Since(start)
fmt.Printf("String manipulation completed in %v\n", elapsed)
}
func mathematicalBenchmark() {
fmt.Println("\n=== Mathematical Operations Benchmark ===")
start := time.Now()
sum := 0.0
for i := 0; i < 10000000; i++ {
sum += math.Sqrt(float64(i))*math.Sin(float64(i)) + math.Cos(float64(i))/math.Sqrt(float64(i)+1)
}
elapsed := time.Since(start)
fmt.Printf("Mathematical operations completed in %v\n", elapsed)
fmt.Printf("Sum: %f\n", sum)
}
func collectionBenchmark() {
fmt.Println("\n=== Collection Operations Benchmark ===")
start := time.Now()
// Create slice with 1,000,000 items
list := make([]int, 0)
for i := 0; i < 1000000; i++ {
list = append(list, i)
}
// Filter even numbers (manual loop - same as .NET)
evenNumbers := make([]int, 0)
for _, x := range list {
if x%2 == 0 {
evenNumbers = append(evenNumbers, x)
}
}
// Square each number (manual loop - same as .NET)
// Use int64 to prevent overflow, same as .NET's long
squaredNumbers := make([]int64, 0)
for _, x := range evenNumbers {
squaredNumbers = append(squaredNumbers, int64(x)*int64(x))
}
// Sum (manual loop - same as .NET)
var total int64 = 0
for _, x := range squaredNumbers {
total += x
}
elapsed := time.Since(start)
fmt.Printf("Collection operations completed in %v\n", elapsed)
fmt.Printf("Total: %d\n", total)
fmt.Printf("List size: %d, Even numbers: %d, Squared numbers: %d\n", len(list), len(evenNumbers), len(squaredNumbers))
}
Benchmark | .NET 10 | Go 1.22 | Winner | Difference |
---|---|---|---|---|
Prime Calculation | 101ms | 97ms | Go | +4.1% |
String Manipulation | 171ms | 187ms | .NET | +9.4% |
Math Operations | 339ms | 255ms | Go | +32.9% |
Collection Processing | 21ms | 35ms | .NET | +66.7% |
Total Time | 650ms | 574ms | Go | +13.2% |
Key Insights
Where Go Truly Shines:
- Mathematical Operations: 32.9% faster - Go’s math libraries show exceptional performance
- CPU-Bound Tasks: Better raw computation for algorithms like prime calculation
- Compilation Speed: Faster build times (not measured but notable)
Where .NET 9 Dominates:
- Collection Processing: Massive 66.7% advantage - LINQ and memory management improvements pay off
- String Manipulation: 9.4% faster - Significant improvements in string handling
π Surprising Findings:
- Collection operations show the biggest performance gap in favor of .NET
- Mathematical operations show the biggest gap in favor of Go
- The total time difference (13.2%) is smaller than expected given the individual category gaps
Final Thoughts
The performance gap has narrowed to the point where developer productivity, ecosystem, and team expertise should be the primary decision factors rather than raw performance alone. Both languages are excellent choices, and the “right” choice depends entirely on your specific use case and team composition.
What’s Next?
π .NET 10 Preview: I’m already testing .NET 10 preview builds - follow me to see if it finally beats Go!