Go Benchmarks:Does Pass by Pointer Really Make a Difference?

TL;DR: Pass by Value vs. Pass by Pointer in Go

I've been diving deep into Go over the past week, exploring its features and performance characteristics.

One of the fundamental concepts I've been examining is how Go handles data passing in functions, particularly with structs.

But first, what is a even a struct ?

For example, here's a struct of 1024Mb (1Gb):

type LargeStruct struct {
    data [1024 * 1024 * 1024]byte // 1024MB of data (1 GB)
}
// 1024 bytes = 1KB
// 1024 KB = 1MB
// 1024 MB = 1GB

Benchmarking: Pass by Value vs. Pass by Pointer

The key question is: Should you pass structs by value or by pointer when performance matters?

In Go, you can pass a struct to a function in two ways:

Pass by Value: A copy of the entire struct is made, which can be inefficient for large structs as it uses additional memory and processing time.

Pass by Pointer: Instead of passing the whole struct, you pass a pointer to it. This avoids copying the struct, making it more memory-efficient, especially for large data sizes.

The Benchmark Setup

To truly understand the performance difference, I designed a benchmark that compares passing large structs by value and by pointer. My goal was to identify when passing by value becomes inefficient as the struct size grows.


How Did I Benchmark?

    // Run benchmarks for sizes from 1 byte to 1024MB (1 Gb)
    for size := 1; size <= 1*1024*1024*1024; size *= 2 { // Increase size by 2x
        durationValue, durationPointer := benchmark(size)

        // Convert size to megabytes for easier readability
        sizeMB := float64(size) / (1024 * 1024)

        // Write the results to CSV (size in MB, passByValue time, passByPointer time)
        writer.Write([]string{
            fmt.Sprintf("%.8f", sizeMB),
            fmt.Sprintf("%d", durationValue.Nanoseconds()),
            fmt.Sprintf("%d", durationPointer.Nanoseconds()),
        })

        // Print status to monitor progress
        fmt.Printf("Completed benchmark for size: %.8f MB \n", sizeMB)
    }

Benchmarking process:

  1. Run the test: Each struct size was tested for both pass-by-value and pass-by-pointer methods.
  2. Record the results: Execution time was recorded for each size and method.
  3. Visualize the results: The results were plotted to observe how the performance scales as the struct size increases using python3 & matplotlib.

Analyzing the Results

benchmark graph

Image description

Why Does This Happen?

Takeaways:

Go provides the flexibility to choose based on your performance needs, so understanding the trade-offs is crucial

System Specifications: The benchmarks were run on: OS: Pop!_OS 22.04 LTS CPU: 13th Gen Intel i5-1340P RAM: 16GB GPU: Intel Device a7a0

This benchmark helped me understand the importance of choosing between pass-by-value and pass-by-pointer in Go functions, especially when dealing with large structs!


Here's the entire benchmark's github repo: go-pointer-vs-value-benchmark

Find me on twitter(x): @anubhavs_twt