Go: Slice and Memory Management

ℹ️ This article is based on Go 1.13.
The slices in Go are very convenient and quite interesting internally. There are many documentations only about it, including this Go blog, which explains the fundamental concepts of the slices, including its internal representation, very well. This article will focus more on the memory management that surrounds the slices. Let’s start with the manipulation of the elements with their copy and deletion.
Copy
Go allows developers to copy a slice thanks to the built-in function copy
. However, the function append
that appends values to it could also be used to copy a slice. Here are two examples:
func main() {
a := []int{4, 2, 1}
b := make([]int, len(a))
copy(b, a)
c := append([]int{}, a...)
}
The newly created slices b
and c
now an underlying array with the same values.
Those two functions can be used in other scenarios, such as the deletion of an element or a part of the slice. Here is an example of removing the second element of the slice thanks to the copy
function:
func main() {
a := []int{4, 2, 1}
copy(a[1:], a[2:])
a = a[:len(a)-1]
}
Here is the same behavior with the append
function:
func main() {
a := []int{4, 2, 1}
a = append(a[:1], a[2:]...)
}
That behavior is actually possible since the Go specification guarantees the result whether there is a memory overlap in the argument or not:
The built-in functions
append
andcopy
assist in common slice operations. For both functions, the result is independent of whether the memory referenced by the arguments overlaps.
The generated asm from the first example shows the underlying function that manages the memory copy:
[...]
0x00a3 00163 (main.go:8) CALL runtime.memmove(SB)
[...]
0x00f4 00244 (main.go:10) CALL runtime.memmove(SB)
This implementation relies on the memmove
function that works with overlaps. Here is an example with a slice of byte to illustrate the overlap issue:
func main() {
a := []byte("hello")
copy(a[2:], a)
}
Copying the memory forward from the first byte of the source to the last one would overlap with the destination:

memmove
solves this issue with the ability to copy backward:

This solves the overlapping issue and satisfies the guarantee offered by the specification.
The implementation in assembly of memmove
is actually more complex than that. If it handles forward and backward copy, it does not always need to loop and can handle the copy with very few instruction.
Reset
Reusing a slice means clearing it first. Go provides a compiler optimization in order to clear a slice fast. Here is an example of clearing a slice of integers:
func main() {
a := []int{4, 2, 1}
for i := range a {
a[i] = 0
}
}
Looping on the slice to clear elements one by one could be cumbersome. To solve that issue, Go 1.5 came with an optimization able to recognize this kind of loop and replace it by a call to a memory cleaning function. This can be confirmed from the assembly code:
0x0047 00071 (main.go:6) CALL runtime.memclrNoHeapPointers(SB)
The suffix NoHeapPointers
refers to slices not containing any pointers. The same function exists for slices containing pointers; the compiler will call memclrHasPointer
in this case.
That optimization speeds up the cleaning significantly. Here is a benchmark with slices of 6, 16, 64 and 256 elements:

Allocation and copy
When allocating a slice, Go first allocates the memory and zeroes it through the functions memclr*
we have seen previously.

However, if the next instruction is a copy of an existing slice, the clear operation would have just wasted time:

For this reason, a CL (#146719) aims to optimize this phase and remove the memory clearing part in case of immediate copy:

This will now speed up the allocation. Here is a benchmark:
func BenchmarkAllocAndCopy(b *testing.B) {
a := make([]int, 250)
for k, _ := range a {
a[k] = k*2
}
b.ResetTimer()
for i := 0;i < b.N; i++ {
b := make([]int, len(a))
copy(b, a)
}
}
Here is the result of the benchmark:
name old time/op new time/op delta
AllocAndCopy500-8 531ns ± 1% 502ns ± 4% -5.44%
AllocAndCopy250-8 284ns ± 1% 272ns ± 5% -4.10%
AllocAndCopy50-8 78.5ns ± 3% 72.1ns ± 1% -8.16%
AllocAndCopy5-8 30.6ns ± 1% 26.1ns ± 1% -14.80%
The only condition to trigger this special allocation with copy is to make sure both instructions — make
and copy
— follow each other.
This compiler optimization is not yet merged and should land in the next versions of Go, making the language smarter when it comes to copying slices.