Go Value Copy Costs

Value copying happens frequently in Go programming. Values assignments, argument passing, channel value send and receive operations are all value copying involved. This article will talk about value copy costs in Go.

Value Sizes

The size of a value means how many bytes (the direct part of) the value will occupy in memory. The indirect underlying parts of a value don't contribute to the size of the value.

In Go, if the types of two values belong to the same kind, and the type kind is not string kind, interface kind, array kind and struct kind, then the sizes of the two value are always equal.

In fact, for the standard Go compiler/runtime, the sizes of two string values are also always equal. The same relation is for the sizes of two interface values.

For the standard Go compiler/runtime, values of the same type always have the same value size. So, often, we call the size of a value as the size of the type of the value.

The size of an array type depends on the element type size and the length of the array type. The array type size is the product of the size of the array element type and the array length.

The size of a struct type depends on all of its fields. For there may be some padding bytes between two adjacent fields, the struct type size is not smaller than (and often larger than) the sum of the respective type sizes of the struct fields.

The following table lists the value sizes of all kinds of types. In the table, one word means one native word, which is 4 bytes on 32bits OSes and 8 bytes on 64bits OSes.

Type Value Size For The Standard Go Compiler (v1.11) Requirement By Go Specification
bool 1 byte not specified
int8, uint8 (byte) 1 byte 1 byte
int16, uint16 2 bytes 2 bytes
int32 (rune), uint32, float32 4 bytes 4 bytes
int64, uint64, float64, complex64 8 bytes 8 bytes
complex128 16 bytes 16 bytes
int, uint 1 word architecture dependent, either 4 or 8 bytes
uintptr 1 word large enough to store the uninterpreted bits of a pointer value
string 2 words not specified
pointer 1 word not specified
slice 3 words not specified
map 1 word not specified
channel 1 word not specified
function 1 word not specified
interface 2 words not specified
struct the sum of sizes of all fields + number of padding bytes a struct type has size zero if it contains no fields that have a size greater than zero
array (element value size) * (array length) an array type has size zero if its element type has zero size

Value Copy Costs

Generally speaking, the cost to copy a value is proportional to the size of the value. However, value sizes are not the only factor to calculate value copying. Different CPU architectures may specially optimize value copying for values with specific sizes. In practice, we can view values with sizes smaller not larger than four native words as small-size values. The costs of copying small-size values are small.

For the standard Go compiler, except values of large-size struct and array types, most values in Go are small-size values.

To avoid large value copy costs in argument passing and channel value send and receive operations, we should try to avoid using large-size struct and array types as function and method parameter types (including method receiver types) and avoid using large-size struct and array types as channel element types. We can use pointer types whose base types are large-size struct and array types instead for such scenarios.

One the other hand, we should also consider the negative effect for garbage collections caused by too many pointers are used at run time. So whether large-size struct and array types or their corresponding pointer types should be used relies on specific scenarios.

Generally, we shouldn't use pointer types whose base types are slice types, map types, channel maps, function types, string types and interface types. The costs of copying values of these assumed base types are small.

We should also try to avoid using the two-iteration-variable forms to iterate array and slice elements if the element types are large-size types, for each element value will be copy to the second iteration variable in the iteration process.

The following is an example to benchmark different ways of slice element iterations.

package main

import "testing"

type S struct{a, b, c, d, e int64}
var sX, sY, sZ = make([]S, 1000), make([]S, 1000), make([]S, 1000)
var sumX, sumY, sumZ int64

func Benchmark_Loop(b *testing.B) {
	for i := 0; i < b.N; i++ {
		sumX = 0
		for j := 0; j < len(sX); j++ {
			sumX += sX[j].a
		}
	}
}

func Benchmark_Range_OneIterVar(b *testing.B) {
	for i := 0; i < b.N; i++ {
		sumZ = 0
		for j := range sY {
			sumZ += sY[j].a
		}
	}
}

func Benchmark_Range_TwoIterVar(b *testing.B) {
	for i := 0; i < b.N; i++ {
		sumY = 0
		for _, v := range sY {
			sumY += v.a
		}
	}
}
Run the benchmarks in the directory of the test file, we will get a result similar to:
Benchmark_Loop-4                500000   3228 ns/op
Benchmark_Range_OneIterVar-4    500000   3203 ns/op
Benchmark_Range_TwoIterVars-4   200000   6616 ns/op

We can find that the efficiency of the two-iteration-variable form is much lower than the other two.


The Go 101 project is hosted on both github and gitlab. Welcome to improve Go 101 articles by submitting corrections for all kinds of mistakes, such as typos, grammar errors, wording inaccuracies, description flaws, code bugs and broken links.

Support Go 101 by playing Tapir's games. Cryptocurrency donations are also welcome:
Bitcoin: 1xucQbv5jujFPPwhyg395ri5yV71hx9g9
Ethereum: 0x5dc4aa2c2bbfaadae373dadcfca11b3358912212