Por que o tamanho da pilha em C # é exatamente de 1 MB?

102

Os PCs de hoje têm uma grande quantidade de RAM física, mas ainda assim, o tamanho da pilha do C # é de apenas 1 MB para processos de 32 bits e 4 MB para processos de 64 bits ( capacidade de pilha em C # ).

Por que o tamanho da pilha no CLR ainda é tão limitado?

E por que é exatamente 1 MB (4 MB) (e não 2 MB ou 512 KB)? Por que foi decidido usar esses valores?

Estou interessado em considerações e razões por trás dessa decisão .

Nikolay Kostov
fonte
6
O tamanho da pilha padrão para processos de 64 bits é 4 MB, é 1 MB para processos de 32 bits. Você pode modificar o tamanho da pilha de threads principais alterando o valor em seu cabeçalho PE. Você também pode especificar o tamanho da pilha usando a sobrecarga certa do Threadconstrutor. MAS, isso levanta a questão, por que você precisa de uma pilha maior?
Yuval Itzchakov
2
Obrigado, editado. :) A questão não é sobre como usar um tamanho de pilha maior, mas por que o tamanho da pilha foi decidido em 1 MB (4 MB) .
Nikolay Kostov
8
Porque cada thread terá esse tamanho de pilha por padrão, e a maioria dos threads não precisa de muito. Acabei de inicializar meu PC e o sistema atualmente executa 1200 threads. Agora faça as contas;)
Lucas Trzesniewski
2
@LucasTrzesniewski Não só isso, deve ser contagioso na memória . Observe que quanto maior o tamanho da pilha, menos threads seu processo pode criar em seu espaço de endereço virtual.
Yuval Itzchakov
Não tenho certeza sobre "exatamente" 1 MB: no meu Windows 8.1, um aplicativo de console .NET Core 3.1 tem 1572864tamanho de pilha padrão de bytes (recuperado usando GetCurrentThreadStackLimits Win32 API). Sou capaz de stackallocaproximadamente 1500000bytes sem StackOverflowException.
George Chakhidze

Respostas:

210

insira a descrição da imagem aqui

Você está olhando para o cara que fez essa escolha. David Cutler e sua equipe selecionaram um megabyte como o tamanho de pilha padrão. Nada a ver com .NET ou C #, isso foi resolvido quando eles criaram o Windows NT. Um megabyte é o que ele escolhe quando o cabeçalho EXE de um programa ou a chamada do winapi CreateThread () não especifica o tamanho da pilha explicitamente. Que é a forma normal, quase qualquer programador deixa que o sistema operacional escolha o tamanho.

Essa escolha provavelmente é anterior ao design do Windows NT, a história é muito obscura sobre isso. Seria bom se Cutler escrevesse um livro sobre isso, mas ele nunca foi escritor. Ele tem sido extraordinariamente influente na maneira como os computadores funcionam. Seu primeiro design de sistema operacional foi RSX-11M, um sistema operacional de 16 bits para computadores DEC (Digital Equipment Corporation). Ele influenciou fortemente o CP / M de Gary Kildall, o primeiro sistema operacional decente para microprocessadores de 8 bits. O que influenciou fortemente o MS-DOS.

Seu próximo projeto foi o VMS, um sistema operacional para processadores de 32 bits com suporte para memória virtual. Muito bem sucedido. Seu próximo foi cancelado pela DEC na época em que a empresa começou a se desintegrar, não sendo capaz de competir com hardware de PC barato. Cue Microsoft, eles lhe fizeram uma oferta que ele não podia recusar. Muitos de seus colegas de trabalho também aderiram. Eles trabalharam no VMS v2, mais conhecido como Windows NT. DEC ficou chateado com isso, o dinheiro mudou de mãos para resolver o problema. Se o VMS já escolheu um megabyte é algo que não sei, só conheço o RSX-11 bem o suficiente. Não é improvável.

Chega de história. Um megabyte é muito , um thread real raramente consome mais do que alguns poucos kilobytes. Portanto, um megabyte é realmente um desperdício. No entanto, é o tipo de desperdício que você pode pagar em um sistema operacional de memória virtual paginada por demanda, aquele megabyte é apenas memória virtual . Apenas números para o processador, um para cada 4096 bytes. Na verdade, você nunca usa a memória física, a RAM da máquina, até que a endereça de fato.

É extremamente excessivo em um programa .NET porque o tamanho de um megabyte foi originalmente escolhido para acomodar programas nativos. Que tendem a criar grandes quadros de pilha, armazenando strings e buffers (arrays) na pilha também. Infame por ser um vetor de ataque de malware, um estouro de buffer pode manipular o programa com dados. Não é a forma como os programas .NET funcionam, strings e arrays são alocados no heap do GC e a indexação é verificada. A única maneira de alocar espaço na pilha com C # é com a palavra-chave stackalloc não segura .

O único uso não trivial da pilha no .NET é por jitter. Ele usa a pilha de seu thread para compilar o MSIL para o código de máquina just-in-time. Eu nunca vi ou verifiquei quanto espaço ele requer, depende da natureza do código e se o otimizador está habilitado ou não, mas algumas dezenas de kilobytes é um palpite aproximado. Caso contrário, este site recebeu o nome, um estouro de pilha em um programa .NET é bastante fatal. Não há espaço suficiente (menos de 3 kilobytes) para executar o JIT confiável de qualquer código que tente capturar a exceção. Kaboom para desktop é a única opção.

Por último, mas não menos importante, um programa .NET faz algo bastante improdutivo com a pilha. O CLR irá comprometer a pilha de um thread. Essa é uma palavra cara que significa que ela não apenas reserva o tamanho da pilha, mas também garante que o espaço seja reservado no arquivo de paginação do sistema operacional para que a pilha possa sempre ser trocada quando necessário. Deixar de confirmar é um erro fatal e encerra um programa incondicionalmente. Isso só acontece em máquinas com pouquíssima RAM que executa muitos processos, tal máquina terá se transformado em melaço antes que os programas comecem a morrer. Um possível problema há mais de 15 anos, não hoje. Os programadores que ajustam seu programa para agir como um carro de corrida de F1 usam o <disableCommitThreadStack>elemento em seu arquivo .config.

Fwiw, Cutler não parou de projetar sistemas operacionais. Essa foto foi feita enquanto ele trabalhava no Azure.


Update, notei que o .NET não confirma mais a pilha. Não tenho certeza de quando ou por que isso aconteceu, já faz muito tempo desde que verifiquei. Acho que essa mudança de design aconteceu em algum lugar perto do .NET 4.5. Mudança bastante sensata.

Hans Passant
fonte
3
Wrt to your comment The only way to allocate space on the stack with C# is with the unsafe stackalloc keyword.- As variáveis ​​locais, por exemplo, uma intdeclarada dentro de um método, não são armazenadas na pilha? Eu acho que eles são.
RBT de
2
Está bem. Agora entendi que um quadro de pilha não é a única opção de armazenamento para variáveis ​​locais de uma função. Ele pode ser armazenado em uma estrutura de pilha, conforme sugerido em um de seus marcadores. Hans muito esclarecedor. Eu não posso dizer o suficiente em agradecimento por escrever postagens tão perspicazes. Honestamente, empilhar é uma grande abstração para a programação em geral, apenas para evitar complexidade desnecessária.
RBT de
Descrição muito detalhada @Hans. Eu estava me perguntando qual é o valor mínimo possível para maxStackSizede um segmento? Não consegui encontrar no [MSDN] ( msdn.microsoft.com/en-us/library/5cykbwz4(v=vs.110).aspx ). Com base em seus comentários, parece que o uso da pilha é o mínimo absoluto e posso usar o menor valor para acomodar o máximo possível de threads. Obrigado.
MKR
1
@KFL: Você pode responder à sua pergunta facilmente experimentando!
Eric Lippert
1
Se o comportamento padrão é não comprometer mais a pilha, então este arquivo markdown precisa ser editado github.com/dotnet/docs/blob/master/docs/framework/…
John Stewien
5

O tamanho da pilha reservada padrão é especificado pelo vinculador e pode ser substituído pelos desenvolvedores por meio da alteração do valor PE no momento do link ou para um segmento individual especificando o dwStackSizeparâmetro para a CreateThreadfunção WinAPI.

Se você criar um thread com o tamanho da pilha inicial maior ou igual ao tamanho da pilha padrão, ele será arredondado para o múltiplo mais próximo de 1 MB.

Por que o valor é igual a 1 MB para processos de 32 bits e 4 MB para 64 bits? Acho que você deve perguntar aos desenvolvedores que projetaram o Windows ou esperar até que alguém responda à sua pergunta.

Provavelmente Mark Russinovich sabe disso e você pode contatá- lo. Talvez você possa encontrar essas informações em seus livros do Windows Internals anteriores à sexta edição, que descreve menos informações sobre pilhas em vez de seu artigo . Ou talvez Raymond Chen conheça os motivos, já que escreve coisas interessantes sobre os componentes internos do Windows e sua história. Ele também pode responder à sua pergunta, mas você deve postar uma sugestão na Caixa de sugestões .

Mas, neste momento, tentarei explicar algumas razões prováveis ​​pelas quais a Microsoft escolheu esses valores usando os blogs do MSDN, Mark e Raymond.

Os padrões têm esses valores provavelmente porque nos primeiros tempos os PCs eram lentos e a alocação de memória na pilha era muito mais rápida do que a alocação de memória no heap. E como as alocações de pilha eram muito mais baratas, elas eram usadas, mas exigia um tamanho de pilha maior.

Portanto, o valor era o tamanho de pilha reservado ideal para a maioria dos aplicativos. É ideal porque permite fazer muitas chamadas aninhadas e alocar memória na pilha para passar estruturas para funções de chamada. Ao mesmo tempo, permite criar muitos tópicos.

Hoje em dia, esses valores são usados ​​principalmente para compatibilidade com versões anteriores, porque as estruturas que são passadas como parâmetros para funções WinAPI ainda são alocadas na pilha. Mas se você não estiver usando alocações de pilha, o uso de pilha de uma thread será significativamente menor do que o padrão de 1 MB e é um desperdício, como Hans Passant mencionou. E para evitar isso, o SO compromete apenas a primeira página da pilha (4 KB), se outra não for especificada no cabeçalho PE da aplicação. Outras páginas são alocadas sob demanda.

Alguns aplicativos substituem o espaço de endereço reservado e inicialmente comprometidos com a otimização do uso da memória. Como exemplo, o tamanho máximo da pilha de um thread de processo nativo do IIS é 256 KB ( KB932909 ). E esta diminuição dos valores padrão é recomendada pela Microsoft:

É melhor escolher o menor tamanho de pilha possível e comprometer a pilha necessária para que o encadeamento ou fibra seja executado de forma confiável. Cada página reservada para a pilha não pode ser usada para nenhum outro propósito.

Fontes:

  1. Tamanho da pilha de tópicos (Microsoft Docs)
  2. Extraindo os limites do Windows: processos e threads (Mark Russinovich)
  3. Por padrão, o tamanho máximo da pilha de um thread criado em um processo IIS nativo é 256 KB (KB932909)
Yoh Deadfall
fonte
Se eu quiser um tamanho de pilha maior, posso defini-lo ( atalasoft.com/cs/blogs/rickm/archive/2008/04/22/… ). Quero saber as considerações e razões por trás dessa decisão.
Nikolay Kostov
2
OK. Agora eu entendo você :) O tamanho da pilha padrão deve ser ideal (veja o comentário de @Lucas Trzesniewski) e deve ser arredondado para o múltiplo mais próximo da granularidade de alocação. Se o tamanho da pilha especificado for maior do que o tamanho da pilha padrão, ele será arredondado para o múltiplo mais próximo de 1 MB. Portanto, a Microsoft escolheu esses tamanhos como os tamanhos de pilha padrão para todos os aplicativos do modo de usuário. E não há outros motivos.
Yoh Deadfall
Alguma fonte? Qualquer documentação? :)
Nikolay Kostov
@Yoh link interessante. Você deve resumir isso em sua resposta.
Lucas Trzesniewski