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:
- Objetos com finalizadores não podem ser alocados em pilha.
- 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.
- 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.
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á:
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.
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
Pull Request Inicial
Egor Bogatov - Twiter
Escape analysis for Java
Escape Analysis in the HotSpot JIT Compiler
Top comments (0)