[Golang] Написание эффективных для памяти и процессора Go-структур, оптимизированных для работы с ЦП 2023
![[Golang] Написание эффективных для памяти и процессора Go-структур, оптимизированных для работы с ЦП 2023 [Golang] Написание эффективных для памяти и процессора Go-структур, оптимизированных для работы с ЦП 2023](https://uproger.com/wp-content/uploads/2023/02/maxresdefault-1.png)
Структуры — это коллекции разнородных данных, определенные программистами для организации информации. Структуры позволяют аккуратно инкапсулировать все данные, относящиеся к одному объекту, в одно определение типа, поведение которого затем может быть реализовано путём определения функций для типа struct.
В этой статье я попытаюсь объяснить, как мы можем эффективно написать структуру с точки зрения использования памяти и циклов процессора.
![[Golang] Написание эффективных для памяти и процессора Go-структур, оптимизированных для работы с ЦП 2023 [Golang] Написание эффективных для памяти и процессора Go-структур, оптимизированных для работы с ЦП 2023](https://uproger.com/wp-content/uploads/2023/02/1_im-myhlchqhsjrs865t0yg.webp)
Давайте рассмотрим структуру TerraformResource, которая предоставлена ниже:
type TerraformResource struct {
Cloud string // 16 bytes
Name string // 16 bytes
HaveDSL bool // 1 byte
PluginVersion string // 16 bytes
IsVersionControlled bool // 1 byte
TerraformVersion string // 16 bytes
ModuleVersionMajor int32 // 4 bytes
}
Давайте посмотрим, сколько памяти требуется для данной структуры, используя приведённый ниже код:
package main
import "fmt"
import "unsafe"
type TerraformResource struct {
Cloud string // 16 bytes
Name string // 16 bytes
HaveDSL bool // 1 byte
PluginVersion string // 16 bytes
IsVersionControlled bool // 1 byte
TerraformVersion string // 16 bytes
ModuleVersionMajor int32 // 4 bytes
}
func main() {
var d TerraformResource
d.Cloud = "aws"
d.Name = "ec2"
d.HaveDSL = true
d.PluginVersion = "3.64"
d.TerraformVersion = "1.1"
d.ModuleVersionMajor = 1
d.IsVersionControlled = true
fmt.Println("==============================================================")
fmt.Printf("Total Memory Usage StructType:d %T => [%d]\n", d, unsafe.Sizeof(d))
fmt.Println("==============================================================")
fmt.Printf("Cloud Field StructType:d.Cloud %T => [%d]\n", d.Cloud, unsafe.Sizeof(d.Cloud))
fmt.Printf("Name Field StructType:d.Name %T => [%d]\n", d.Name, unsafe.Sizeof(d.Name))
fmt.Printf("HaveDSL Field StructType:d.HaveDSL %T => [%d]\n", d.HaveDSL, unsafe.Sizeof(d.HaveDSL))
fmt.Printf("PluginVersion Field StructType:d.PluginVersion %T => [%d]\n", d.PluginVersion, unsafe.Sizeof(d.PluginVersion))
fmt.Printf("ModuleVersionMajor Field StructType:d.IsVersionControlled %T => [%d]\n", d.IsVersionControlled, unsafe.Sizeof(d.IsVersionControlled))
fmt.Printf("TerraformVersion Field StructType:d.TerraformVersion %T => [%d]\n", d.TerraformVersion, unsafe.Sizeof(d.TerraformVersion))
fmt.Printf("ModuleVersionMajor Field StructType:d.ModuleVersionMajor %T => [%d]\n", d.ModuleVersionMajor, unsafe.Sizeof(d.ModuleVersionMajor))
}
Результат
>> go run golang-struct-memory-allocation.go
==============================================================
Total Memory Usage StructType:d main.TerraformResource => [88]
==============================================================
Cloud Field StructType:d.Cloud string => [16]
Name Field StructType:d.Name string => [16]
HaveDSL Field StructType:d.HaveDSL bool => [1]
PluginVersion Field StructType:d.PluginVersion string => [16]
ModuleVersionMajor Field StructType:d.IsVersionControlled bool => [1]
TerraformVersion Field StructType:d.TerraformVersion string => [16]
ModuleVersionMajor Field StructType:d.ModuleVersionMajor int32 => [4]
Таким образом, общее выделение памяти, требуемое для структуры TerraformResource, составляет 88 байт. Вот как будет выглядеть распределение памяти для типа TerraformResource:
![[Golang] Написание эффективных для памяти и процессора Go-структур, оптимизированных для работы с ЦП 2023 [Golang] Написание эффективных для памяти и процессора Go-структур, оптимизированных для работы с ЦП 2023](https://uproger.com/wp-content/uploads/2023/02/1_wgujxtdloncxzu5by-log-1-1024x451.webp)
Но как получилось 88 байт (16 +16 + 1 + 16 + 1+ 16 + 4 = 70 байт)? Откуда берутся эти дополнительные 18 байт?
Когда дело доходит до выделения памяти для структур, им всегда выделяются смежные блоки памяти, выровненные по байтам, а поля выделяются и хранятся в том порядке, в котором они определены. Концепция выравнивания по байтам в этом контексте означает, что смежные блоки памяти выровнены со смещениями.
![[Golang] Написание эффективных для памяти и процессора Go-структур, оптимизированных для работы с ЦП 2023 [Golang] Написание эффективных для памяти и процессора Go-структур, оптимизированных для работы с ЦП 2023](https://uproger.com/wp-content/uploads/2023/02/1_1afhpdmuaii3ie4y5qkf1a-1024x404.webp)
Мы можем ясно видеть, что TerraformResource.HaveDSL , TerraformResource.isVersionControlled и TerraformResource.ModuleVersionMajor занимают всего 1 байт, 1 байт и 4 байта соответственно. Остальная часть пространства заполняется пустыми байтами заполнения.
Итак, возвращаясь к математике:
Байты выделения =16 байт +16 байт + 1 байт+ 16 байт+ 1 байт + 16 байт + 4 байта = 70 байт
Пустые байты заполнения = 7 байт + 7 байт + 4 байта = 18 байт
Общее количество байтов = байты выделения + пустые байты заполнения = 70 байт + 18 байт = 88 байт
Итак, как нам это исправить? Мы могли бы переопределить нашу структуру следующим образом:
type TerraformResource struct {
Cloud string // 16 bytes
Name string // 16 bytes
PluginVersion string // 16 bytes
TerraformVersion string // 16 bytes
ModuleVersionMajor int32 // 4 bytes
HaveDSL bool // 1 byte
IsVersionControlled bool // 1 byte
}
Запустите этот код с оптимизированной структурой:
package main
import "fmt"
import "unsafe"
type TerraformResource struct {
Cloud string // 16 bytes
Name string // 16 bytes
PluginVersion string // 16 bytes
TerraformVersion string // 16 bytes
ModuleVersionMajor int32 // 4 bytes
HaveDSL bool // 1 byte
IsVersionControlled bool // 1 byte
}
func main() {
var d TerraformResource
d.Cloud = "aws"
d.Name = "ec2"
d.HaveDSL = true
d.PluginVersion = "3.64"
d.TerraformVersion = "1.1"
d.ModuleVersionMajor = 1
d.IsVersionControlled = true
fmt.Println("==============================================================")
fmt.Printf("Total Memory Usage StructType:d %T => [%d]\n", d, unsafe.Sizeof(d))
fmt.Println("==============================================================")
fmt.Printf("Cloud Field StructType:d.Cloud %T => [%d]\n", d.Cloud, unsafe.Sizeof(d.Cloud))
fmt.Printf("Name Field StructType:d.Name %T => [%d]\n", d.Name, unsafe.Sizeof(d.Name))
fmt.Printf("HaveDSL Field StructType:d.HaveDSL %T => [%d]\n", d.HaveDSL, unsafe.Sizeof(d.HaveDSL))
fmt.Printf("PluginVersion Field StructType:d.PluginVersion %T => [%d]\n", d.PluginVersion, unsafe.Sizeof(d.PluginVersion))
fmt.Printf("ModuleVersionMajor Field StructType:d.IsVersionControlled %T => [%d]\n", d.IsVersionControlled, unsafe.Sizeof(d.IsVersionControlled))
fmt.Printf("TerraformVersion Field StructType:d.TerraformVersion %T => [%d]\n", d.TerraformVersion, unsafe.Sizeof(d.TerraformVersion))
fmt.Printf("ModuleVersionMajor Field StructType:d.ModuleVersionMajor %T => [%d]\n", dModuleVersionMajor, unsafe.Sizeof(d.ModuleVersionMajor))
}
Результат
>> go run golang-struct-memory-allocation-optimized.go
==============================================================
Total Memory Usage StructType:d main.TerraformResource => [72]
==============================================================
Cloud Field StructType:d.Cloud string => [16]
Name Field StructType:d.Name string => [16]
HaveDSL Field StructType:d.HaveDSL bool => [1]
PluginVersion Field StructType:d.PluginVersion string => [16]
ModuleVersionMajor Field StructType:d.IsVersionControlled bool => [1]
TerraformVersion Field StructType:d.TerraformVersion string => [16]
ModuleVersionMajor Field StructType:d.ModuleVersionMajor int32 => [4]
Теперь общее выделение памяти для типа TerraformResource составляет 72 байта. Давайте посмотрим, как выглядит выравнивание памяти:
![[Golang] Написание эффективных для памяти и процессора Go-структур, оптимизированных для работы с ЦП 2023 [Golang] Написание эффективных для памяти и процессора Go-структур, оптимизированных для работы с ЦП 2023](https://uproger.com/wp-content/uploads/2023/02/1_zdk8snjkswymnf0momyjgg.webp)
Просто выполнив правильное выравнивание структуры данных для элементов struct, мы смогли уменьшить объем памяти с 88 байт до 72 байт…Великолепно!
Снова обращаемся к математике:
Байты выделения = 16 байт + 16 байт + 16 байт + 16 байт +4 байта + 1 байт + 1 байт = 70 байт
Пустые байты заполнения = 2 байта
Общее количество байтов = байты выделения + пустые байты заполнения = 70 байт + 2 байта = 72 байта
Правильное выравнивание структуры данных помогает нам не только эффективно использовать память, но и сокращать циклы чтения ЦП….Как?
Процессор считывает память словами, которые составляют 4 байта в 32-разрядных системах, 8 байт в 64-разрядных системах. Теперь нашему первому объявлению типа структуры TerraformResource потребуется слов, чтобы процессор считал всё:
![[Golang] Написание эффективных для памяти и процессора Go-структур, оптимизированных для работы с ЦП 2023 [Golang] Написание эффективных для памяти и процессора Go-структур, оптимизированных для работы с ЦП 2023](https://uproger.com/wp-content/uploads/2023/02/1_d8fh5hbazrii7ctdzpd8qw.webp)
Однако оптимизированная структура займёт всего слов, как показано ниже:
![[Golang] Написание эффективных для памяти и процессора Go-структур, оптимизированных для работы с ЦП 2023 [Golang] Написание эффективных для памяти и процессора Go-структур, оптимизированных для работы с ЦП 2023](https://uproger.com/wp-content/uploads/2023/02/1_sld7-jfluwxyitn9b-_bga.webp)
Определив структуру должным образом, выровняв структуру данных, мы смогли эффективно использовать распределение памяти и сделали структуру быстрой с точки зрения чтения процессором.
Это всего лишь небольшой пример; подумайте о большой структуре с 20 или 30 полями разных типов. Продуманное выравнивание структуры данных действительно является эффективным лайфхаком.
Надеюсь, эта статья смогла пролить некоторый свет на внутренние компоненты struct, их распределение памяти и требуемые циклы чтения процессором!