Preocupações específicas da linguagem C ++
Primeiro de tudo, não há alocação de "pilha" ou "pilha" exigida pelo C ++ . Se você está falando sobre objetos automáticos em escopos de bloco, eles ainda não são "alocados". (BTW, a duração automática do armazenamento em C definitivamente NÃO é a mesma que "alocada"; a última é "dinâmica" na linguagem C ++.) A memória alocada dinamicamente está no armazenamento gratuito , não necessariamente no "heap", embora o este geralmente é a implementação (padrão) .
Embora, de acordo com as regras semânticas da máquina abstrata , os objetos automáticos ainda ocupem memória, uma implementação em C ++ em conformidade pode ignorar esse fato quando provar que isso não importa (quando não altera o comportamento observável do programa). Essa permissão é concedida pela regra como no ISO C ++, que também é a cláusula geral que permite as otimizações usuais (e também existe uma regra quase igual na ISO C). Além da regra como se, o ISO C ++ também deve regras de exclusão de cópiapermitir a omissão de criações específicas de objetos. As chamadas de construtor e destruidor envolvidas são assim omitidas. Como resultado, os objetos automáticos (se houver) nesses construtores e destruidores também são eliminados, em comparação com a semântica abstrata ingênua implícita no código-fonte.
Por outro lado, a alocação gratuita de loja é definitivamente "alocação" por design. Sob as regras ISO C ++, essa alocação pode ser alcançada através de uma chamada de uma função de alocação . No entanto, desde a ISO C ++ 14, existe uma nova regra (não como se) que permite mesclar ::operator new
chamadas da função de alocação global (ie ) em casos específicos. Portanto, partes das operações de alocação dinâmica também podem não funcionar, como no caso de objetos automáticos.
As funções de alocação alocam recursos de memória. Os objetos ainda podem ser alocados com base na alocação usando alocadores. Para objetos automáticos, eles são apresentados diretamente - embora a memória subjacente possa ser acessada e usada para fornecer memória a outros objetos (por posicionamento new
), mas isso não faz muito sentido como armazenamento gratuito, porque não há como mover o recursos em outros lugares.
Todas as outras preocupações estão fora do escopo do C ++. No entanto, eles ainda podem ser significativos.
Sobre implementações de C ++
O C ++ não expõe registros de ativação reificados ou algum tipo de continuação de primeira classe (por exemplo, pelos famosos call/cc
); não há como manipular diretamente os quadros de registro de ativação - onde a implementação precisa colocar os objetos automáticos. Uma vez que não há interoperações (não portáteis) com a implementação subjacente (código não portável "nativo", como código de montagem em linha), uma omissão da alocação subjacente dos quadros pode ser bastante trivial. Por exemplo, quando a função chamada é incorporada, os quadros podem ser mesclados efetivamente com outros, portanto não há como mostrar o que é a "alocação".
No entanto, uma vez respeitadas as interoperações, as coisas estão ficando complexas. Uma implementação típica do C ++ expõe a capacidade de interoperabilidade no ISA (arquitetura do conjunto de instruções) com algumas convenções de chamada como o limite binário compartilhado com o código nativo (máquina no nível do ISA). Isso seria explicitamente oneroso, principalmente ao manter o ponteiro da pilha , que geralmente é mantido diretamente por um registro no nível ISA (com provavelmente instruções específicas da máquina para acessar). O ponteiro da pilha indica o limite do quadro superior da chamada de função (atualmente ativa). Quando uma chamada de função é inserida, é necessário um novo quadro e o ponteiro da pilha é adicionado ou subtraído (dependendo da convenção do ISA) por um valor não inferior ao tamanho de quadro necessário. O quadro é então dito alocadoquando o ponteiro da pilha após as operações. Parâmetros de funções também podem ser passados para o quadro da pilha, dependendo da convenção de chamada usada para a chamada. O quadro pode conter a memória de objetos automáticos (provavelmente incluindo os parâmetros) especificados pelo código-fonte C ++. No sentido de tais implementações, esses objetos são "alocados". Quando o controle sai da chamada de função, o quadro não é mais necessário, geralmente é liberado restaurando o ponteiro da pilha de volta ao estado anterior à chamada (salvo anteriormente de acordo com a convenção de chamada). Isso pode ser visto como "desalocação". Essas operações tornam o registro de ativação efetivamente uma estrutura de dados LIFO, por isso costuma ser chamada de " pilha (chamada) ".
Como a maioria das implementações em C ++ (principalmente as que visam o código nativo no nível ISA e usam a linguagem assembly como saída imediata) usam estratégias semelhantes como essa, um esquema de "alocação" tão confuso é popular. Essas alocações (bem como desalocações) passam ciclos de máquina e podem ser caras quando as chamadas (não otimizadas) ocorrem com frequência, mesmo que as microarquiteturas de CPU modernas possam ter otimizações complexas implementadas por hardware para o padrão de código comum (como usar um empilhar o mecanismo na implementação PUSH
/ POP
instruções).
Mas, de qualquer maneira, em geral, é verdade que o custo da alocação de quadros de pilha é significativamente menor do que uma chamada para uma função de alocação que opera o armazenamento gratuito (a menos que seja totalmente otimizado) , o que em si pode ter centenas (se não milhões de :-) operações para manter o ponteiro da pilha e outros estados. As funções de alocação geralmente são baseadas na API fornecida pelo ambiente hospedado (por exemplo, tempo de execução fornecido pelo sistema operacional). Diferente do objetivo de reter objetos automáticos para chamadas de funções, essas alocações são de uso geral, portanto, elas não terão estrutura de quadro como uma pilha. Tradicionalmente, eles alocam espaço do armazenamento de pool chamado heap (ou vários heaps). Diferente da "pilha", o conceito "pilha" aqui não indica a estrutura de dados que está sendo usada;é derivado de implementações de idiomas anteriores décadas atrás . (BTW, a pilha de chamadas geralmente é alocada com tamanho fixo ou especificado pelo usuário do heap pelo ambiente na inicialização do programa ou do encadeamento.) A natureza dos casos de uso torna as alocações e desalocações de um heap muito mais complicadas (do que pressionar ou soltar). quadros de pilha) e dificilmente possível de ser otimizado diretamente pelo hardware.
Efeitos no acesso à memória
A alocação de pilha usual sempre coloca o novo quadro no topo, por isso possui uma boa localidade. Isso é amigável para o cache. OTOH, a memória alocada aleatoriamente no armazenamento gratuito não possui essa propriedade. Desde o ISO C ++ 17, existem modelos de recursos de pool fornecidos por <memory>
. O objetivo direto dessa interface é permitir que os resultados de alocações consecutivas sejam próximos na memória. Isso reconhece o fato de que essa estratégia geralmente é boa para desempenho com implementações contemporâneas, por exemplo, ser amigável para armazenar em cache em arquiteturas modernas. É sobre o desempenho do acesso, e não da alocação .
Concorrência
A expectativa de acesso simultâneo à memória pode ter efeitos diferentes entre a pilha e as pilhas. Uma pilha de chamadas geralmente pertence exclusivamente a um encadeamento de execução em uma implementação C ++. OTOH, heaps geralmente são compartilhados entre os threads em um processo. Para esses heaps, as funções de alocação e desalocação precisam proteger a estrutura de dados administrativos internos compartilhados da corrida de dados. Como resultado, alocações e desalocações de heap podem ter uma sobrecarga adicional devido a operações de sincronização interna.
Eficiência espacial
Devido à natureza dos casos de uso e das estruturas de dados internas, os heaps podem sofrer fragmentação da memória interna , enquanto a pilha não. Isso não afeta diretamente o desempenho da alocação de memória, mas em um sistema com memória virtual , a baixa eficiência de espaço pode piorar o desempenho geral do acesso à memória. Isso é particularmente terrível quando o HDD é usado como uma troca de memória física. Pode causar latência bastante longa - às vezes bilhões de ciclos.
Limitações de alocações de pilha
Embora as alocações de pilha geralmente tenham desempenho superior às alocações de heap na realidade, isso certamente não significa que as alocações de pilha sempre possam substituir as alocações de heap.
Primeiro, não há como alocar espaço na pilha com um tamanho especificado em tempo de execução de maneira portátil com o ISO C ++. Existem extensões fornecidas por implementações como alloca
o VLA (matriz de comprimento variável) do G ++, mas existem razões para evitá-las. (IIRC, a fonte Linux remove o uso do VLA recentemente.) (Observe também que a ISO C99 possui o VLA obrigatório, mas a ISO C11 torna o suporte opcional.)
Segundo, não há uma maneira confiável e portátil de detectar a exaustão do espaço na pilha. Isso geralmente é chamado de estouro de pilha (hmm, a etimologia deste site) , mas provavelmente com mais precisão, estouro de pilha . Na realidade, isso geralmente causa acesso inválido à memória e o estado do programa é corrompido (... ou talvez pior, uma falha de segurança). De fato, o ISO C ++ não tem um conceito de "pilha" e o torna indefinido quando o recurso está esgotado . Tenha cuidado com a quantidade de espaço que resta para objetos automáticos.
Se o espaço da pilha acabar, há muitos objetos alocados na pilha, que podem ser causados por chamadas de funções ativas ou uso inadequado de objetos automáticos. Tais casos podem sugerir a existência de erros, por exemplo, uma chamada de função recursiva sem condições corretas de saída.
No entanto, chamadas recursivas profundas às vezes são desejadas. Nas implementações de idiomas que exigem suporte a chamadas ativas não acopladas (onde a profundidade da chamada é limitada apenas pela memória total), é impossível usar a pilha de chamadas nativa (contemporânea) diretamente como o registro de ativação do idioma de destino, como implementações típicas de C ++. Para contornar o problema, são necessárias formas alternativas de construção dos registros de ativação. Por exemplo, SML / NJ aloca explicitamente quadros na pilha e usa pilhas de cactos . A alocação complicada desses quadros de registro de ativação geralmente não é tão rápida quanto os quadros da pilha de chamadas. No entanto, se essas linguagens forem implementadas ainda mais com a garantia de recursão adequada da cauda, a alocação direta da pilha no idioma do objeto (ou seja, o "objeto" no idioma não é armazenado como referências, mas os valores primitivos nativos que podem ser mapeados individualmente para objetos C ++ não compartilhados) são ainda mais complicados com mais penalidade de desempenho em geral. Ao usar o C ++ para implementar essas linguagens, é difícil estimar os impactos no desempenho.