Dig101: dig more, simplified more and know more
Я полагаю, что после нескольких предыдущих статей вы также обнаружили, что структуры почти везде.
String, slice и map — все это использует структуру под капотом.
Сегодня мы сосредоточимся на выравнивании памяти struct,
Понимание этого очень поможет лучше использовать структуру и понять реализацию некоторых библиотек исходного кода.
Перед этим уточним несколько терминов, чтобы облегчить последующий анализ.
- слово
единица данных, используемая для представления ее природы, также называемаяmachine word. Слово — это фиксированная длина, которую компьютер использует для одновременной обработки транзакций.
- длина слова
Количество битов в слове (т.е. длина слова).
Современные компьютеры обычно имеют длину слова 16, 32 или 64 бита. (Длина слова типичной N-битной системы равнаN/8байт. )
Большинство регистров в компьютере имеют размер в одно слово. Единицей передачи данных между ЦП и памятью также обычно является одно слово. Кроме того, адрес, используемый для обозначения места хранения в памяти, часто имеет длину слова.
См. в ВикипедииХарактер
Почему 0x01 выровнен?
Проще говоря, процессор операционной системы обращается к памяти не байт за байтом, а в соответствии с длинами слов, такими как 2, 4 и 8.
Таким образом, когда процессор считывает данные из подсистемы памяти в регистры или записывает данные регистров в память, длина передаваемых данных обычно равна длине слова.
Например, гранулярность доступа 32-битной системы составляет 4 байта (байта), а 64-битной системы — 8 байтов.
Когда длина запрашиваемых данныхnбайт и адрес данныхnВыравнивание байтов, тогда операционная система может найти данные за раз, что будет более эффективно. Никаких дополнительных операций, таких как многократное чтение, обработка операций выравнивания и т. д., не требуется.
Выравнивание структуры данных 0x02
Давайте сначала посмотрим на определение размера базовой структуры данных.
гарантия размера
Например, официальная документация Go.size and alignment guaranteesпоказано:
| type | size in bytes |
|---|---|
| byte, uint8, int8 | 1 |
| uint16, int16 | 2 |
| uint32, int32, float32 | 4 |
| uint64, int64, float64, complex64 | 8 |
| complex128 | 16 |
A struct or array type has size zero if it contains no fields (or elements, respectively) that have a size greater than zero. Two distinct zero-size variables may have the same address in memory.
struct{}и[0]T{}размера 0; разные переменные размера 0 могут указывать на один и тот же адрес блока.
гарантия выравнивания
- For a variable x of any type: unsafe.Alignof(x) is at least 1.
- For a variable x of struct type: unsafe.Alignof(x) is the largest of all the values unsafe.Alignof(x.f) for each field f of x, but at least 1.
- For a variable x of array type: unsafe.Alignof(x) is the same as the alignment of a variable of the array's element type.
Выравнивание этого описания, переведенного в соответствующий тип, приведено в следующей таблице.
Ссылаться наgo101-memory layout
| type | alignment guarantee |
|---|---|
| bool, byte, uint8, int8 | 1 |
| uint16, int16 | 2 |
| uint32, int32 | 4 |
| float32, complex64 | 4 |
| arrays | по его элементам (element) типовое решение |
| structs | по своим полям (field) типовое решение |
| other types | машинное слово(machine word)размер |
Размер машинного слова здесь составляет 4 байта в 32-битной системе и 8 байтов в 64-битной системе.
Проверьте код ниже:
type T1 struct {
a [2]int8
b int64
c int16
}
type T2 struct {
a [2]int8
c int16
b int64
}
fmt.Printf("arrange fields to reduce size:\n"+
"T1 align: %d, size: %d\n"+
"T2 align: %d, size: %d\n",
unsafe.Alignof(T1{}), unsafe.Sizeof(T1{}),
unsafe.Alignof(T2{}), unsafe.Sizeof(T2{}))
/*
output:
arrange fields to reduce size:
T1 align: 8, size: 24
T2 align: 8, size: 16
*/
Взяв в качестве примера 64-битную систему, анализ выглядит следующим образом:
T1,T2Самое большое внутреннее полеint64, размер 8 байт, выравнивание определяется по машинному слову, а это 8 байт под 64 бит, поэтому будет выравниваться по 8 байт
T1.aРазмер 2 байта, и байты заполняются для выравнивания (следующие поля уже выровнены, поэтому они заполняются напрямую)
T1.bРазмер 8 байт, выровненный
T1.cРазмер 2 байта, заполните 6 байт для выравнивания (сзади нет поля, поэтому заполните его напрямую)
Общий размер8+8+8=24
T2генерал-лейтенантcПосле продвижения,aиcОбщий размер 4 байта, а выравнивание производится заполнением 4 байта
Общий размер8+8=16
так,Разумное перераспределение полей может уменьшить заполнение и сделать поля структуры более тесно расположенными.
0x03 выравнивание полей нулевого размера
поля нулевого размера (zero sized field)Относится кstruct{},
Размер равен 0. Его не нужно выравнивать, когда он используется как поле, но когда он используется как последнее поле структуры (final field) необходимо согласовать.
Зачем?
Потому что, если есть указатель на этоfinal zero field, возвращаемый адрес будет находиться вне структуры (то есть указывать на другую память),
Если этот указатель останется живым и не освободит соответствующую память, возникнет проблема утечки памяти (память не освобождается из-за освобождения структуры)
Итак, Go предназначен для такого родаfinal zero fieldТакже сделал прокладку, чтобы сделать выравнивание.
Проверка кода выглядит следующим образом:
type T1 struct {
a struct{}
x int64
}
type T2 struct {
x int64
a struct{}
}
a1 := T1{}
a2 := T2{}
fmt.Printf("zero size struct{} in field:\n"+
"T1 (not as final field) size: %d\n"+
"T2 (as final field) size: %d\n",
// 8
unsafe.Sizeof(a1),
// 64位:16;32位:12
unsafe.Sizeof(a2))
выравнивание адреса памяти 0x04
отспецификация небезопасного пакета, имеются следующие инструкции:
Computer architectures may require memory addresses to be aligned; that is, for addresses of a variable to be a multiple of a factor, the variable's type's alignment. The function Alignof takes an expression denoting a variable of any type and returns the alignment of the (type of the) variable in bytes. For a variable x:
uintptr(unsafe.Pointer(&x)) % unsafe.Alignof(x) == 0
Грубо говоря, если типtГарантия выравнивания дляn, то типtАдрес каждого значения во время выполнения должен бытьnкратно .
это вsync.WaitGroupЕсть хорошие приложения:
type WaitGroup struct {
noCopy noCopy
state1 [3]uint32
}
// state returns pointers to the state and sema fields stored within wg.state1.
func (wg *WaitGroup) state() (statep *uint64, semap *uint32) {
// 判定地址是否8位对齐
if uintptr(unsafe.Pointer(&wg.state1))%8 == 0 {
// 前8bytes做uint64指针statep,后4bytes做sema
return (*uint64)(unsafe.Pointer(&wg.state1)), &wg.state1[2]
} else {
// 后8bytes做uint64指针statep,前4bytes做sema
return (*uint64)(unsafe.Pointer(&wg.state1[1])), &wg.state1[0]
}
}
Дело в томWaitGroup.state1это поле,
мы знаемuint64Выравнивание определяется машинным словом, 32-битная система — 4 байта, 64-битная система — 8 байт.
Чтобы убедиться, что в 32-битных системах также можно вернуть 64-битное выравнивание (8bytes aligned) указатель (*uint64)
использовать его с умом[3]uint32.
Сначала на 64-битных системах и 32-битные системы,uint32Гарантированно быть выровненным в 4бабах
которыйstate1Адрес 4Н:uintptr(unsafe.Pointer(&wg.state1))%4 == 0
Чтобы обеспечить 8-битное выравнивание, нам нужно только оценитьstate1Является ли адрес кратным 8
- Если это так (N — четное число), то первые 8 байтов выравниваются по 64-битному принципу.
- В противном случае (N нечетно), то последние 8 байтов выравниваются по 64-битному принципу.
А оставшиеся 4 байта можно отдатьsemaИспользование в полевых условиях без потери памяти
Но почему 64-битное выравнивание должно быть гарантировано в 32-битной системе?uint64Как насчет указателей?
Ответ заключается в том, чтобы гарантировать атомарный доступ к 64-битным выровненным 64-битным словам и в 32-битных системах. Давайте подробнее рассмотрим ниже.
0x05 64-битная гарантия безопасного доступа
существуетatomic-bugупоминается в:
On x86-32, the 64-bit functions use instructions unavailable before the Pentium MMX. On non-Linux ARM, the 64-bit functions use instructions unavailable before the ARMv6k core.
On ARM, x86-32, and 32-bit MIPS, it is the caller's responsibility to arrange for 64-bit alignment of 64-bit words accessed atomically. The first word in a variable or in an allocated struct, array, or slice can be relied upon to be 64-bit aligned.
Грубо говоря, если вы хотите атомарно оперировать 64-битными словами (такими как uint64) в 32-битной системе, вызывающая сторона должна убедиться, что ее адрес данных выровнен по 64-битам, иначе атомарный доступ будет ненормальным.
Зачем?
Зачем гарантировать
Вот простой анализ:
также взятьuint64Например, размер равен 8 байтам, что выравнивается по 4 байтам в 32-разрядных системах и по 8 байтам в 64-разрядных системах.
В 64-битной системе 8 байтов точно соответствуют размеру слова, поэтому атомарные доступы могут выполняться за один раз, не затрагиваясь и не прерываясь другими операциями.
И 32-битная система, выравнивание по 4 байта, длина слова тоже 4 байта, может появитьсяuint64Данные распределяются вдва блока данных, для завершения доступа требуются две операции.
Если можно изменить другие операции между двумя операциями, атомарность не может быть гарантирована.
Такой доступ также небезопасен.
с этой точки зренияissue-6404Также упоминается в:
This is because the int64 is not aligned following the bool. It is 32-bit aligned but not 64-bit aligned, because we're on a 32-bit system so it's really just two 32-bit values side by side.
как гарантировать
The first word in a variable or in an allocated struct, array, or slice can be relied upon to be 64-bit aligned.
Первое 64-битное слово в переменной или открытой структуре, массиве и значении слайса может считаться выровненным по 8 байтам.
в этом предложенииоткрытьЭто означает, что оно создается с помощью объявления, make и new, а это означает, что 64-битное слово, созданное таким образом, может быть гарантированно выровнено по 64-битам.
Но это все же относительно абстрактно, разберем на примере.
64-битные слова, к которым можно безопасно получить атомарный доступ в 32-битной системе:
- Само 64-битное слово
// GOARCH=386 go run types/struct/struct.go
var c0 int64
fmt.Println("64位字本身:",
atomic.AddInt64(&c0, 1))
- 64-битный массив слов, срез
c1 := [5]int64{}
fmt.Println("64位字数组、切片:",
atomic.AddInt64(&c1[:][0], 1))
- Первое поле структуры представляет собой выровненное 64-битное слово и соседние 64-битные слова.
c2 := struct {
val int64 // pos 0
val2 int64 // pos 8
valid bool // pos 16
}{}
fmt.Println("结构体首字段为对齐的64位字及相邻的64位字:",
atomic.AddInt64(&c2.val, 1),
atomic.AddInt64(&c2.val2, 1))
- Первое поле в структуре — вложенная структура, а ее первый элемент — 64-битное слово.
type T struct {
val2 int64
_ int16
}
c3 := struct {
val T
valid bool
}{}
fmt.Println("结构体中首字段为嵌套结构体,且其首元素为64位字:",
atomic.AddInt64(&c3.val.val2, 1))
- Структуры добавляют заполнение, чтобы сделать выровненные 64-битные слова
c4 := struct {
val int64 // pos 0
valid bool // pos 8
// 或者 _ uint32
// 使32位系统上多填充 4bytes
_ [4]byte // pos 9
val2 int64 // pos 16
}{}
fmt.Println("结构体增加填充使对齐的64位字:",
atomic.AddInt64(&c4.val2, 1))
- 64-битный фрагмент слова в структуре
c5 := struct {
val int64
valid bool
val2 []int64
}{val2: []int64{0}}
fmt.Println("结构体中64位字切片:",
atomic.AddInt64(&c5.val2[0], 1))
The first element in slices of 64-bit elements will be correctly aligned
Срез здесь представляет собой указатель, а данные указывают на 64-битный массив слов, открытый в основной куче, такой как c1.
Если его заменить массивом, он будет паниковать,
Потому что выравнивание массива структуры все равно зависит от полей в структуре
c51 := struct {
val int64
valid bool
val2 [3]int64
}{val2: [3]int64{0}}
// will panic
atomic.AddInt64(&c51.val2[0], 1)
- 64-битный указатель слова в структуре
c6 := struct {
val int64
valid bool
val2 *int64
}{val2: new(int64)}
fmt.Println("结构体中64位字指针:",
atomic.AddInt64(c6.val2, 1))
изменить на замок
Это немного сложно?На самом деле не очень удобно обеспечивать 64-битные слова с выравниванием по 8 байтам на 32-битной системе
Конечно, вы можете не использовать атомарный доступ (atomic), используйте блокировку(mutex), чтобы избежать этой ошибки
c := struct{
val int16
val2 int64
}{}
var mu sync.Mutex
mu.Lock()
c.val2 += 1
mu.Unlock()
Наконец, собственно, передWaitGroup.state1Таким образом, чтобы обеспечить выравнивание 8 байт, остается немного непроанализированного:
Вот почему атомарный доступ к состоянию не используется напрямуюuint64, и использовать упомянутую выше гарантию выравнивания 64-битного слова?
Думаю, вы тоже обдумывали ответ: еслиWaitGroupПри вложении в другие структуры могут возникнуть проблемы, если он не размещен в верхней части структуры, что ограничивает его использование.
в заключении:
- Выравнивание памяти позволяет ЦП более эффективно обращаться к данным в памяти.
- Выравнивание структуры: если гарантия выравнивания типа t равна n, то выравнивание каждого значения типа t равноадресДолжно быть кратно n во время выполнения.
которыйuintptr(unsafe.Pointer(&x)) % unsafe.Alignof(x) == 0
- Если в структуре слишком много полей, вы можете попытаться переупорядочить их, чтобы сделать поля более близкими друг к другу и уменьшить потери памяти.
- Чтобы избежать полей нулевого размера в качестве последнего поля структуры, будут потери памяти.
- Атомарный доступ к 64-битным словам в 32-битных системах должен быть гарантированно выровнен по 8 байтам; конечно, если в этом нет необходимости, используйте блокировку (
mutex) понятнее и проще
Рекомендуется набор инструментов:dominikh/go-tools, три инструмента structlayout, structlayout-optimize и structlayout-pretty более интересны.
См. код этой статьиNewbMiao/Dig101-Go
See more: Нужно ли выравнивание памяти в Golang?
Первый публичный аккаунт статьи:newbmiao(Добро пожаловать, чтобы обратить внимание и получать своевременные обновления)
Рекомендуемое чтение:Серия Dig101-Go