Go: Slice and Memory Management
By: Vincent Blanchon
Tags: Compiler, Golang, Go

Illustration created for “A Journey With Go”, made from the original Go Gopher, created by Renee French.

ℹ️ 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 and copy 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:

Forward copy

memmove solves this issue with the ability to copy backward:

Backward copy

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.