вступление

Вы когда-нибудь думали о том, как улучшить свой код или как добиться успеха на собеседовании?

«выравнивание» структур — это процесс оптимизации, в котором мы можем уменьшить потребление памяти правильно построенной структурой.

Благодаря этому сгенерированный машинный код использует одну инструкцию для чтения/записи пространства памяти, без этого их могло бы быть 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 выводит на экран. вывод указывает на это.

  1. IsPublic от начала структуры до поля IsPublic расстояние 0 байт
  2. Следующий кадр указывает, как мы считаем эти X во втором поле 8 BytesConclusion
  3. Возраст заполняет все пространство (занимает 8 байт), поэтому путь от начала с учетом предыдущего поля равен 16
  4. И так продолжается…

Заключение

Мы использовали последовательность типов данных в соответствии с их размером. Переупорядочив поля структуры, можно улучшить как использование памяти, так и скорость приложения.

Случай использования

В повседневной работе программиста это может не пригодиться, если у нас не ограниченная память, но я считаю, что все же стоит следовать этой практике для более качественного продукта. В моей работе практическое применение нашло широкое применение в секторе блокчейна. Каждая операция с памятью в виртуальной машине состояний Эфириума имеет реальную стоимость.

Я приведу здесь примеры, которые также не обязательно относятся к самому языку Go, потому что это зависит от того, как компилятор его обрабатывает, и каждый другой язык также может иметь свое приложение.

  • Блокчейн — я сталкивался с такой практикой в ​​Ethereum, где используется язык Solidity. Это хороший способ снизить стоимость выполнения некоторых операций в памяти EVM. Под стоимостью здесь я подразумеваю стоимость виртуальных денег.
  • Встроенная система — процессор и память работают в ограниченном режиме. Процессоры могут выдавать ошибку (такие случаи были в старых процессорах) или обеспечивать альтернативное поведение в случае некорректного обращения к памяти
  • Везде, где мы хотели бы поставлять хороший и качественный код.

Инструменты

В официальном пакете go есть анализаторы, которые сделают всю работу за нас, дополнительную работу также может сделать за нас CI/CD

https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/fieldalignment