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:

  1. Prime Number Calculation (CPU-intensive)
  2. String Manipulation (Memory allocation)
  3. Mathematical Operations (Floating-point performance)
  4. 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!