Выравнивание полей и эффективное распределение памяти — 08.03.2021
Производительность растёт, а старые методы оптимизации порой забываются. Один из них — выравнивание полей структур в памяти. Программы на Go быстры и сопоставимы с C++, но потребление памяти во время работы всё ещё можно уменьшить, правильно расположив поля.
Выравнивание ускоряет доступ к памяти: процессору достаточно одной инструкции для чтения или записи ячейки. Без выравнивания данные могут оказаться между адресами, кратными размеру машинного слова — и тогда процессору потребуется две инструкции или больше.
Начнём с пустой структуры и посмотрим, сколько занимает она сама и указатель на неё:
type Foo struct {}
func main() {
x := &Foo{}
y := Foo{}
fmt.Println(unsafe.Sizeof(x)) // 8
fmt.Println(unsafe.Sizeof(y)) // 0
}
Пустая структура занимает 0 байт, а указатель — 8: его размер постоянен и равен длине машинного слова (здесь 8 байт).
Теперь добавим пару полей:
type Foo struct {
aaa bool
bbb int32
}
// Sizeof: 8
А теперь поиграем с количеством и порядком полей:
type Foo struct {
aaa bool // 1 по умолчанию, но с заполнением — 4
bbb int32 // 4 — максимум в этой структуре
ccc bool // 1 по умолчанию, но с заполнением — 4
}
// Sizeof: 12
В языке C используется термин «машинное слово» размером 4 или 8 байт в зависимости от разрядности процессора. В Go используется «требуемое выравнивание» — оно равно размеру памяти, необходимой самому большому полю структуры.
Если все поля — int32, требуемое выравнивание составит 4 байта.
Если в структуре есть int64, требуемое выравнивание вырастает до 8 байт.
Если поля не «дотягивают» до размера требуемого выравнивания, они сворачиваются за счёт смещений:
type Foo struct {
aaa bool // 1
bbb int32 // 4 (макс.)
ccc bool // 1
ddd bool // 1
}
// Sizeof: 12 — ccc и ddd «свернулись» в одно выравнивание (4 байта)
int32 на int64 — и требуемое выравнивание становится 8 байт, а размер той же структуры вырастает до 24.Главный приём: расположить поля так, чтобы соседние занимали не более одного машинного слова. Вот неоптимальный вариант:
type Foo struct {
aaa [2]bool // 2 байта
bbb int32 // 4 байта
ccc [2]bool // 2 байта
}
// Sizeof: 12
Достаточно поставить два коротких поля рядом — и структура ужимается:
type Foo struct {
aaa [2]bool // 2 байта
ccc [2]bool // 2 байта
bbb int32 // 4 байта
}
// Sizeof: 8
Кроме unsafe.Sizeof, в пакете есть ещё несколько полезных функций.
unsafe.Sizeof — размерunsafe.Offsetof — смещение поляunsafe.Alignof — выравниваниеСравним смещения полей у оптимальной и неоптимальной структур:
type Foo struct {
aaa [2]bool // смещение: 0
bbb int32 // смещение: 4
ccc [2]bool // смещение: 8
}
type Bar struct {
aaa [2]bool // смещение: 0
ccc [2]bool // смещение: 2
bbb int32 // смещение: 4
}
// смещения Foo: aaa: 0; bbb: 4; ccc: 8
// смещения Bar: aaa: 0; ccc: 2; bbb: 4
А функция unsafe.Alignof покажет требуемое выравнивание:
fmt.Printf("%+v\n", unsafe.Alignof(Foo{})) // 4
fmt.Printf("%+v\n", unsafe.Alignof(Bar{})) // 4
Расставлять поля вручную утомительно, а порой и нерационально — особенно когда структур в проекте сотни или тысячи. На помощь приходят линтеры.
Foo размера 12 может быть размером 8.suggest-new предложит более эффективную раскладку.Оба удобно подключаются к golangci-lint — превосходному средству контроля качества кода. Например, из конфигурационного файла:
linters-settings:
maligned:
# выводит более эффективную схему структуры; false по умолчанию
suggest-new: true
| Понятие | Суть |
|---|---|
| Выравнивание | Расположение полей по адресам, кратным размеру слова, — для доступа за одну инструкцию |
| Требуемое выравнивание | Равно размеру самого большого поля структуры (4 или 8 байт) |
| Приём оптимизации | Ставить короткие поля рядом, чтобы соседние занимали не более машинного слова |
| Инструменты | unsafe.Sizeof/Offsetof/Alignof, aligncheck, maligned, golangci-lint |