вступление
Вы когда-нибудь думали о том, как улучшить свой код или как добиться успеха на собеседовании?
«выравнивание» структур — это процесс оптимизации, в котором мы можем уменьшить потребление памяти правильно построенной структурой.
Благодаря этому сгенерированный машинный код использует одну инструкцию для чтения/записи пространства памяти, без этого их могло бы быть 2 и более.
type foo struct {} x := new(foo) y := foo{} fmt.Println(unsafe.Sizeof(x)) // 8 bytes fmt.Println(unsafe.Sizeof(y)) // 0 bytes type foo struct { aaa bool bbb int32 } x := new(foo) y := foo{} fmt.Println(unsafe.Sizeof(x)) // 8 bytes fmt.Println(unsafe.Sizeof(y)) // 8 bytes
Здесь у нас есть структура без полей и структура с полями. Один экземпляр с указателем, а другой без.
- Указатель, как мы его видим, имеет фиксированный размер, зависящий от так называемых «машинных слов» — в C от архитектуры системы зависит, сколько бит он имеет 32/64. Для 32-битной системы константа будет 4 байта, а для 64-битной системы — 8 байт.
- Чем больше полей, тем больше размер структуры
Краткая информация о процессоре
Представьте, что у нас есть 64-битный процессор. Это означает, что процессор может передавать 64-битные данные за такт.
Cцикл блокировки – это время, необходимое процессору для обработки информации.
Мы можем сказать, что 32-разрядный ЦП может преобразовывать 4-байтовые данные за 1 цикл, а 64-разрядный ЦП может преобразовывать 8-байтовые данные за 1 цикл (32-разрядный = 4 байта, 64-разрядный = 8). байт)
Еще примеры:
type foo struct { aaa bool // 1 bytes bbb int32 // 4 bytes ссс bool // 1 bytes } x := new(foo) y := foo{} fmt.Println(unsafe.Sizeof(x)) // 8 bytes fmt.Println(unsafe.Sizeof(y)) // 12 bytes
Давайте представим, как может выглядеть чтение данных из процессора в терминах циклов. В Go смещением в памяти будет наибольший размер поля, в нашем случае 4 байта (32 бита). Если бы мы использовали переменную большего размера, например. uint64 или строка, максимальная длина — 8 байт. строка будет разделена на 2 отдельных цикла.
x — тратится память, это целых 6 байт. Наши данные заняли 6 байт
type foo struct { bbb int32 // 4 bytes aaa bool // 1 bytes ссс bool // 1 bytes } x := new(foo) y := foo{} fmt.Println(unsafe.Sizeof(x)) // 8 bytes fmt.Println(unsafe.Sizeof(y)) // 8 bytes
Нам удалось сэкономить место на 4 байта, иметь на 1 процессорный цикл меньше и, таким образом, всего 2 байта свободного места.
Что еще
GoLang предоставляет больше инструментов из пакета `unsafe`.
`unsafe.Offsetof` — возвращает количество байтов между началом структуры и началом поля.
`unsafe.Alignof` — показывает «требуемое выравнивание», рассчитанное для данной структуры.
type Employer struct { IsPublic bool Status bool Image float32 Age int64 Name string } type BadEmployer struct { IsPublic bool Age int64 Status bool Image float32 Name string } func main() { x := Employer{} y := BadEmployer{} fmt.Printf("X: offsets of fields: IsPublic: %+v; Status: %+v Image: %+v Age: %+v Name: %+v\n", unsafe.Offsetof(x.IsPublic), unsafe.Offsetof(x.Status), unsafe.Offsetof(x.Image), unsafe.Offsetof(x.Age), unsafe.Offsetof(x.Name)) fmt.Printf("Y: offsets of fields: IsPublic: %+v; Status: %+v Image: %+v Age: %+v Name: %+v\n", unsafe.Offsetof(y.IsPublic), unsafe.Offsetof(y.Status), unsafe.Offsetof(y.Image), unsafe.Offsetof(y.Age), unsafe.Offsetof(y.Name)) fmt.Printf("X: Sizeof: %v\n", unsafe.Sizeof(x)) fmt.Printf("y: Sizeof: %v", unsafe.Sizeof(y)) } // Output: // X: offsets of fields: IsPublic: 0; Status: 1 Image: 4 Age: 8 Name: 16 // Y: offsets of fields: IsPublic: 0; Age: 8 Status: 16 Image: 20 Name: 24 // X: Sizeof: 32 // y: Sizeof: 40
Я написал его, чтобы лучше понять, что unsafe.Offset выводит на экран. вывод указывает на это.
- IsPublic от начала структуры до поля IsPublic расстояние 0 байт
- Следующий кадр указывает, как мы считаем эти X во втором поле 8 BytesConclusion
- Возраст заполняет все пространство (занимает 8 байт), поэтому путь от начала с учетом предыдущего поля равен 16
- И так продолжается…
Заключение
Мы использовали последовательность типов данных в соответствии с их размером. Переупорядочив поля структуры, можно улучшить как использование памяти, так и скорость приложения.
Случай использования
В повседневной работе программиста это может не пригодиться, если у нас не ограниченная память, но я считаю, что все же стоит следовать этой практике для более качественного продукта. В моей работе практическое применение нашло широкое применение в секторе блокчейна. Каждая операция с памятью в виртуальной машине состояний Эфириума имеет реальную стоимость.
Я приведу здесь примеры, которые также не обязательно относятся к самому языку Go, потому что это зависит от того, как компилятор его обрабатывает, и каждый другой язык также может иметь свое приложение.
- Блокчейн — я сталкивался с такой практикой в Ethereum, где используется язык Solidity. Это хороший способ снизить стоимость выполнения некоторых операций в памяти EVM. Под стоимостью здесь я подразумеваю стоимость виртуальных денег.
- Встроенная система — процессор и память работают в ограниченном режиме. Процессоры могут выдавать ошибку (такие случаи были в старых процессорах) или обеспечивать альтернативное поведение в случае некорректного обращения к памяти
- Везде, где мы хотели бы поставлять хороший и качественный код.
Инструменты
В официальном пакете go есть анализаторы, которые сделают всю работу за нас, дополнительную работу также может сделать за нас CI/CD
https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/fieldalignment