Golang Project Structure

Tutorials, tips and tricks for writing and structuring code in Go (with additional content for other programming languages)

How to Make Go Structs More Efficient

Language

  • unknown

by

It may be surprising to learn that two Go structs can contain exactly the same fields and yet one can require more — or less — memory than the other.

Since we generally want to ensure that we never use more memory than required, we are going to look at a technique called structure packing and how it can be applied to the Go programming language.

Creating an Example Struct

Let’s look at the following example code, which just defines two custom types, one of which is a struct containing three fields:

package main

type City uint8

const (
	NewYork City = iota
	London
	Paris
	Mumbai
)

type Person struct {
	currentResidence City
	uniqueID         int64
	passportNumber   int16
}

As you can see, I have declared some example City constants, so you can see how the type could be used to represent four different locations around the world. The iota keyword just ensures that each of the constants has a unique numeric value.

Now let’s declare a Person variable and see how much memory it requires:

package main

import (
	"fmt"
	"unsafe"
)

func main() {
	me := Person{
		currentResidence: London,
		uniqueID:         9248511308,
		passportNumber:   10564,
	}

	fmt.Printf(
		"My Person struct uses %d bytes.\n",
		unsafe.Sizeof(me),
	)
}

When I run this code, it tells me that our Person struct uses a total of 24 bytes in order to hold its data.

And yet how can that be so?

Let’s think it through step-by-step: the uniqueID field is a 64-bit number, so it requires eight bytes of storage, the passportNumber field is a 16-bit number, so it only requires two bytes of storage, and the currentResidence field is a custom City type that’s stored as an 8-bit number, only requiring one byte of storage.

(Note that we haven’t taken into account whether a field’s numeric value is signed or unsigned, because an unsigned integer takes up exactly the same amount of space in memory as its corresponding signed integer, which is why the maximum value of an unsigned integer is always greater than the maximum value of a signed integer, because a signed integer will already have used one bit just to store the number’s sign.)

If we add up the amount of storage needed to store the three fields separately, we only get to eleven bytes (because 8 + 2 + 1 == 11).

However, as we’ve just seen, the Person struct seemed to be using more than twice as much memory as that!

Thinking About Data Alignment

When we were measuring the size of our data, we talked in terms of bytes.

However, a computer’s CPU doesn’t read data in bytes — instead, it reads data in words.

A word is equivalent to eight bytes on a 64-bit system.

On an older 32-bit system, which some of you may still be using, a word is equivalent to four bytes.

It’s important to remember that a machine will tend to read data in multiples of words, and this is what explains why our Person struct used 24 bytes.

It was using a single word for each field, even though only the uniqueID field (which was a 64-bit integer) made full use of the word.

My machine has a 64-bit processor, so if each word is eight bytes, then it becomes easy to understand how we get to the number twenty-four (because 8 * 3 == 24).

A small yellow car is shown in a huge parking space. Either side of it are two huge white buses.
A parking space is a set size, but smaller cars can park in it, just like big buses can. Similarly, small fields can fit into a word of memory in the same way that bigger fields can.

The passportNumber field, which was a 16-bit integer and so only required two bytes of storage, would still access a whole word from memory but only use the first quarter of it, leaving the last three quarters unused.

Likewise, the currentResidence field, which is a custom City type that is ultimately equivalent to an 8-bit integer, only really requires a single byte of storage and yet a whole word is still accessed from memory and the last seven bytes are not used at all.

Reducing the Amount of Memory Our Struct Requires

There’s not much we can do to reduce the memory footprint of the uniqueID field, since it must use a whole word, otherwise it wouldn’t be able to represent a 64-bit number.

However, did you notice how it would be possible to fit the passportNumber and currentResidence fields into a single word, since they only use up three bytes between them?

In fact, we can force the Go compiler to share one word of memory for these two fields simply by reordering them when we define our Person struct.

When we defined it in the previous example, we placed the uniqueID field in between the other two fields, which meant that the smaller fields couldn’t share memory, because they were separated by a whole word between them.

If, on the other hand, we were to put the passportNumber and currentResidence fields together as either the first or last fields in the struct, then we suddenly allow them to share memory, such as in the example below:

package main 

type Person struct {
	uniqueID         int64
	passportNumber   int16
	currentResidence City // remember that this is really a uint8
}

When I run the main function that we wrote earlier with this slightly modified Person definition, I can see that the same data is now stored on my machine using only 16 bytes of memory.

This is still more than the 11 bytes of memory that we calculated the fields should need in total, but since we can only use a multiple of 8 bytes, which is a 64-bit word, that’s as small as we can possibly make the struct, since 8 bytes wouldn’t be enough.

This ordering of the Person struct is certainly more efficient than the one we began with, and, indeed, it is now as efficient as can be.

Reordering the fields of a struct in order to reduce its memory usage is exactly what was meant by the term structure packing that I used at the very beginning of this blog post.

Using the Fieldalignment Tool

The Go team has developed an official tool called fieldalignment that will search for structs in your code that contain fields that aren’t ordered as efficiently as they otherwise could be.

The tool can be installed by running the following command:

go install golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment@latest

Then we can run the program on a Go file or package by giving it the appropriate path like so:

fieldalignment main.go

Running the fieldalignment tool on my initial example code would produce the following output:

main.go:12:13: struct of size 24 could be 16

Note that if you include the --fix flag when running the program, as shown below, it will automatically edit your Go file so that the struct fields are in the most efficient order:

fieldalignment --fix main.go

Using the tool like this means that you never need to worry about manually reordering the fields in structs in order to ensure the most efficient use of memory.

Leave a Reply

Your email address will not be published. Required fields are marked *