Alocando classes na Stack
Tags: programming, dotnet, dotnetcore, csharp
Published: 2023-02-22 13:00:00

Cada vez mais buscamos formas de evitar a pressão no GC e aumentar o desempenho das nossas aplicações. Se olharmos para o nosso código muitas vezes utilizamos classes que em determinados trechos de código poderiam facilmente ser substituídas por estruturas de dados, que passariam a ser alocadas na Stack. Porém, essa alteração traria outros problemas que talvez não valesse a pena.

Esse recurso é muito utilizado em Java e está disponível desde a versão do DotNet Core 3, mas é um recurso pouco difundido em DotNet e vem desabilitado por padrão.

Então que funcionalidade é essa?

O nome dela é Escape Analysis e teve seu primeiro protótipo em DotNet realizado em 2016 e publicada no DotNet Core 3, de acordo com a documentação.

Então o que vem a ser esse Escape Analysis?

Toda instância de um objeto é alocada na Heap Gerenciada. Então imagine se o tempo de vida desse objeto fosse o mesmo tempo de vida do método onde ele foi alocado. Assim poderíamos mover esse objeto para a Stack e é isso que o Escape Analysis ou EA faz: Analisa o escopo da instância de um objeto e se o mesmo não escapar ele pode ser movido com segurança para a Stack.

Essa análise não é tão simples assim e tem vários outros pontos que devem ser levados em consideração. 
Alguns pontos observados:

1. Objetos com finalizadores não podem ser alocados em pilha.
2. Objetos alocados em um loop podem ser alocados em pilha somente se a alocação não escapar da iteração do loop no qual está alocado.
3. Deve haver um limite no tamanho máximo de objetos alocados na pilha.

Para habilitar esse recurso será necessário incluir uma variável de ambiente chamada COMPlus_JitObjectStackAllocation e colocar o seu valor para 1. 

No DotNet7 a variável de ambiente COMPlus_JitObjectStackAllocation teve o nome alterado para DOTNET_JitObjectStackAllocation.

Considerando uma simples implementação, apenas para demonstrar que o recurso existe, vamos criar uma classe que soma dois pontos e utilizar o Benckmark.Net para mensurar as alocações.

```csharp
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Running;
using System.Runtime.CompilerServices;

BenchmarkRunner.Run(typeof(Teste));
Console.ReadLine();

[MemoryDiagnoser]
[DisassemblyDiagnoser(printSource: true)]
[Config(typeof(ConfigWithCustomEnvVars))]
public class Teste
{
    private class ConfigWithCustomEnvVars : ManualConfig
    {
        private const string JitObjectStackAllocation =
            "ComPlus_JitObjectStackAllocation";

        public ConfigWithCustomEnvVars()
        {
            AddJob(Job.Default
                .WithEnvironmentVariables(
                    new EnvironmentVariable(JitObjectStackAllocation, "0"))
                .WithId("JitObjectStackAllocation Off"));
            AddJob(Job.Default
                .WithEnvironmentVariables(
                    new EnvironmentVariable(JitObjectStackAllocation, "1"))
                .WithId("JitObjectStackAllocation On"));
        }
    }

    [Params(1, 5)]
    public int A { get; set; }
    [Params(1, 5)]
    public int B { get; set; }

    [Benchmark]
    public int Calcular()
    {
        var calculadora = new Calculadora(A, B);
        return calculadora.Soma();
    }
}

public class Calculadora
{
    private int v1;
    private int v2;

    public Calculadora(int v1, int v2)
    {
        this.v1 = v1;
        this.v2 = v2;
    }

    internal int Soma()
    {
        return v1 + v2;
    }
}
```
Após a exeução dos testes o resultado será:

![Resultado Benchmark](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qsamtw4pfbckylr4kb77.png)

Vejam que o mesmo código com a variável de ambiente COMPlus_JitObjectStackAllocation definida como 0 gerou alocações em GEN 0 e alocou 24 Bytes e com a variável de ambiente COMPlus_JitObjectStackAllocation definida para 1 não gerou nenhuma alocação e ainda teve o menor tempo de execução.

Podemos ir um pouco além e olhar o código assemby gerado. Na imagem abaixo o primeiro trecho de código refere-se ao COMPlus_JitObjectStackAllocation definido como 0 e o segundo trecho, menor, com o COMPlus_JitObjectStackAllocation definido para 1.

![Código Assembly](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xyp9b0e48fzt901ipend.png)

Com o COMPlus_JitObjectStackAllocation definido para 0 podemos notar uma quantidade maior de código e acessos a endereços de memória, mas com COMPlus_JitObjectStackAllocation definido para 1 observamos um código muito menor e onde podemos ver valores armazenados nas posições de memória apontadas pelos endereços [rcx+8] e [rcx+0C] sendo movidos para os registradores eax e edx para que seja realizada a operação de add (soma).

Este recurso apesar de ser pouco difundido é útil em casos onde a redução da pressão no GC se faz necessária. 
Conseguir alocar classes diretamente na Stack ajudará a diminuir a pressão no GC aumentando a performance na sua aplicação. 

O exemplo foi simples, apenas para demonstrar o recurso. Convido você a realizar seus próprios testes e compartilhar aqui. 

Até a próxima!

Referências

[object-stack-allocation](https://github.com/dotnet/runtime/blob/main/docs/design/coreclr/jit/object-stack-allocation.md)
[Pull Request Inicial](https://github.com/dotnet/coreclr/pull/20814)
[Egor Bogatov - Twiter](https://twitter.com/egorbo/status/1060184817717952512)
[Escape analysis for Java](https://dl.acm.org/doi/10.1145/320385.320386)
[Escape Analysis in the HotSpot JIT Compiler](https://blogs.oracle.com/javamagazine/post/escape-analysis-in-the-hotspot-jit-compiler)