DEV Community

Cover image for Golang Writing memory efficient and CPU optimized Go Structs
Satyajiijt Roy
Satyajiijt Roy

Posted on • Updated on

Golang Writing memory efficient and CPU optimized Go Structs

A struct is a typed collection of fields, useful for grouping data into records. This allows all the data relating to one entity to be neatly encapsulated in one lightweight type definition, behavior can then be implemented by defining functions on the struct type.

This blog I will try to explain how we can efficiently write struct in terms of Memory Usages and CPU Cycles.

Let’s consider this struct below, definition of terraform resource type for some weird use-case I have:

type TerraformResource struct {
  Cloud                string                       // 16 bytes
  Name                 string                       // 16 bytes
  HaveDSL              bool                         //  1 byte
  PluginVersion        string                       // 16 bytes
  IsVersionControlled  bool                         //  1 byte
  TerraformVersion     string                       // 16 bytes
  ModuleVersionMajor   int32                        //  4 bytes
}
Enter fullscreen mode Exit fullscreen mode

Let see how much memory allocation is required for the TerraformResource struct using code below:

package main

import "fmt"
import "unsafe"

type TerraformResource struct {
  Cloud                string                       // 16 bytes
  Name                 string                       // 16 bytes
  HaveDSL              bool                         //  1 byte
  PluginVersion        string                       // 16 bytes
  IsVersionControlled  bool                         //  1 byte
  TerraformVersion     string                       // 16 bytes
  ModuleVersionMajor   int32                        //  4 bytes
}

func main() {
    var d TerraformResource
    d.Cloud = "aws"
    d.Name = "ec2"
    d.HaveDSL = true
    d.PluginVersion = "3.64"
    d.TerraformVersion = "1.1"
    d.ModuleVersionMajor = 1
    d.IsVersionControlled = true
    fmt.Println("==============================================================")
    fmt.Printf("Total Memory Usage StructType:d %T => [%d]\n", d, unsafe.Sizeof(d))
    fmt.Println("==============================================================")
    fmt.Printf("Cloud Field StructType:d.Cloud %T => [%d]\n", d.Cloud, unsafe.Sizeof(d.Cloud))
    fmt.Printf("Name Field StructType:d.Name %T => [%d]\n", d.Name, unsafe.Sizeof(d.Name))
    fmt.Printf("HaveDSL Field StructType:d.HaveDSL %T => [%d]\n", d.HaveDSL, unsafe.Sizeof(d.HaveDSL))
    fmt.Printf("PluginVersion Field StructType:d.PluginVersion %T => [%d]\n", d.PluginVersion, unsafe.Sizeof(d.PluginVersion))
    fmt.Printf("ModuleVersionMajor Field StructType:d.IsVersionControlled %T => [%d]\n", d.IsVersionControlled, unsafe.Sizeof(d.IsVersionControlled))
    fmt.Printf("TerraformVersion Field StructType:d.TerraformVersion %T => [%d]\n", d.TerraformVersion, unsafe.Sizeof(d.TerraformVersion))
    fmt.Printf("ModuleVersionMajor Field StructType:d.ModuleVersionMajor %T => [%d]\n", d.ModuleVersionMajor, unsafe.Sizeof(d.ModuleVersionMajor))  
}
Enter fullscreen mode Exit fullscreen mode

Output

==============================================================
Total Memory Usage StructType:d main.TerraformResource => [88]
==============================================================
Cloud Field StructType:d.Cloud string => [16]
Name Field StructType:d.Name string => [16]
HaveDSL Field StructType:d.HaveDSL bool => [1]
PluginVersion Field StructType:d.PluginVersion string => [16]
ModuleVersionMajor Field StructType:d.IsVersionControlled bool => [1]
TerraformVersion Field StructType:d.TerraformVersion string => [16]
ModuleVersionMajor Field StructType:d.ModuleVersionMajor int32 => [4]
Enter fullscreen mode Exit fullscreen mode

So total memory allocation required for the TerraformResource struct is 88 bytes. This is how the memory allocation will look like for TerraformResource type

But how come 88 bytes, 16 +16 + 1 + 16 + 1+ 16 + 4 = 70 bytes, where is this additional 18 bytes coming from ?

When it comes to memory allocation for structs, they are always allocated contiguous, byte-aligned blocks of memory, and fields are allocated and stored in the order that they are defined. The concept of byte-alignment in this context means that the contiguous blocks of memory are aligned at offsets equal to the platforms word size.

We can clearly see that TerraformResource.HaveDSL , TerraformResource.isVersionControlled and TerraformResource.ModuleVersionMajor are only occupying 1 Byte, 1 Byte and 4 Bytes respectively. Rest of the space is fill with empty pad bytes.

So going back to same math

Allocation bytes = 16 bytes + 16 bytes + 1 byte + 16 bytes + 1 byte + 16 byte + 4 bytes

Empty Pad bytes = 7 bytes + 7 bytes + 4 bytes = 18 bytes

Total bytes = Allocation bytes + Empty Pad bytes = 70 bytes + 18 bytes = 88 bytes

So, How do we fix this ? With proper data structure alignment what if we redefine our struct like this

type TerraformResource struct {
  Cloud                string                       // 16 bytes
  Name                 string                       // 16 bytes
  PluginVersion        string                       // 16 bytes
  TerraformVersion     string                       // 16 bytes
  ModuleVersionMajor   int32                        //  4 bytes
  HaveDSL              bool                         //  1 byte
  IsVersionControlled  bool                         //  1 byte
}
Enter fullscreen mode Exit fullscreen mode

Run the same Code with optimized struct

package main

import "fmt"
import "unsafe"

type TerraformResource struct {
  Cloud                string                       // 16 bytes
  Name                 string                       // 16 bytes
  PluginVersion        string                       // 16 bytes
  TerraformVersion     string                       // 16 bytes
  ModuleVersionMajor   int32                        //  4 bytes
  HaveDSL              bool                         //  1 byte
  IsVersionControlled  bool                         //  1 byte
}

func main() {
    var d TerraformResource
    d.Cloud = "aws"
    d.Name = "ec2"
    d.HaveDSL = true
    d.PluginVersion = "3.64"
    d.TerraformVersion = "1.1"
    d.ModuleVersionMajor = 1
    d.IsVersionControlled = true
    fmt.Println("==============================================================")
    fmt.Printf("Total Memory Usage StructType:d %T => [%d]\n", d, unsafe.Sizeof(d))
    fmt.Println("==============================================================")
    fmt.Printf("Cloud Field StructType:d.Cloud %T => [%d]\n", d.Cloud, unsafe.Sizeof(d.Cloud))
    fmt.Printf("Name Field StructType:d.Name %T => [%d]\n", d.Name, unsafe.Sizeof(d.Name))
    fmt.Printf("HaveDSL Field StructType:d.HaveDSL %T => [%d]\n", d.HaveDSL, unsafe.Sizeof(d.HaveDSL))
    fmt.Printf("PluginVersion Field StructType:d.PluginVersion %T => [%d]\n", d.PluginVersion, unsafe.Sizeof(d.PluginVersion))
    fmt.Printf("ModuleVersionMajor Field StructType:d.IsVersionControlled %T => [%d]\n", d.IsVersionControlled, unsafe.Sizeof(d.IsVersionControlled))
    fmt.Printf("TerraformVersion Field StructType:d.TerraformVersion %T => [%d]\n", d.TerraformVersion, unsafe.Sizeof(d.TerraformVersion))
    fmt.Printf("ModuleVersionMajor Field StructType:d.ModuleVersionMajor %T => [%d]\n", d.ModuleVersionMajor, unsafe.Sizeof(d.ModuleVersionMajor))
}
Enter fullscreen mode Exit fullscreen mode

Output

go run golang-struct-memory-allocation-optimized.go

==============================================================
Total Memory Usage StructType:d main.TerraformResource => [72]
==============================================================
Cloud Field StructType:d.Cloud string => [16]
Name Field StructType:d.Name string => [16]
HaveDSL Field StructType:d.HaveDSL bool => [1]
PluginVersion Field StructType:d.PluginVersion string => [16]
ModuleVersionMajor Field StructType:d.IsVersionControlled bool => [1]
TerraformVersion Field StructType:d.TerraformVersion string => [16]
ModuleVersionMajor Field StructType:d.ModuleVersionMajor int32 => [4]
Enter fullscreen mode Exit fullscreen mode

Now total memory allocation for the TerraformResource type is 72 bytes. Let’s see how the memory alignments looks likes

Just by doing proper data structure alignment for the struct elements we were able to reduce the memory footprint from 88 bytes to 72 bytes....Sweet!!

Let’s check the math

Allocation bytes = 16 bytes + 16 bytes + 16 bytes + 16 bytes + 4 bytes + 1 byte + 1 bytes = 70 bytes

Empty Pad bytes = 2 bytes

Total bytes = Allocation bytes + Empty Pad bytes = 70 bytes + 2 bytes = 72 bytes

Proper data structure alignment not only helps us use memory efficiently but also with CPU Read Cycles….How ?

CPU Reads memory in words which is 4 bytes on a 32-bit, 8 bytes on a 64-bit systems. Now our first declaration of struct type TerraformResource will take 11 Words for CPU to read everything

However the optimized struct will only take 9 Words as shown below

By defining out struct properly data structured aligned we were able to use memory allocation efficiently and made the struct fast and efficient in terms of CPU Reads as well.

This is just a small example, think about a large struct with 20 or 30 fields with different types. Thoughtful alignment of data structure really pays off … 🤩

Hope this blog was able to shed some light on struct internals, their memory allocations and required CPU reads cycles. Hope this helps!!

Happy Coding!!

Top comments (12)

Collapse
 
ankush981 profile image
Ankush Thakur

🔥🔥🔥

Collapse
 
isudarsan profile image
SUDARSAN

Thanks a lot of your detailed explanation. Helpful alot

Collapse
 
deadlock profile image
Satyajiijt Roy

Glad to know!!

Collapse
 
gusga profile image
Gustavo Giménez

Great way to explain this topic. congrats

Collapse
 
deadlock profile image
Satyajiijt Roy

Thanks

Collapse
 
arpitvarshneya profile image
Arpit Varshneya • Edited

Didn't get why empty pad of 7 bytes, 7 bytes and 4 bytes respectively were added to make the sum of 8 bytes by complier? Why 8?

Collapse
 
deadlock profile image
Satyajiijt Roy

Because, the current version of the standard Go compiler, the alignment guarantees of other types may be either 4 or 8, depends on different build target architectures. This is also true for gccgo.

Collapse
 
skysolderone profile image
Skysolderone

thanks

Collapse
 
fracasula profile image
Francesco Casula

Interesting article. A linter might be a good idea in this case. It could make writing memory optimized structs quite easy. It can be integrated in CI as well instead of having to always rely on code reviews etc...

Collapse
 
drsensor profile image
૮༼⚆︿⚆༽つ

Thanks for the write up! TIL, go compiler doesn't auto align the struct. I wonder if I should fit my struct to 128bit, hoping that the compiler will optimize some of r/w fields operation with SIMD instructions 🤔

Collapse
 
motaman profile image
MB

Great write-up!. Thanks so much for sharing your findings so clearly :)

Collapse
 
deadlock profile image
Satyajiijt Roy

Thanks @motaman