Skip to main content

Memory allocation differences between *[]T and []*T in Go

Go

Preface

Recently I had a discussion with my coworker about the performance differences between *[]T and []*T in Go.

As a result, I wrote this article to summarize our thoughts.

Go Slices internals

Before beginning the performance testing, let’s review the structure of the Go slices mentioned below.

A slice is a descriptor of an array segment. It consists of a pointer to the array, the length of the segment, and its capacity (the maximum length of the segment).

slice-struct
Our variable s, created earlier by make([]byte, 5), is structured like this:
slice-1
The length is the number of elements referred to by the slice. The capacity is the number of elements in the underlying array (beginning at the element referred to by the slice pointer).

The memory allocation for the slice

In Go, the memory size of a pointer depends on the underlying architecture.

On most modern architectures, including 64-bit systems, a pointer occupies 8 bytes of memory.

Therefore, based on the structure of the mentioned slices, we can determine that the slice occupies 24 bytes (since the int type occupies 8 bytes).

package main

import (
	"fmt"
	"unsafe"
)

type point struct {
	x int
	y int
}

func main() {
	fmt.Printf("Size of struct: %d bytes\n", unsafe.Sizeof(point{}))
	fmt.Printf("Size of pointer struct: %d bytes\n", unsafe.Sizeof(&point{}))
	fmt.Printf("Size of slice: %d bytes\n", unsafe.Sizeof([]point{}))
	fmt.Printf("Size of pointer slice with struct: %d bytes\n", unsafe.Sizeof(&[]point{}))
}

/*
    Size of struct: 16 bytes
    Size of pointer struct: 8 bytes
    Size of slice: 24 bytes
    Size of pointer slice with struct: 8 bytes
*/

Go Slices benchmark testing

I’ve prepared two functions for showing the memory allowcation differences between *[]T and []*T.

func returnSliceWithPointers() []*point
func returnSliceWithStructs() *[]point

The following benchmark code will show you that wich functions will run faster and use memory less.

package main

import "testing"

type point struct {
	x int
	y int
}

const length int = 10000

func returnSliceWithPointers() []*point {
	points := make([]*point, length)
	for i := 0; i < length; i++ {
		points[i] = &point{}
	}

	return points
}

func returnSliceWithStructs() *[]point {
	points := make([]point, length)
	for i := 0; i < length; i++ {
		points[i] = point{}
	}

	return &points
}

func Benchmark_SliceWithPointers(b *testing.B) {
	for i := 0; i < b.N; i++ {
		returnSliceWithPointers()
	}
}

func Benchmark_SliceWithStructs(b *testing.B) {
	for i := 0; i < b.N; i++ {
		returnSliceWithStructs()
	}
}

After executing the command below, we can obtain the benchmark result.

go test -bench . -benchmem -benchtime 10000x

goos: darwin
goarch: arm64
pkg: slice-benchmark
Benchmark_SliceWithPointers-8              10000            119199 ns/op          241920 B/op      10001 allocs/op
Benchmark_SliceWithStructs-8               10000              9913 ns/op          163840 B/op          1 allocs/op
PASS
ok      slice-benchmark 1.663s

We can observe that using *[]T(slice with the structs) will result in lower memory usage and faster execution.

Memory usage

Simple calculation of memory usage for *[]T and []*T.

*[]T 24 bytes + 8 bytes + 10000 * 16 bytes = 160032 bytes

[]*T 24 bytes + 10000 * (16 bytes + 8 bytes) = 240024 bytes

Real world scenario

In real world scenarios, we acknowledge that *[]T is more efficient than []*T.

However, when using a pointer to a slice of type *[]T, an additional dereference operation is required, namely the * operator, to access both the slice itself and the elements within it. This increases code complexity and readability.

package main

import "fmt"

type point struct {
	x int
	y int
}

func returnSliceWithStructs() *[]point {
	points := []point{
		{1, 2},
		{2, 4},
	}

	return &points
}

func main() {
	fmt.Printf("The lengthe of the slice is %d", len((*returnSliceWithStructs())))
}

// The lengthe of the slice is 2

To improve code readability, our team prefers to use []*T instead of *[]T, even if it means sacrificing some performance.

Reference