What's the performance difference between passing large structs by value vs. by pointer?
Quick Answer
Passing a struct by value copies every field of the struct onto the stack (or into a register, for small structs) at the call site, which is cheap for small structs but grows linearly with struct size — a struct with many fields or large arrays can make each function call meaningfully more expensive just from the copy. Passing by pointer only copies a single machine word (the pointer itself), regardless of how large the pointed-to struct is, but it introduces an extra indirection on every field access, and it can force the struct to escape to the heap (see escape analysis) if the pointer outlives the calling function's stack frame. For small structs (a few machine words or less), value passing is typically just as fast or faster due to cache locality and avoiding a heap allocation; for large structs, or when mutation is needed, pointer passing usually wins.
Detailed Answer
There's no universal answer here — the right choice depends on struct size, mutation needs, and whether the value needs to escape.
Small struct: value passing is often fine or even faster
type Point struct{ X, Y int } // 16 bytes on a 64-bit system
func distance(a, b Point) float64 {
dx := a.X - b.X
dy := a.Y - b.Y
return math.Sqrt(float64(dx*dx + dy*dy))
}
Copying 16 bytes onto the stack is essentially free, likely cheaper than the indirection cost of dereferencing a pointer, and it avoids any risk of the struct escaping to the heap.
Large struct: pointer passing usually wins
type LargeConfig struct {
Settings [200]string // large, fixed-size backing data
// ... many more fields ...
}
func process(cfg *LargeConfig) { // one pointer-sized copy, regardless of struct size
// ...
}
Passing LargeConfig by value would copy the entire struct, potentially kilobytes, on every single call. Passing *LargeConfig copies only a pointer, no matter how large the struct grows.
The mutation consideration, independent of size
func resetX(p Point) {
p.X = 0 // only modifies the local copy — caller's Point is untouched
}
func resetX(p *Point) {
p.X = 0 // modifies the caller's actual Point
}
If a function needs to mutate the caller's data, it must take a pointer regardless of size — this is a correctness requirement, not just a performance one.
The practical guidance
Don't guess: benchmark the specific case if performance genuinely matters, since cache locality and escape analysis can make the "obvious" choice wrong in specific situations. As a starting heuristic, structs under roughly 3-4 machine words are usually fine by value; larger structs, or any struct that needs mutation, should generally use a pointer.