Alocações em .NET



This content originally appeared on DEV Community and was authored by Pedro Jesus

Reference Type é alocada na memória heap e Value Type é alocada na memória Stack.

Já adianto que essa afirmativa, acima, está errada!

Se essa afirmativa acima, ou uma variante dela, te parece correta, esse artigo é pra você. Já cansei de ver inúmeros artigos brasileiros (acredito que todos os que vi) sobre gerenciamento de memória fazendo essa afirmativa e, consequentemente espalhando desinformação.

Para Reference Type, é ok considerar que é armazenado na memória heap, até porque é muito sutil quando não é. Agora, é bem comum Value Types serem armazenados fora da memória stack.

Ainda sobre a afirmativa acima. Isso já foi debatido, em 2018, pelo time que trabalha no runtime e na linguagem, você pode ver toda a discussão aqui. Resultando na atualização da documentação oficial para remover tal afirmativa.

Ter esse conceito errado pode não atrapalhar sua carreira agora, principalmente se você for júnior ou até mesmo pleno, porém para os níveis sênior ou master/especialista já é problemático. Uma vez que haverá necessidade de escrever códigos performáticos ou até mesmo investigar/debugar um problema de perfomance. Ter esse modelo de memória equivocado vai te atrapalhar. Sem mais delongas…

Definição

Vamos começar pela definição de cada um desses termos, pela ECMA-335 pode-se dizer (livre tradução):

  • Value Type: Valores descritos por um tipo de valor são independentes (cada um pode ser compreendido sem referência a outros valores);
  • Reference Type: Valores do tipo referência representam o endereço de um outro valor, que podem ser de 4 tipos, sendo eles objeto, interface, pointeiro e built-in reference types.

Value Type

Value Types em C# são os tipos representados por struct e quando utilizadas em código são passadas por valor, ou seja, é feita uma cópia do Value Type em questão.
Um exemplo desse comportamento.

int x = 42;
Console.WriteLine(x); // 42
MudarValor(x);
Console.WriteLine(x); // 42


void MudarValor(int x) => x = 69;

Claro que você pode mudar esse comportamento e passar o Value Type por referência, através de um manager pointer. Em C# isso é feito através das keywords ref e in, onde o in indica que o Value Type será passado como um referência readonly.

int x = 42;
Console.WriteLine(x); // 42
MudarValor(ref x);
Console.WriteLine(x); // 69

void MudarValor(ref int x) => x = 69;

Utizando o ref mudou-se o valor de x, pois passou-se uma referência o local de memória e sobrescreveu o valor que estava lá.

Existem algumas limitações para se passar um Value Type por referência. Por exemplo, métodos assíncronos não podem ter parâmetros in ou ref. E essa limitação ocorre pelo simples fato de que, em métodos assíncronos, o ValueType pode ir parar na memória heap!

int x = 42;
Console.WriteLine(x);
MudarValor(ref x);
Console.WriteLine(x);

async void MudarValor(ref int x) => x = 69; // Erro de compilação

Por enquanto vamos ignorar o ref struct, esse tipo terá um capítulo só para ele.

Reference Type

Reference Type em C# são tipos que herdam de System.Object e não herdam de System.ValueType. Esse tipo guarda uma referência para um endereço de memória fazendo com as alterações sejam vista por todos que observam esse valor.

var a = new A() { X = 42 };

Console.WriteLine(a); // 42
MudarValor(a);
Console.WriteLine(a); // 69


void MudarValor(A a) => a.X = 69;

class A
{
    public int X;
    public override string ToString() => X.ToString();
}

No exemplo acima, temos uma classe A e dentro dela um field do tipo int (Value Type), e como você já deve ter “sacado”, esse inteiro está na memória heap. Ao passar a para o método MudarValor e alterar o valor X, ele vai alterar o valor naquele espaço de memória, sobrescrevendo o valor que estava antes em X.

Agora, quando se pensa nisso mais a fundo pode ocorrer algumas confusões, por exemplo neste caso.

var a = new A() { X = 42 };

Console.WriteLine(a.X); // 42
MudarValor(a);
Console.WriteLine(a.X); // 42


void MudarValor(A a) => a = null;

class A
{
    public int X;
    public override string ToString() => X.ToString();
}

Ficou confus@ pelo fato do segundo WriteLine ter printado 42 em vez de uma NullReferenceException ter estourado? Se não tiver ficado confus@ pode pular para o próximo capítulo, agora se ficou…

Acontece que quando se passa a para o método, esta se passando uma cópia de um ponteiro (ponteiros no C# funcionam como Value Type), então eu estou anulado a copia dentro método, o ponteiro do método principal continua apontando para o endereço de memória onde A está alocado e com isso não vai ter uma NullReferenceException. Isso pode mudar se a for passado por referência. Por exemplo…

var a = new A() { X = 42 };

Console.WriteLine(a.X); // 42
MudarValor(ref a);
Console.WriteLine(a.X); // NullReferenceException

void MudarValor(ref A a) => a = null;

class A
{
    public int X;
    public override string ToString() => X.ToString();
}

Agora temos a NullReferenceException, e o motivo é que o método MudarValor recebe uma referência de memória de onde está o ponteiro que referência A, e ao marcar como null, dentro do método, aquele espaço na memória será sobrescrito com null, causando a Exception.

Onde eles vivem?

Eu espero, que até aqui tenha ficado claro que Value Type pode viver tanto na memória stack quanto na heap e Reference Type vive na memória heap. Se estiver claro, vamos ir pouco mais fundo…

O local onde esses tipos serão alocados vai depender do contexto em que estão sendo criados e de análises feitas pelo compilador. Se o compilador ver sentido um Value Type pode ser alocado em um CPU register, que é ainda mais rápido que a memória stack, ou seja, em alguns casos o Value Type nem vai viver na memória stack!

Quando ocorrer um boxing o Value Type é alocado na memória heap.

int x = 42; // Não está na heap
object boxing = x; //Está na heap

Mesmo quando ocorre o boxing o comportamento de Value Type se mantém, isto é, ele é passado como cópia quando for utilizado. Você pode ver isso aqui.

E sobre Reference Types, por padrão, estará alocado na memória heap, mas eles também podem ser alocados na memória Stack ou CPU Registers se o compilador perceber que essa classe e seus membros estão limitados ao tempo de vida do método, isso ocorre por algo chamado Escape analysis que você pode ler mais sobre aqui.

Como devo pensar então?

Reference Type é alocada na memória heap e Value Type é alocada na memória Stack.

Acredito que ao chegar aqui essa frase, também, te pareça errada. E deve ter ficado uma pergunta “Então, como devo pensar sobre isso?” e como disse Eric Lippert o melhor jeito a se pensar é short term storage (armazenamento de curto tempo de vida, em tradução livre) e long term storage (armazenamento de longo tempo de vida, em tradução livre). Então, se o objeto (tanto Value Type quanto Reference Type) estiver alocado na heap terá um tempo de vida longo. Se não, terá um tempo de vida curto. Pensar dessa forma torna mais assertiva a investigação de gerenciamento de memória no seu software.

Vale lembrar, que uma struct pode ser alocada inicialmente fora da heap e promovida para heap se tornando long term storage. É muito fácil perceber isso na AsyncStateMachine, que só irá ser alocada na heap se o método marcado com async completar-se de maneira assíncrona, ou seja, o framework irá chamar o método

IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine);

Fazendo o boxing da struct e movendo-a para a memória heap.

Se quiser olhar mais afundo sobre a relação de métodos marcados com async e o AsyncStateMachine, recomendo o post do Angelo sobre o tema.

Conclusão

Espero que você tenha aprendido, pelo menos, uma coisa nova sobre gerenciamento de memória no .NET e espalhe a palavra. É importante que mais artigos tragam as informações corretas e validadas pelo time que trabalha na linguagem.

Este artigo foi bem simplificado e introdutório, e isso foi proposital, não quis adicionar muita informação aqui e sim dar uma base para que você aprenda o conceito de forma correta e consiga procurar mais referências sobre o tema, os links que deixei espalhado por este artigo são um bom começo.

ref struct (Bônus)

Como prometido, um capítulo apenas para esse tipo especial de Value Type, introduzido no C# 7.2 em 2017. A ref struct é um Value Type que NÃO pode viver na heap. Ela tem um tempo de vida curto e não pode escapar do contexto do método em que foi criada, com isso ela tem uma série de limitações, garantidas pelo compilador, mas no fim das contas é apenas uma struct, como pode ser visto nesse lowering.

Esse tipo é utilizado, especialmente, em algoritmos de alta performance. A garantia de que ela não vai vazar para a heap, e por isso não será gerenciada pelo Garbage Collector, e por isso o endereço desses objetos são determinísticos, ou seja, uma vez alocado ele não mudará, logo não é necessário fazer verificações para ver se o objeto ainda está no mesmo endereço.

Porém, não caia na armadilha de achar que Value Type é sempre mais performático que Reference Type. Antes de refatorar seu código, baseado em um post duvidoso do Linkedin (praticamente todos os posts sobre benchmark ou gerenciamento de memória são duvidosos), faça medições para ter certeza que a mudança trará ganhos!

Finalizo a parte bônus com essa imagem e recomendando a palestra ministrada pelo Andy no .NET Conf 2024, New Features in the .NET 9 JIT.


This content originally appeared on DEV Community and was authored by Pedro Jesus