Anubhav Samanta

Sep 30, 2024 • 4 min read

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

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

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

Pass by Value_: Copies the entire struct when passed to a function, causing performance issues for large structs.

Pass by Pointer_: Passes a reference (pointer) to the struct, avoiding the overhead of copying large data.

Performance Loss: Pass by value starts to slow down noticeably when struct sizes exceed 10MB due to the memory cost of copying.

Optimal for Large Structs: Pass by pointer is more efficient and stable, especially for data sizes greater than 10MB.

Key Insight: For small structs, pass by value is fine. For larger structs, use pass by pointer to save time and memory.


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_ ?

- A struct in Go is a way to group different types of data together similar to C.

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?

- The benchmark involves gradually increasing the struct size from 1 byte to 1GB while comparing the time(nanoseconds) it takes to pass these structs by value and by pointer and increasing the struct size 2x with each iteration.

- For each size, I recorded the execution time using Go’s high-precision timers and exported them to a CSV for visualization.

// 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)

		fmt.Printf("Completed benchmark for size: %.8f MB \n", sizeMB)

	}

- generated csv file looks like this:

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

From the graph, it’s clear that passing by value starts to slow down significantly as struct sizes increase, especially beyond 10MB. Passing by pointer remains relatively constant, with only a slight increase in execution time as struct size grows.

1. Pass by Value: Noticeable slowdown as size increases, indicating the inefficiency due to copying larger data.

2. Pass by Pointer: More consistent and efficient, with minimal slowdown, making it ideal for larger structs.

Why Does This Happen?

The difference stems from Go's memory management. When you pass a large struct by value, the entire struct is copied, which increases memory usage and processing time. With a pointer, Go only passes the memory address, avoiding the need to copy large amounts of data.


Takeaways:

  • For small structs, pass by value is fine and might even be preferable for simplicity.

  • For larger structs, pass by pointer is more efficient and can save significant time and memory.

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: https://github.com/anubhavgh023/go-pointer-vs-value-benchmark)

Find me on twitter(x): [@anubhavs_twt](https://twitter.com/anubhavs_twt)

Join Anubhav on Peerlist!

Join amazing folks like Anubhav and thousands of other people in tech.

Create Profile

Join with Anubhav’s personal invite link.

0

8

0