Resolve the memory layout of struct

Resolve the memory layout of struct

In normal development process, we often use map[string]struct {} to implement a Set. The reason for using struct {} is that struct {} does not occupy memory space. Why does an empty struct not occupy memory space? What is the memory footprint for a custom struct?

Size of struct

Struct is like an object in java, it is a contiguous space in memory. In java, the reference of an object is actually the starting point of the object's memory address in heap memory. When accessing the Nth object, it is the location of the access (starting address + address offset of the Nth object). Similarly in go, the size of go struct is determined by the property in struct. For example:

type A struct {
    x int8
    y int8
    z int8
}
var a A
fmt.Println(unsafe.Sizeof(a)) // 3

You can see the output: 3, which means that an a object takes up 3 bytes of memory, and the memory structure is as follows:

Memory alignment of struct

Let's modify the field type a little and see the results again

type B struct {
    x int8
    y int32
    z int8
}
var b B
fmt.Println(unsafe.Sizeof(b)) // 12

Here the output will be 8, int32 takes 4 bytes as explained above, plus two 1-byte int8, which should output 6. This leads to the memory alignment mechanism in go:
In computers, when a CPU accesses memory, it does not access a byte at a time, but a word. The word length of a word is determined by the number of bits of the cpu. For example, a 32-bit CPU has a 32-bit word length, which is 4 bytes, and a 64-bit CPU has a 64-bit word length, which is 8 bytes. To reduce the number of times the CPU interacts with memory when accessing objects, go Memory alignment is performed at compile time according to certain rules, which requires two bus cycles to prevent access to an object's properties.
If the B object above is 6 bytes, the memory structure is as follows:

Looking at the figure above, at this time the cpu word length is 1 byte, then accessing the y object requires the cpu to interact with memory twice
So after memory alignment in go, the memory structure is as follows:

The yellow part is the memory alignment part, so when accessing property y, the cpu only needs to read the memory once.

Alignment rule for struct

The Sizeof function with the built-in unsafe package is used to get the size of a variable, and the Alignof function with the built-in unsafe package is used to get the alignment coefficient of a variable, for example:

var a A
fmt.Println(unsafe.Alignof(a.x)) // 1
fmt.Println(unsafe.Alignof(a.y)) // 4
fmt.Println(unsafe.Alignof(a.z)) // 1
fmt.Println(unsafe.Alignof(a)) // 4

The rules are as follows:

  • For any type of variable x, unsafe.Alignof(x) is at least 1;
  • For struct type variable x, calculate unsafe for each field F of X. Alignof (x.f), unsafe.Alignof(x) is equal to the maximum value;
  • For variable X of type array, unsafe.Alignof(x) is equal to the alignment factor of the element types that make up the array;
    The alignment factor must be an integer multiple of the size of the object, that is, in go, for any object a, the equation unsafe.Sizeof(a) = X * unsafe.Aligof(a) must be established
    So with this equation, object B above, X should be at least 2, but because unsafe.Sizeof(b) = 12, so X equals 3, so we can optimize the size of object B to 8 by simply adjusting the field order
type C struct {
    x int8
    z int8
    y int32
}
var c C
fmt.Println(unsafe.Sizeof(b)) // 8

The memory structure at this time is as follows:

So this example shows that a reasonable arrangement of the position of attributes in an object can reduce the size of the object's memory.

Nested struct

On this basis, you can infer the unsafe of nested structures and slices, arrays. Sizeof, unsafe.Aligniof size, to practice

type D struct {
	x int
	a A
	b B
	arr [5]A
	sli []A
}

var d D
fmt.Println(unsafe.Alignof(d)) // 8
fmt.Println(unsafe.Sizeof(d)) // 64
fmt.Println("---------")
fmt.Println(unsafe.Alignof(d.x)) // 8
fmt.Println(unsafe.Sizeof(d.x)) // 8
fmt.Println("---------")
fmt.Println(unsafe.Alignof(d.a)) // 1
fmt.Println(unsafe.Sizeof(d.a)) // 3
fmt.Println("---------")
fmt.Println(unsafe.Alignof(d.b)) // 4
fmt.Println(unsafe.Sizeof(d.b)) // 12
fmt.Println("---------")
fmt.Println(unsafe.Alignof(d.arr)) // 1
fmt.Println(unsafe.Sizeof(d.arr)) // 15
fmt.Println("---------")
fmt.Println(unsafe.Alignof(d.sli)) // 8
fmt.Println(unsafe.Sizeof(d.sli)) // 24

Where a + b does the memory alignment, filling two bytes, arr does the memory alignment, filling one byte
Why is a slice 24 bytes here, which is related to the structure of the slice, in runtime/slice. Slice is defined in go:

len and cap take up 8 bytes on 64-bit cpu, array is a pointer to the underlying array and also takes up 8 bytes, so it adds up to 24 bytes.

Keywords: goland

Added by AMCH on Sat, 08 Jan 2022 20:03:05 +0200