Heap, Stack, Boxing and Unboxing, Performance... vamos ordenar as coisas!
Heap, Stack, Boxing and Unboxing, Performance... vamos ordenar as coisas!
Ok, então há muitos termos voando por aí e precisamos ordená-los para entender os conceitos completamente. Então vamos começar com o Heap and Stack. Veremos como essas duas estruturas afetam o boxe e o unboxing e, além disso, por que isso é caro. Então pegue um chá ou café e comece nossa jornada.
Heap and Stack - Caso de uso
Agora uma coisa para começar: nem heap nem stack são conceitos de C#. O compilador Microsoft C# emite instruções que definem se suas coisas vão para o heap ou **stack **, mas você pode escrever seu próprio compilador que recebe código C# e não tem dois tipos diferentes de armazenamento de memória.
Estamos todos familiarizados com uma Stack- "Ultimo a entrar primeiro a sair". Veja que o último elemento que colocamos no topo da pilha é o primeiro que podemos pegar. Esse mesmo comportamento que temos com nossa pilha de memória. Vejamos o seguinte trecho de código:
public void AMethod()
{
int i = 2; // Line 1
int j = 3; // Line 2
MyClass myClass = new MyClass(); // Line 3
}
Agora vamos: Na linha 1 temos a seguinte stack de memória:
-------
| Stack |
|-------|
| i = 2 |
-------
Agora vamos para a linha 2:
-------
| Stack |
|-------|
| j = 3 |
|-------|
| i = 2 |
-------
Vemos que no topo da stack colocamos a última variável (j = 3). Agora está ficando interessante, a linha 3 é uma classe que é criada. Agora temos que introduzir a segunda estrutura: Heap.
------------- -------------
| Stack | | Heap |
|-------------| references |-------------|
| myClass(ref)| ---------> | myClass obj |
|-------------| -------------
| j = 3 |
|-------------|
| i = 2 |
-------------
Ah! OK. Então agora encontramos uma classe, então um tipo de referência. E vemos que este é alocado no heap e temos uma referência na stack. Agora vamos sair do método e vamos dar uma olhada no que acontece então e abraçar a "mágica".
------- -------------
| Stack | | Heap |
|-------| |-------------|
| myClass obj |
-------------
Então, agora que saímos da nossa função, o Stack está completamente vazio, mas nosso Heap ainda existe! Olá Sr. Coletor de Lixo! Sim, essa é a única razão pela qual temos um coletor de lixo. Em algum momento seu Heap está cheio de coisas que você não precisa mais e alguém tem que cuidar.
Portanto, o Stack é local para a função (e também para o thread atual), mas o Heap não é. A pilha é, por assim dizer, uma maneira muito conveniente de modelar uma vida útil dessas variáveis. Agora, por que não salvamos tudo em uma estrutura de dados? Simplificando: ganhos maciços de desempenho. Deixe-me explicar.
Tipos de dados primitivos (como int ou double) não são complexos. Eles não consistem em referências a outros objetos, como a maioria dos tipos de referência. Se o requisito for de memória dinâmica, ela é alocada no heap ou então vai para uma pilha. Fique comigo por um segundo e você verá por que minha citação inicial da introdução não faz sentido e simplesmente não é verdade.
Antes de prosseguirmos: Por que a pilha é muito mais rápida que o heap?
Para isso podemos verificar o que acontece quando você aloca algo no heap. Se você está familiarizado com C/C++ você conhece malloc, alloc e amigos. Eles fazem o trabalho pesado. Bem, o sistema operacional subjacente faz isso tecnicamente. De qualquer forma, seu sistema operacional precisa verificar se há espaço suficiente disponível para o seu objeto reservar. Se sim, você tem que reservar isso (por favor, tenha em mente que também temos coisas como ASLR que o tornam ainda mais caro). Agora o bloco de memória está reservado. Puhhhh tipo de feito (sim, isso é simplificado).
Agora o que acontece com a stack? Incrementar o ponteiro da stack. Sim, isso é tudo. Como mostrado no meu exemplo. Por quê? Porque sua stack é alocada quando você inicia seu aplicativo e permanece o tempo todo. É por isso que você obtém StackOverflowExceptions porque a coisa que você tenta colocar não se encaixa mais. Ou porque você tem uma recursão que vai para o fundo (talvez sua condição de aborto nunca seja acionada ?? ) ou, por exemplo, você tem um grande variedade como variável local (sim, eles podem estar na pilha, mais tarde).
Agora isso era muito para digerir. Pequena recapitulação: Heap e Stack são estruturas para salvar algum tipo de estado. O heap é usado para memória dinâmica e a Stack é usada para memória estática.
Então surge a pergunta: todos os tipos de valor são considerados memória estática e todos os tipos de referência são considerados memória dinâmica? Ou em outras palavras:
"Os tipos de valor são armazenados na Stack. Tipos de referência no heap
Resposta curta: Não!
Resposta longa: Todos os tipos de referência estão indo para o heap. Essa parte é verdade. Mas nem todos os tipos de valor são armazenados sempre na Stack. E posso dar-lhe um exemplo muito simples.
int i = 0;
int y = 2;
y = i;
y = 3;
Console.Write(i);
Acho que é óbvio que a parte Console.Write(i) imprimirá apenas "0" e não "3". Mas vamos dar o seguinte exemplo
public class BetterInt
{
public int Value;
}
BetterInt i = new BetterInt();
i.Value = 0;
BetterInt y = new BetterInt();
y = i;
y.Value = 3;
Console.Write(i.MyValue);
Mas agora nosso Console imprimirá "3" em vez de "0", embora ainda tenhamos o mesmo tipo de valor. Bem, nosso tipo de valor é armazenado no heap neste caso. É necessário porque o componente proprietário também está no heap e simplesmente não podemos remover a propriedade porque o componente proprietário ainda está vivo. Para simplificar: se o escopo de propriedade do seu tipo de valor estiver no heap, seu tipo de valor também estará no heap.
Outro exemplo seriam os delegados. Os delegados podem "superar" seu escopo onde são chamados. Imagine que você tenha algo assim:
public void Init()
{
int number = 3;
someClass.OnSent += () => Console.Write(number); // This would print 3
}
Não podemos simplesmente remover o tipo de valor mesmo que ele seja usado no encerramento.
Boxing and Unboxing
Agora podemos chegar ao ponto em que tudo isso está relacionado ao boxing e unboxing.
Dê uma olhada neste código:
public void BeatBox()
{
int i = 10; // Line 1
object o = i; // Line 2
int io = (int)o; // Line 3
}
Agora objeto o = i isso é boxing. Sempre acontece implícito e isso: int io = (int)o; é unboxing que sempre acontece explícito. Dê uma olhada em nosso Stack and Heap. Primeiro, como fica na Linha 2
--------- --------
| Stack | | Heap |
|---------| references |--------|
| O(ref) | --------> | 10 |
|---------| --------
| i = 10 |
---------
Então nosso objeto o tem uma referência da stack para o heap. E por isso temos que criar esse objeto no heap. Por isso custa tempo. Nota lateral pequena: Cada ponteiro está na stack . Quando você passa tipos de referência para sua função, esse ponteiro de referência é salvo no Stack. Então boxe é quando movemos dados da stack para o heap. E o unboxing é o contrário. Vejamos a linha 3:
--------- --------
| Stack | | Heap |
|---------| | -------|
| io = 10 | <-------- | |
|---------| references | 10 |
| O(ref) | --------> | |
|---------| --------
| i = 10 |
---------
Podemos ver como o unboxing é o caminho de volta do heap para a stack. O unboxing em geral não é tão caro quanto o boxing.
Matrizes na stack
Você pode aprender que as variedades são passadas por referência, o que na maioria dos casos é verdade. Mas também acho que você já ouviu falar da palavra-chave stackalloc. Se não dá uma olhada aqui. Ele basicamente permite que você armazene uma matriz de, digamos, inteiros completamente na stack. Agora, por favor, não refatore seu código para usar stackalloc em todos os lugares em vez de variedades normais. Existem certas limitações e em quase todos os casos é melhor usar variedades "normais".
int* bar = stackalloc int [100];
Isso reservaria uma variedade na stack com 100 inteiros. Por favor, novamente: Não passe pelo seu código agora e refatore para uma pequena melhoria de desempenho e possíveis vários novos bugs introduzidos!
Resumo
Espero poder dar uma boa introdução sobre Stack, Heap e como eles estão relacionados ao boxing/unboxing, bem como tipos de valor e tipos de referência. Temos diferentes tipos de gerenciamento de memória para diferentes cenários. A pilha é rápida, mas tem, devido à sua natureza, certas limitações que a pilha não possui.
Recursos
- https://www.quora.com/When-do-value-types-get-stored-in-heap
- https://stackoverflow.com/questions/1932155/why-value-types-are-stored-onto-stacks#:~:text=Basically%20in%20the%20CLR%20memory,stack%20instead%20of%20the%20heap.
- https://www.c-sharpcorner.com/article/C-Sharp-heaping-vs-stacking-in-net-part-i/
- https://docs.microsoft.com/en-us/archive/blogs/ericlippert/the-stack-is-an-implementation-detail-part-one