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)
}
- generated csv file looks like this:
Benchmarking process:
- Run the test: Each struct size was tested for both pass-by-value and pass-by-pointer methods.
- Record the results: Execution time was recorded for each size and method.
-
Visualize the results: The results were plotted to observe how the performance scales as the struct size increases using python3 & matplotlib.
- 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.
- Pass by Value: Noticeable slowdown as size increases, indicating the inefficiency due to copying larger data.
- 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: go-pointer-vs-value-benchmark
Find me on twitter(x): @anubhavs_twt
Top comments (0)