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
}
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))
}
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]
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
}
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))
}
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]
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!!
Top comments (13)
🔥🔥🔥
thanks
Great way to explain this topic. congrats
Thanks
Thanks a lot of your detailed explanation. Helpful alot
Glad to know!!
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?
Because, the current version of the standard Go compiler, the alignment guarantees of other types may be either
4
or8
, depends on different build target architectures. This is also true forgccgo
.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...
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 🤔
Great write-up!. Thanks so much for sharing your findings so clearly :)
Thanks @motaman
Here's a great ready-made solution:github.com/t34-dev/go-field-alignment