Otimizando tamanho em memória de Structs em Golang
Tags: go, coding, learning, performance
Published: 2023-05-22 01:57:04


Quando trabalhamos com Go, é muito comum utilizarmos a estrutura *Struct* para agrupar vários campos com diferentes tipos primitivos de dados, mas como será que essa estrutura é armazenada em memória e existe alguma maneira de otimizar esse espaço ocupado em memória?

Com esse questionamento acabei me deparando com dois conceitos que antes eu não conhecia, chamados de **_Data Alignment_** e **_Padding_**.

Para podermos entender os conceitos, é importante primeiro termos conhecimento de **quanto cada tipo primitivo de dado ocupa em memória**, não vou me extender muito nesse assunto em específico, mas segue um breve resumo dos tipos primitivos e seus respectivos tamanhos em bytes:

    Tipos primitivos em Go e seus respectivos tamanhos em bytes:

    * string - 16 bytes
    * int32 - 4 bytes
    * int64 - 8 bytes
    * int - depende da arquitetura do Sistema Operacional(32 bits - 4 bytes ou 64 bits - 8 bytes)
    * uint - mesmas regras aplicados ao tipo int e respectivamente int32 e int64
    * float32 - 4 bytes
    * float64 - 8 bytes
    * bool - 1 byte

Caso queira avaliar mais profundamente os tipos primitivos e seus tamanhos em Go você pode consultar [aqui](https://golang.org/src/go/types/sizes.go).

Em primeiro caso, para entendermos a ideia do *Padding*, vamos trabalhar com essa struct:

    type StructA struct {
        i32 int32
        s   string
        b   bool
    }

Nós sabemos que um _int32_ ocupa 4 bytes, uma _string_ ocupa 16 bytes e um _bool_ ocupa 1 byte certo? Então em teoria o tamanho em memória da struct deveria ser exatamente a soma em bytes de todos os campos que ela contém, no caso da nossa *StructA* deveria ser 21 bytes certo?

Bom, podemos validar esse tamanho com a função [**<u>Sizeof() do pacote unsafe</u>**](https://golang.org/pkg/unsafe/) que retorna o tamanho em bytes alocado para a estrutura passada na função dessa maneira:

    package main

    import (
        "fmt"
        "unsafe"
    )

    type StructA struct {
        i32 int32  // 4 bytes
        s   string // 16 bytes
        b   bool   // 1 bytes
    }

    func main() {
        a := StructA{}
        fmt.Println("Tamanho da StructA: ", unsafe.Sizeof(a))
    }

O retorno que imaginamos seria 21 bytes certo? Mas o retorno desse script é:

    Tamanho da StructA: 32

E agora vem o ponto de dúvida, **por que exatamente essa struct ocupa 32 bytes se a soma de todos os campos é 21 bytes? É aí que entra o _Data Alignment_.**

Para entermos melhor o conceito, é importante que saibamos de maneira abstrata como a memória é organizada em nossa máquina. A estrutura de memória é armazenada em células, seguindo uma estrutura parecida com essa:

    0x00 |  dados  |   
    0x01 |   ...   |
    0x02 |   ...   |

**Cada célula de memória, armazena um tamanho específico de bits, que depende da arquitetura do nosso sistema operacional (x32 ou x64), ou seja, 4 bytes para sistemas x32 e 8 bytes para sistemas x64**, podemos imaginar cada celula dessa maneira, tomando como premissa um SO x64:

    0x00 | byte | byte | byte | byte | byte | byte | byte | byte |  
    0x01 |   ...   |
    0x02 |   ...   |

**O _Data Alignment_ estrutura cada variável que criamos em nosso programa, para sempre estar alinhado em somente uma célula de memória.** Por exemplo, quando temos uma variável do tipo int32 (4 bytes), ele trabalhar para que todos os 4 bytes estejam na mesma célula de memória, para evitar que o processador tenha que acessar mais de uma célular de memória para ler ou escrever o conteúdo da nossa variável. Portanto nossa StructA, está organizada dessa forma em memória:

    0x00 | i32Byte | i32Byte | i32Byte | i32Byte | - | - | - | - |  
    0x01 | sByte | sByte| sByte| sByte| sByte | sByte | sByte | sByte |
    0x02 | sByte | sByte| sByte| sByte| sByte | sByte | sByte | sByte |
    0x03 | boolByte| - | - | - | - | - | - | - |

Se contarmos cada byte alinhado na estrutura de memória acima representado pelo “| |”, podemos contar no total 32 casas, ou seja, nossos 32 bytes retornados pela função *unsafe.SizeOf()*.

A estrutura vazia na célula de memória representada pelo “-”, é o que chamamos de **_Padding_**. **Sua função é basicamente completar o espaço não usado naquela célula, para que a próxima estrutura possa começar em uma nova célula de memória.** Já que caso a alocação dos bytes da nossa variável string inciasse no final da célula *0x00*, o processador acabaria tendo que acessar 3 células de memória invés de 2 para trabalhar no conteúdo dessa variável, já que a organização ficaria dessa forma:

    0x00 |i32Byte|i32Byte|i32Byte|i32Byte|sByte|sByte|sByte|sByte|  
    0x01 | sByte | sByte| sByte| sByte| sByte | sByte | sByte | sByte |
    0x02 | sByte | sByte| sByte | sByte | - | - | - | - |
    0x03 | boolByte| - | - | - | - | - | - | - |

Bom, agora sabemos que o **_Data Alignment_ é responsável por trabalhar o alinhamento dos bytes das nossas variáveis nas células de memória**, para que o **processador execute o mínimo de instruções possíveis** para manipular um dado em memória e o **_Padding_ é o responsável por “preencher” os espaços não usados em cada endereço**, fornecendo o apoio para o alinhamento mais eficiente em memória.

Sabendo disso, como então podemos utilizar esse conhecimento para otimizar nossas structs? É extremamente simples, basta **reordenar os campos para que sempre que possível dois tipos que somado seus tamanhos em bytes seja no máximo 8**, ou seja, podem ocupar interiamente o mesmo alocamento de 8 bytes na memória.

Para exemplicar, vamos criar uma nova struct chamada *StructAOptimized*, onde reordenaremos os campos de maneira mais eficiente:

    package main

    import (
        "fmt"
        "unsafe"
    )

    type StructA struct {
        i32 int32  // 4 bytes
        s   string // 16 bytes
        b   bool   // 1 bytes
    }

    type StructAOptimized* struct {
        i32 int32  // 4 bytes
        b   bool   // 1 bytes
        s   string // 16 bytes
    }

    func main() {
        a := StructA{}
        aOptimized := StructAOptimized{}

        fmt.Println("Tamanho da StructA: ", unsafe.Sizeof(a))
        fmt.Println("Tamanho da StructA Otimizada: ", unsafe.Sizeof(aOptimized))
    }

O retorno desse script será:

    Tamanho da StructA:  32
    Tamanho da StructA Otimizada:  24

Percebam que a única mudança da StructA para a StructAOptimized foi que alteremos a ordem entre o campo booleano e o campo string, apenas com esse pequeno ajuste reduzimos o tamanho alocado da struct em memória em 8 bytes. Mas por que isso ocorre?

O *Data Alignment* realiza o alinhamento dos dados em memória de maneira sequencial, portanto quando temos a estrutura da StructAOptimized, nossas células de memória estarão alinhadas dessa forma:

    0x00 | i32Byte | i32Byte | i32Byte | i32Byte | boolByte |-|-|-|  
    0x01 | sByte | sByte| sByte| sByte| sByte | sByte | sByte | sByte |
    0x02 | sByte | sByte| sByte| sByte| sByte | sByte | sByte | sByte |

Percebam que como nossa variável do tipo int32 ocupa somente 4 bytes, há ainda 4 bytes livres no mesmo espaço de alocamento. Quando a segunda variável a ser alocado é uma string, o _Data Alignment_ começa a alocar os bytes da nossa string em uma nova estrutura, afim de otimizar o trabalho do processador, já que nossa variável string não acabe inteiramente nos 4 bytes que sobraram.

Agora, no caso da nossa StructAOptimized o segundo campo a ser alocado é um boolean, que necessita de 1 byte, como é possível alocar todos os seus bytes no espaço restante do endereço 0x00, ele não precisa criar uma nova estrutura de 8 bytes.

Espero que esse conteúdo tenha sido útil pra você que leu até aqui. Lembrando que toda a exemplificação foi utilizando Go, mas o mesmo comportamento pode ser reproduzido em outras linguagens.

Deixei um repositório no Github com os testes realizados na explicação e mais uma demonstração com outras duas structs, você pode acessa-lo aqui: [https://github.com/viniciusgabrielfo/go-struct-optimization-test](https://github.com/viniciusgabrielfo/go-struct-optimization-test)

## Referências:

* [https://stackoverflow.com/questions/38875369/what-is-data-alignment-why-and-when-should-i-be-worried-when-typecasting-pointe](https://stackoverflow.com/questions/38875369/what-is-data-alignment-why-and-when-should-i-be-worried-when-typecasting-pointe)

* [https://www.geeksforgeeks.org/structure-member-alignment-padding-and-data-packing/](https://www.geeksforgeeks.org/structure-member-alignment-padding-and-data-packing/)