Structure size optimization in Golang (alignment/padding). More effective memory layout (linters).

Roman Romadin
ITNEXT
Published in
4 min readFeb 13, 2021

--

Photo by Harrison Broadbent on Unsplash

Today I would like to tell you about the alignment of structures in Golang.

The capacity of the machines is growing every year. And our computing power allows us to perform increasingly demanding tasks. We strive to apply new and new optimization techniques and may forget the old ones, which include the alignment of the fields of structures in memory. In this article, we will use the experience of the past years.

This is good when the program is written simply and logically and it has good flexibility. Go programs are fast programs. And the speed of the program on Go is comparable to a C++program.

In this article, we will look at how you can reduce the memory consumption for storing data during the operation of the program itself — optimize structures (struct). And we will measure how much memory consumption has changed after optimizing the structure in our program. This can be done by “alignment”.

Alignment speeds up memory access by generating code that requires one instruction each to read and write a memory location. Without alignment, we may face a situation where the processor will have to use two or more instructions to access data located between addresses that are multiples of the size of the machine word.

Play with struct size

First, let’s create a structure without fields `type Foo struct {} ‘ and see how much memory it takes and how much the pointer to it takes:

https://play.golang.org/p/Inbi7TzL7P5

Running our program in the console, we will get:

08

Now let’s add a couple of fields:

The result will be 8 and 8.

So we can make sure that the pointer has a constant size and depends on the length of the machine word on your machine-8 bytes.

Now let’s play with the number and order of the fields:

The result will be 12.

In the C-language, the term “machine word” is used 4 or 8 bytes (depending on the bit depth of the machine — 32/64).

And in Go “required alignment” is used. Its value is equal to the size of the memory required for the largest field in the structure.

That is, if there are only int32 fields in the structure, then the “required alignment” will be 4 bytes. And if there are both int32 and int64 - then 8 bytes.

And so the example above is equivalent to:

Add another field “at the end” of the `ddd bool ‘ structure.

And if we don’t reach the “required alignment” size of the consecutive fields, the poplars will “collapse” due to offsets.

Here we will also see — 12. Because the optimization worked here. Due to the presence of offsets.

We can see that the `c bool` and `dd bool ‘fields have “collapsed” to a single “required alignment” for this structure — 4 bytes (due to the presence of int32).

Answer: 24. Why? Because now the “required alignment” for this structure is 8 bytes.

From this, we can already proceed to optimize our structure-apply alignment. We can swap the structure fields that take

up less than one machine word in memory. Try to arrange them in such an order that the adjacent fields occupy no more than one machine word.

Now try to guess why this structure is not optimal in terms of alignment?

The `unsafe ‘ construction.Sizeof(Foo{})` printed: 12.

This can be optimized like this:

then, `unsafe.Sizeof(Foo {})` — printed already: 8.

By the way, except `unsafe.Sizeof`, there are several other similar functions in the `unsafe ‘ package.

`unsafe.Offsetof` — returns the number of bytes between the start of the struct and the start of the field.

Let me explain it in code (https://play.golang.org/p/LG7azYGHX8M):

A pretty useful tool can be obtained from the `unsafe.Alignof` function — which shows the ”required alignment“ calculated for the given structure.

As an example, we can calculate the “required alignment” for our structures from the code above:

Tools optimization

This is all very well, but tedious. And it may be irrational. Especially if we have hundreds or even thousands of such structures on the project.

In fact, there are several tools that will help us find places (structures) to optimize.

Let’s look at them:

Use aligncheck for our structs — `Foo` and `Bar`:

And indeed — we can optimize the structure of ‘Foo` and then it will be exactly ‘Bar’.

Use maligned for our structs — `Foo` and `Bar`:

Excellent!

It is also very convenient to use ‘aligned’ in the top linter for Go — ‘golang clint’.

To do this, you just need to enable ‘maligned’ in the ‘golangci-lint’ settings.

Example, from the configuration file`. golangci.example. yml`

Read more here: https://golangci-lint.run/usage/configuration/

This is very convenient and will allow us to run a check for the possibility of optimization not only in manual mode-locally on the developer’s computer.

But also embedded in our CI / CD.

So, we found out what alignment is and understood the mechanisms of its work.

We learned a little about how to optimize the consumed memory allocated for our structures.

We learned how to measure the difference before and after optimization.

And we were able to find tools that will allow you to automate the search for structures for optimization (aligncheck, maligned and golangci-lint).

Go is comparable to a C++program.

--

--