Desempenho - Economizando memória em Structs com StructLayout
Tags: csharp, dotnet, performance, braziliandevs
Published: 2021-04-18 18:20:31

Olá, este é um artigo da seção **Desempenho** e, desta vez, vamos falar sobre StructLayout, uma forma de organização dos campos de uma struct que permite economizar memória.

Antes de começarmos, vale lembrar a razão pela qual utilizamos structs. Structs, de forma resumida, são *value types*, que são alocados em um espaço de memória não monitorado pelo Garbage Collector chamado *stack*, e tem um ciclo de vida restrito ao método onde foram declarados ou recebidos como argumentos. 

Portanto, utilizar structs representa uma economia de memória do ponto de vista da aplicação, e de processamento do ponto de vista do *runtime*, já que o Garbage Collector não sofrerá pressão por elas e, consequentemente, será menos acionado.

Isto dito, vamos entender como structs são alinhadas na memória.

## Alinhamento em Memória

O que é alinhamento em memória? É a forma como o compilador distribui os bytes que serão alocados para a sua struct na memória. Por padrão, strcuts são alinhadas em pacotes cujo tamaho é dado de acordo com o tamanho do maior campo presente. Ou seja, se o maior campo de uma struct for um inteiro de 64bit, que tem 8 bytes, os campos de sua struct serão alinhados em blocos de 8 bytes.

Imagine a seguinte struct:

```c#
namespace Lab.Desempenho.StructLayout
{
    public struct Struct1
    {
        public byte Byte;
        public int Int;
        public long Long;
    }
}
```

Seu alinhamento em memória seria o seguinte:

```c#
Size: 16 bytes. Paddings: 3 bytes (%18 of empty space)
|===============================|
|     0: Byte Byte (1 byte)   |
|-------------------------------|
|   1-3: padding (3 bytes)      |
|-------------------------------|
|   4-7: Int32 Int (4 bytes)  |
|-------------------------------|
|  8-15: Int64 Long (8 bytes) |
|===============================|
```

Repare no seguinte: lendo do final do para o começo da tabela acima, temos nesta struct um campo `long` (inteiro de 64 bits, com 8 bytes), e ele determina o tamanho dos blocos de alinhamento em memória da struct por padrão por ser o maior campo. Acima dele, temos 4 bytes de nosso campo `int` (4 bytes), um `padding` (3 bytes) e nosso campo `byte` (1 byte).

Agora, você pode estar se perguntando, o que é esse `padding`? Ele é o responsável por garantir que nossa struct será alinhada em blocos de 8 bytes. Ou seja, caso em vez de um `int` e um `byte` tivéssemos dois `byte`, esse `padding` seria de 6 bytes em vez de 3, pois 6 bytes seria a diferença do tamanho do bloco, 8 bytes, para os 2 bytes dos campos `byte` declarados.

Um detalhe muito interessante aqui é que a ordem dos campos importa, já que o alinhamento também é dado por ele. Então, veja este outro exemplo com a mesma struct, mas trocando a ordem de nosso campo `long` com nosso campo `int`:

```c#
namespace Lab.Desempenho.StructLayout
{
    public struct Struct1
    {
        public byte Byte;
        public long Long;
        public int Int;
    }
}
```

Seu alinhamento passaria a ser o seguinte:

```c#
Size: 24 bytes. Paddings: 11 bytes (%45 of empty space)
|=============================|
|     0: Byte Byte (1 byte)   |
|-----------------------------|
|   1-7: padding (7 bytes)    |
|-----------------------------|
|  8-15: Int64 Long (8 bytes) |
|-----------------------------|
| 16-19: Int32 Int (4 bytes)  |
|-----------------------------|
| 20-23: padding (4 bytes)    |
|=============================|
```

Repare em um detalhe fundamental: não apenas a organização dos blocos mudou, como o tamanho da struct aumentou em 8 bytes!

Isso aconteceu porque, como o alinhamento é dado pelo campo de maior tamanho e pela ordem de declaração, foi necessário aumentar o primeiro `padding` de 3 para 7 bytes, para acompanhar o tamanho do campo `long` e, para que o campo `int` também o acompanhasse, foi necessário criar um novo `padding` com 4 bytes.

Ou seja, apenas por força da ordem de declaração de seus campos, sua struct ficou 11 bytes maior!

## StructLayout: lidando com o alinhamento

Agora que entendemos como nossas structs são alinhadas na memória, a pergunta que fica é: como lidamos com isso e evitamos os `paddings`?

Existem duas formas de resolver este problema. Um deles, que você já deve ter imaginado, é organizar os campos em uma sequência que resulte na menor quantidade e tamanho possível de `paddings`. Mas há uma outra forma, fornecida pelo próprio .Net que nos ajuda com essa tarefa ingrata, o atributo `StructLayout`.

Com este atributo, podemos não apenas definir a ordem pela qual nossos campos serão distribuídos pelo compilador como, também, informar o tamanho do bloco de alinhamento, de modo a controlar a ocorrência de paddings.

Vejamos um exemplo com uma nova versão da mesma struct:

```c#
using System.Runtime.InteropServices;

namespace Lab.Desempenho.StructLayout
{
    [StructLayout(LayoutKind.Auto)]
    public struct Struct2
    {
        public byte Byte;
        public long Long;
        public int Int;
    }
}
```

O resultado será o seguinte:

```c#
Size: 16 bytes. Paddings: 3 bytes (%18 of empty space)
|=============================|
|   0-7: Int64 Long (8 bytes) |
|-----------------------------|
|  8-11: Int32 Int (4 bytes)  |
|-----------------------------|
|    12: Byte Byte (1 byte)   |
|-----------------------------|
| 13-15: padding (3 bytes)    |
|=============================|
```

Repare que, neste caso, usando o atributo `StructLayout` e informando o `LayoutKind` como `Auto`, o compilador se encarregou de organizar nossos campos do maior para o menor, utilizando o `padding` de 3 bytes apenas para compensar o restante para completar os 8 bytes de nosso `long`.

Ou seja, utilizando o atributo com esta configuração, nos livramos do trabalho para organizar nossos campos. 

Legal. Né? Mas tem mais!

É possível não apenas determinar a ordem de alinhamento dos campos como, também, definir o tamanho do pacote de alinhamento.

Vejamos um novo exemplo:

```c#
using System.Runtime.InteropServices;

namespace Lab.Desempenho.StructLayout
{
    [StructLayout(LayoutKind.Sequential, Pack = 4)]
    public struct Struct3
    {
        public int Int;
        public long Long;
        public int Int2;
    }
}
```

Repare que, aqui, temos o `LayoutKind.Sequential`, que significa que o alinhamento vai seguir a mesma ordem definida no código (que é a opção padrão). Além disso, introduzimos uma propriedade chamada `Pack`, que é o tamanho do pacote de alinhamento dos bytes de nossos campos, com o valor 4. Isso significa que serão criados blocos com 4 bytes para alinhar nossos campos, e não mais 8 como seria o padrão dado pelo campo de maior tamanho (nosso, `long`).

Portanto, o resultado será o seguinte:

```c#
Size: 16 bytes. Paddings: 0 bytes (%0 of empty space)
|=============================|
|   0-3: Int32 Int (4 bytes)  |
|-----------------------------|
|  4-11: Int64 Long (8 bytes) |
|-----------------------------|
| 12-15: Int32 Int2 (4 bytes) |
|=============================|
```

E voi lá! Temos apenas os 16 bytes de nossos campos sendo alinhados. Nosso `long` é entendido como uma sequência com dois blocos com 4 bytes cada, e isso permite que nossa struct evite dos dois paddings com 4 bytes que seriam criados caso a propriedade `Pack` não tivesse sido definida.

Vamos colocar a afirmação acima a prova?

A struct abaixo tem a mesma distribuição de campos do exemplo acima, mas dispensa o atributo `StructLayout` e, por consequência, a propriedade `Pack` (lembrando que a ausência do atributo tem o mesmo efeito de declarar seu `LayoutKind` como `Sequential`):

```c#
namespace Lab.Desempenho.StructLayout
{
    public struct Struct4
    {
        public int Int;
        public long Long;
        public int Int2;
    }
}
```

Veja o resultado abaixo: temos agora não mais 16 bytes, mas 24. Isso porque a ausência da propriedade `Pack` de `StructLayout` fez retornar o comportamento padrão, que é definir o tamanho do pacote a partir do campo de maior tamanho.

```c#
Size: 24 bytes. Paddings: 8 bytes (%33 of empty space)
|=============================|
|   0-3: Int32 Int (4 bytes)  |
|-----------------------------|
|   4-7: padding (4 bytes)    |
|-----------------------------|
|  8-15: Int64 Long (8 bytes) |
|-----------------------------|
| 16-19: Int32 Int2 (4 bytes) |
|-----------------------------|
| 20-23: padding (4 bytes)    |
|=============================|
```

## Conclusão:

Pudemos observar como um detalhe simples, como a forma como distribuímos os campos em nossas structs pode afetar seu tamanho final e, por consequência, o desempenho de nossa aplicação. Sem dúvida uma questão tão interessante quanto pouco conhecida.

Mas tão interessante quando conhecer este comportamento do alinhamento das structs, é compreender os comportamentos do compilador do C#. Conhecer a maneira como ele traduz nosso código nos dá uma visão de como promover otimizações em nosso código com um mínimo de esforço.

Em fututos posts da seção **Desempenho** traremos mais detalhes sobre como podemos extrair maior velocidade de execução e maior economia de recursos utilizando pequenos truques, sejam eles a partir de ferramentas do próprio .Net como neste caso, ou através de simples mudanças na escrita de nosso código.

Como de costume, segue um projeto de exemplo no [Github](https://github.com/wsantosdev/lab-desempenho-structlayout) para verificar os resultados obtidos no post. Para a geração destes relatórios é utilizado um componente muito interessante, o [ObjectLayoutInspector](https://github.com/SergeyTeplyakov/ObjectLayoutInspector).

Gostou? Me deixe saber pelos indicadores. Tem dúvidas ou sugestões? Deixe um comentário ou me procure pelas redes sociais.

Até a próxima!