The original connection
This article will cover type alignment and size assurance in Go(memory allocated). It is important to understand how Go ensures that the size of struct types is correctly evaluated and that the library sync/ Atomic 64-bit functions are properly used.
Go is part of the C family of programming languages, and many of the concepts discussed in this article are derived from C.
Go
Ensures type alignment in
To make full use of CPU instructions and obtain optimal performance, the memory block (start) address allocated for a value of a given type must be a multiple of an integer N, which is called the address alignment guarantee for the value of that type, or simply the alignment guarantee for that type. We can also say that the address of the addressable value of this type is guaranteed to be n-byte aligned.
In fact, each type has two guarantees for it, one when it’s a field of another type (struct) and one when it’s a declaration of a variable, the type of an array element, and so on. I call the former the field alignment guarantee for types and the latter the general alignment guarantee for types.
For a type, we can call unsafe.algnof (t) for its general alignment guarantee, where t is the unfielded value of type T, or unsafe.algnof (x.t) for its field alignment guarantee, where x is the value of a struct and a is the field value of type T.
Function calls in the standard library Unsafe are always at compile time.
At runtime, for values of type T, we can call reflect.typeof (T).align () to get a general alignment guarantee for type T, or reflect.typeof (T).fieldalign () to get a general alignment guarantee for fields of type T.
For the current official Go compiler (Version 1.16), the field alignment guarantee and the general alignment guarantee for types are always the same. For the GCCGO compiler, this statement is incorrect.
The Go specification mentions only a few guarantees of type alignment:
Ensure the following minimum alignment attributes 1. For any type of variable x: unsafe.alignof (x) has a value of at least 1.2. For variables of struct type 😡 for each field unsafe.alignof (x.f) has a value of at least 1, while unsafe.alignof (x) takes the largest of all values 3. For variables of array type: unsafe.alignof (x) has the same value as the alignment of each element type variable in the array
All Go specifications do not specify exactly alignment guarantees for any type. It just specifies some minimum requirements.
For the same compiler, the exact type alignment guarantee may vary between data structures and between compiler versions. For standard Go compilers that list the current version (1.16), alignment guarantees are listed here.
type alignment guarantee
------ ------
bool, uint8, int8 1
uint16, int16 2
uint32, int32 4
float32, complex64 4
arrays depend on element types
structs depend on field types
other types size of a native word
Copy the code
Here, the size of the native word (or machine word) is 4 bytes in 32-bit architecture and 8 bytes in 64-bit architecture.
This means that for the current version of the standard Go compiler, other types of alignment guarantees may be 4 or 8, depending on the target compilation architecture. The same applies to GCCGO.
In summary, we don’t need to worry about alignment of value addresses during Go programming unless we want to optimize memory consumption or use the sync/atomic 64-bit functions. Please read the following two sections for details.
Type size and structure padding
The Go specification only establishes the following type size guarantees:
type size in bytes
------ ------
uint8, int8 1
uint16, int16 2
uint32, int32, float32 4
uint64, int64 8
float64, complex64 8
complex128 16
uint, int implementation-specific,
generally 4 on 32-bit
architectures, and 8 on
64-bit architectures.
uintptr implementation-specific,
large enough to store
the uninterpreted bits
of a pointer value.
Copy the code
The Go specification does not make size guarantees for other types of values. A complete list of other different type sizes determined by the standard Go compiler is included in the value copy costs.
Standard Go compilers (including GCCGO) will ensure that the value size of a type is a multiple of the guaranteed alignment of that type.
To satisfy the type alignment guarantees mentioned earlier, the Go compiler may fill in a few bytes between the fields of struct values. This makes it possible that the size of a struct type value may not simply be the sum of all the fields of that type.
Here is an example of how bytes can be populated between fields of a struct. We’ve learned:
- Alignment guaranteed and built in
int8
Types are all 1 byte in size - Alignment guaranteed and built in
int16
The size of each type is 2 bytes - The built-in
int64
The size of the type is 8 bytes,int64
Type alignment is guaranteed to be 4 bytes in 32-bit architectures and 8 bytes in 64-bit architectures T1
The type andT2
The alignment guarantee for types is the maximum alignment guarantee for their respective fields, for example,int64
Field alignment is guaranteed. So their alignment is guaranteed to be 4 bytes in 32-bit architecture and 8 bytes in 64-bit architecture.T1
The type andT2
The size of the types must be a guaranteed multiple of their respective alignment, for example, in 32-bit architectures4N
In 64-bit architectures it is8N
.
type T1 struct {
a int8
// On 64-bit architectures, to make field b
// 8-byte aligned, 7 bytes need to be padded
// here. On 32-bit architectures, to make
// field b 4-byte aligned, 3 bytes need to be
// padded here.
b int64
c int16
// To make the size of type T1 be a multiple
// of the alignment guarantee of T1, on 64-bit
// architectures, 6 bytes need to be padded
// here, and on 32-bit architectures, 2 bytes
// need to be padded here.
}
// The size of T1 is 24 (= 1 + 7 + 8 + 2 + 6)
// bytes on 64-bit architectures and is 16
// (= 1 + 3 + 8 + 2 + 2) on 32-bit architectures.
type T2 struct {
a int8
// To make field c 2-byte aligned, one byte
// needs to be padded here on both 64-bit
// and 32-bit architectures.
c int16
// On 64-bit architectures, to make field b
// 8-byte aligned, 4 bytes need to be padded
// here. On 32-bit architectures, field b is
// already 4-byte aligned, so no bytes need
// to be padded here.
b int64
}
// The size of T2 is 16 (= 1 + 1 + 2 + 4 + 8)
// bytes on 64-bit architectures, and is 12
// (= 1 + 1 + 2 + 8) on 32-bit architectures.
Copy the code
Although T1 and T2 have the same set of fields, they are different in size. One interesting thing about the standard Go compiler is that sometimes a field of size 0 can affect structure padding. Read about this issue in the unofficial GO FAQ for details.
Alignment requirements for 64-bit word atom operations
A 64-bit word indicates a value of the underlying type INT64 or uint64.
This article atomic manipulation refers to the fact that 64-bit atomic manipulation on 64-bit words requires that the addresses of 64-bit words must be 8-byte aligned. This is not a problem for the 64-bit architectures supported by the standard Go compiler, because 64-bit words are always 8-byte aligned on these 64-bit architectures.
However, on 32-bit architectures, the standard Go compiler makes an alignment guarantee of just 4 bytes. 64-bit atomic operations on 64-bit words that are not 8-byte aligned will cause panic at run time. To make matters worse, 64-bit atomic functions were not supported on very old CPU architectures.
At the end of the sync/atomic document, it says:
On x86-32 machines, instructions for 64-bit functions are not available before using Pentium MMX. On non-Linux ARMS, instructions for 64-bit functions are not available prior to the ARMv6k kernel. On ARM and x86-32 machines, the caller’s chide is to arrange 64-bit alignment for 64-bit words accessed atomically. The first word in a variable or allocated structure, array, or slice can rely on 64-bit alignment.
So, because of those two principles, things are not so bad.
- The very old CPU architecture is not the current mainstream CPU architecture. If a program needs to synchronize 64-bit words on these architectures, there are other synchronization techniques to come to the rescue.
- On other, less ancient 32-bit architectures, there are ways to ensure that some 64-bit words rely on 64-bit alignment.
These methods are described as depending on a 64-bit aligned variable or the first (64-bit) word in an allocated struct, array, or slice. What do I mean by distribution? We can think of an assigned value as a declared variable, a value returned by the built-in make function, or a reference to a value returned by the built-in new method. If a slice comes from an allocated array and the first element of the slice is the first element of the array, then the value of the slice is also considered an allocated value.
The description of which 64-bit words can rely on 64-bit alignment on 32-bit architectures is somewhat understated. There are many more 64-bit words that can rely on 8-byte alignment. In fact, if a number or slice whose first element type is a 64-bit word can rely on 64-bit alignment, then all elements of the array/slice can also be accessed by atoms. Making a simple and clear description of all the 64-bit words that can rely on 64-bit alignment on a 32-bit architecture is a bit tricky and verbose, so the official documentation is conservative.
Here is an example of some 64-bit words that can be accessed securely or unsecurely in both 64-bit and 32-bit architectures.
type (
T1 struct {
v uint64
}
T2 struct{_int16
x T1
y *T1
}
T3 struct{_int16
x [6]int64
y *[6]int64})var a int64 // a is safe
var b T1 // b.v is safe
var c [6]int64 // c[0] is safe
var d T2 // d.x.v is unsafe
var e T3 // e.x[0] is unsafe
func f(a) {
var f int64 // f is safe
var g = []int64{5: 0} // g[0] is safe
var h = e.x[:] // h[0] is unsafe
// Here, d.y.v and e.y[0] are both safe,
// for *d.y and *e.y are both allocated.
d.y = new(T1)
e.y = &[6]int64{}
_, _, _ = f, g, h
}
// In fact, all elements in c, g and e.y.v are
// safe to be accessed atomically, though Go
// official documentation never makes the guarantees.
Copy the code
If a 64-bit word field of a struct type (usually the first) is to be accessed by atoms in the code, we should always use the assigned struct type value to ensure that on 32-bit architectures, the fields accessed by atoms always depend on 8-byte alignment. When this struct is used for a field of another struct type, we should arrange that field as the first field of the other struct type, and always use the assigned value of the other struct type.
Sometimes, if we are not sure whether a 64-bit word is atomically accessible, we can use a value of type [15]byte to determine the 64-bit word runtime address. For example,
package mylib
import (
"unsafe"
"sync/atomic"
)
type Counter struct {
x [15]byte // instead of "x uint64"
}
func (c *Counter) xAddr(a) *uint64 {
// The return must be 8-byte aligned.
return (*uint64)(unsafe.Pointer(
(uintptr(unsafe.Pointer(&c.x)) + 7) /8*8))}func (c *Counter) Add(delta uint64) {
p := c.xAddr()
atomic.AddUint64(p, delta)
}
func (c *Counter) Value(a) uint64 {
return atomic.LoadUint64(c.xAddr())
}
Copy the code
With this solution, the Counter type can be freely and securely embedded into other user types, even 32-bit architectures. The downside of this solution is that it wastes 7 bytes per value of type Counter, and it uses unsafe Pointers. The Sync library uses a [3] UNIT32 value in place of this solution. This problem assumes that the alignment of type UNIT32 is guaranteed to be a multiple of 4 bytes. This assumption is true for standard Go compilers and gCCGO compilers, and then it may be wrong for other third-party Go compilers.
Russ Cox proposed that addresses for 64-bit words should always be 8-byte aligned, whether on 64-bit or 32-bit architectures, to make Go programming easier. Currently (Go 1.16) this proposal has not been adopted.