Por que um replaceia novos operadores padrão e excluir?

Por que um replaceia o operador padrão como new e delete com um new personalizado e delete operadores?

Esta é a continuação do Overloading new e delete no imensamente iluminado C ++ FAQ:
Sobrecarga do operador.

Uma input de acompanhamento para esta FAQ é:
Como devo escrever o padrão ISO C ++ em conformidade com new e personalizados operadores de delete ?

Nota: A resposta é baseada nas lições de More Effective C ++ de Scott Meyers.
(Nota: Esta é uma input para o C ++ FAQ do Stack Overflow . Se você quiser criticar a idéia de fornecer um FAQ neste formulário, então o post no meta que iniciou tudo isso seria o lugar para fazer isso. essa questão é monitorada na sala de chat do C ++ , onde a ideia do FAQ começou em primeiro lugar, então é muito provável que sua resposta seja lida por aqueles que surgiram com a ideia.)

Pode-se tentar replace operadores new e delete por várias razões, a saber:

Para detectar erros de uso:

Há uma série de maneiras em que o uso incorreto de new e delete pode levar a bestas temidas de vazamento de comportamento indefinido e memory . Exemplos respectivos de cada um são:
Usando mais de uma delete na new memory ed e não chamando delete na memory alocada usando new .
Um operador sobrecarregado new pode manter uma lista de endereços alocados e a delete operador sobrecarregado pode remover endereços da lista, então é fácil detectar tais erros de uso.

Da mesma forma, vários erros de programação podem levar a excessos de dados (gravação além do final de um bloco alocado) e atrasos (gravação antes do início de um bloco alocado).
Um operador Sobrecarregado new pode sobre-alocar blocos e colocar padrões conhecidos de byte (“assinaturas”) antes e depois da memory disponibilizada aos clientes. As exclusões do operador sobrecarregado podem verificar se as assinaturas ainda estão intactas. Assim, ao verificar se essas assinaturas não estão intactas, é possível determinar que uma saturação ou subexecução ocorreu em algum momento durante a vida útil do bloco alocado, e a exclusão do operador pode registrar esse fato, juntamente com o valor do ponteiro incorreto, ajudando assim no fornecimento de uma boa informação de diagnóstico.


Para melhorar a eficiência (velocidade e memory):

Os operadores new e delete funcionam razoavelmente bem para todos, mas de forma otimizada para ninguém. Esse comportamento surge do fato de que eles são projetados apenas para uso geral. Eles têm que acomodar padrões de alocação que variam desde a alocação dinâmica de alguns blocos que existem para a duração do programa até a alocação constante e a desalocação de um grande número de objects de vida curta. Eventualmente, o operador new e o operador delete essa nave com compiladores e tomam uma estratégia de meio-caminho.

Se você tiver um bom entendimento dos padrões de uso de memory dinâmica do seu programa, muitas vezes poderá descobrir que as versões personalizadas do operador novo e do operador excluem o outperform (mais rápido no desempenho ou exigem menos memory em até 50%) que as padrão. É claro que, a menos que você tenha certeza do que está fazendo, não é uma boa ideia fazer isso (nem tente isso se não entender os meandros envolvidos).


Para coletar statistics de uso:

Antes de pensar em replace new e delete para melhorar a eficiência, como mencionado no item 2, você deve coletar informações sobre como seu aplicativo / programa usa a alocação dinâmica. Você pode coletar informações sobre:
Distribuição de blocos de alocação,
Distribuição de vidas,
Ordem de alocações (FIFO ou LIFO ou aleatória),
Entender padrões de uso muda ao longo de um período de tempo, quantidade máxima de memory dinâmica usada etc.

Além disso, às vezes você pode precisar coletar informações de uso, como:
Conte o número de objects dinamicamente de uma class,
Restringir o número de objects sendo criados usando alocação dinâmica etc.

Todas essas informações podem ser coletadas substituindo o new customizado e delete e incluindo o mecanismo de coleta de diagnóstico no new sobrecarregado e delete .


Para compensar o alinhamento de memory abaixo do ideal em new :

Muitas arquiteturas de computadores exigem que dados de tipos específicos sejam colocados na memory em tipos específicos de endereços. Por exemplo, uma arquitetura pode exigir que os pointers ocorram em endereços que sejam múltiplos de quatro (ou seja, alinhados em quatro bytes) ou que duplas devam ocorrer em endereços que sejam múltiplos de oito (ou seja, alinhados em oito bytes). O não cumprimento de tais restrições pode levar a exceções de hardware em tempo de execução. Outras arquiteturas são mais tolerantes e podem permitir que funcione, reduzindo o desempenho. O operador new com alguns compiladores não garante o alinhamento de oito bytes para alocações dinâmicas de duplas. Nesses casos, a substituição do operador padrão por um new que garanta um alinhamento de oito bytes pode gerar grandes aumentos no desempenho do programa e pode ser um bom motivo para replace os operadores new e delete .


Para agrupar objects relacionados próximos um do outro:

Se você sabe que estruturas de dados particulares são geralmente usadas juntas e você gostaria de minimizar a frequência de falhas de página ao trabalhar com os dados, pode fazer sentido criar um heap separado para as estruturas de dados para que eles sejam agrupados em apenas alguns páginas quanto possível. As versões de colocação personalizadas de new e de delete podem possibilitar esse agrupamento.


Para obter um comportamento não convencional:

Às vezes você quer que os operadores new e delete façam algo que as versões fornecidas pelo compilador não oferecem.
Por exemplo: Você pode gravar uma delete operador personalizada que substitua a memory desalocada por zeros para aumentar a segurança dos dados do aplicativo.

Primeiro de tudo, há realmente um número diferente de operadores new e de delete (um número arbitrário, na verdade).

Primeiro, existem ::operator new , ::operator new[] , ::operator delete e ::operator delete[] . Segundo, para qualquer class X , existem X::operator new , X::operator new[] , X::operator delete e X::operator delete[] .

Entre eles, é muito mais comum sobrecarregar os operadores específicos da class do que os operadores globais – é bastante comum que o uso de memory de uma determinada class siga um padrão específico suficiente para que você possa criar operadores que forneçam melhorias substanciais em relação aos padrões. Geralmente, é muito mais difícil prever o uso de memory quase exatamente ou especificamente em uma base global.

Provavelmente também vale a pena mencionar que, embora o operator new e operator new[] estejam separados um do outro (da mesma forma para qualquer X::operator new e X::operator new[] ), não há diferença entre os requisitos para os dois. Um será invocado para alocar um único object e o outro para alocar uma matriz de objects, mas cada um deles ainda recebe apenas uma quantidade de memory necessária e precisa retornar o endereço de um bloco de memory (pelo menos) tão grande.

Falando de requisitos, provavelmente vale a pena revisar os outros requisitos 1 : os operadores globais devem ser verdadeiramente globais – você não pode colocar um dentro de um namespace ou fazer um estático em uma unidade de tradução específica. Em outras palavras, existem apenas dois níveis nos quais as sobrecargas podem ocorrer: uma sobrecarga específica da class ou uma sobrecarga global. Entre pontos como “todas as classs no namespace X” ou “todas as alocações na unidade de tradução Y” não são permitidas. Os operadores específicos da class são obrigatórios para serem static – mas você não é realmente obrigado a declará-los como estáticos – eles serão estáticos se você declará-los explicitamente static ou não. Oficialmente, os operadores globais retornam muito a memory alinhada para que possa ser usada para um object de qualquer tipo. Extraoficialmente, há um pequeno espaço de manobra em um aspecto: se você receber um pedido para um bloco pequeno (por exemplo, 2 bytes) você realmente precisará fornecer memory alinhada para um object até esse tamanho, já que tentar armazenar algo maior lá levaria a um comportamento indefinido de qualquer maneira.

Tendo coberto essas preliminares, voltemos à pergunta original sobre por que você deseja sobrecarregar esses operadores. Em primeiro lugar, devo salientar que as razões para sobrecarregar os operadores globais tendem a ser substancialmente diferentes das razões para sobrecarregar os operadores específicos da class.

Como é mais comum, falarei sobre os operadores específicos da class primeiro. A principal razão para o gerenciamento de memory específico da class é o desempenho. Isso geralmente vem em uma das duas formas (ou ambas): melhorando a velocidade ou reduzindo a fragmentação. A velocidade é melhorada pelo fato de que o gerenciador de memory lida com blocos de um tamanho específico, então pode retornar o endereço de qualquer bloco livre ao invés de gastar qualquer tempo verificando se um bloco é grande o suficiente, dividindo um bloco em dois se for muito grande, etc. A fragmentação é reduzida (principalmente) da mesma maneira – por exemplo, a pré-alocação de um bloco grande o suficiente para N objects fornece exatamente o espaço necessário para N objects; alocar o valor de um object de memory alocará exatamente o espaço para um object, e não um único byte a mais.

Há uma variedade muito maior de motivos para sobrecarregar os operadores de gerenciamento de memory global. Muitos deles são orientados para debugging ou instrumentação, como o rastreamento da memory total necessária por um aplicativo (por exemplo, em preparação para portar para um sistema embarcado) ou debugging de problemas de memory, mostrando incompatibilidades entre alocar e liberar memory. Outra estratégia comum é alocar memory extra antes e depois dos limites de cada bloco solicitado e escrever padrões exclusivos nessas áreas. No final da execução (e possivelmente outras vezes também), essas áreas são examinadas para ver se o código foi escrito fora dos limites alocados. Outra é tentar melhorar a facilidade de uso automatizando pelo menos alguns aspectos de alocação ou exclusão de memory, como com um coletor de lixo automatizado .

Um alocador global não padrão pode ser usado para melhorar o desempenho também. Um caso típico seria replace um alocador padrão que era lento em geral (por exemplo, pelo menos algumas versões do MS VC ++ em torno de 4.x chamariam as funções HeapFree e HeapFree do sistema para cada operação de alocação / exclusão). Outra possibilidade que vi na prática ocorreu nos processadores Intel ao usar as operações SSE. Estes operam em dados de 128 bits. Embora as operações funcionem independentemente do alinhamento, a velocidade é aprimorada quando os dados são alinhados aos limites de 128 bits. Alguns compiladores (por exemplo, MS VC ++ novamente 2 ) não reforçaram necessariamente o alinhamento a esse limite maior, portanto, mesmo que o código usando o alocador padrão funcionasse, a substituição da alocação poderia fornecer uma melhoria substancial de velocidade para essas operações.


  1. A maioria dos requisitos são abordados em §3.7.3 e §18.4 do padrão C ++ (ou §3.7.4 e §18.6 em C ++ 0x, pelo menos a partir do N3291).
  2. Sinto-me obrigado a ressaltar que não pretendo pegar no compilador da Microsoft – duvido que tenha um número incomum de tais problemas, mas, por acaso, uso muito, por isso tenho a consciência de seus problemas.

Muitas arquiteturas de computadores exigem que dados de tipos específicos sejam colocados na memory em tipos específicos de endereços. Por exemplo, uma arquitetura pode exigir que os pointers ocorram em endereços que sejam múltiplos de quatro (ou seja, alinhados em quatro bytes) ou que duplas devam ocorrer em endereços que sejam múltiplos de oito (ou seja, alinhados em oito bytes). O não cumprimento de tais restrições pode levar a exceções de hardware em tempo de execução. Outras arquiteturas são mais tolerantes e podem permitir que funcione, reduzindo o desempenho.

Para esclarecer: se uma arquitetura requer, por exemplo, que os dados double sejam alinhados em oito bytes, então não há nada para otimizar. Qualquer tipo de alocação dinâmica do tamanho apropriado (por exemplo, malloc(size) , operator new(size) , operator new[](size) , new char[size] que size >= sizeof(double) ) é garantidamente alinhado corretamente . Se uma implementação não fizer essa garantia, ela não estará em conformidade. Alterar o operator new para fazer “a coisa certa” nesse caso seria uma tentativa de “consertar” a implementação, não uma otimização.

Por outro lado, algumas arquiteturas permitem tipos diferentes (ou todos) de alinhamento para um ou mais tipos de dados, mas fornecem garantias de desempenho diferentes dependendo do alinhamento para esses mesmos tipos. Uma implementação pode, então, retornar a memory (novamente, assumindo uma solicitação de tamanho apropriado) que esteja alinhada de modo sub-ótimo e ainda esteja em conformidade. É disso que trata o exemplo.

O operador new que inclui alguns compiladores não garante alinhamento de oito bytes para alocações dinâmicas de duplas.

Citação, por favor. Normalmente, o novo operador padrão é apenas um pouco mais complexo que um wrapper malloc, que, pelo padrão, retorna a memory que é adequadamente alinhada para QUALQUER tipo de dados que a arquitetura de destino suporta.

Não que eu esteja dizendo que não há boas razões para sobrecarregar novos e deletar para as próprias aulas … e você mencionou vários outros legítimos aqui, mas o acima não é um deles.

Relacionado a statistics de uso: orçamentação por subsistema. Por exemplo, em um jogo baseado em console, você pode reservar uma fração de memory para a geometry do modelo 3D, alguns para texturas, alguns para sons, alguns para scripts de jogos, etc. Os alocadores personalizados podem marcar cada alocação por subsistema e emitir uma aviso quando os orçamentos individuais são excedidos.

Eu usei para alocar objects em uma arena de memory compartilhada específica. (Isso é semelhante ao que @Russell Borogove mencionou.)

Anos atrás desenvolvi software para a CAVE . É um sistema VR multi-wall. Usou um computador para acionar cada projetor; 6 foi o máximo (4 paredes, piso e teto), enquanto 3 foi mais comum (2 paredes e no chão). As máquinas se comunicavam através de hardware de memory compartilhada especial.

Para apoiá-lo, eu obtive minhas classs de cena normais (não-CAVE) para usar um novo “novo” que coloca as informações da cena diretamente na arena da memory compartilhada. Em seguida, passei esse ponteiro para os renderizadores escravos nas diferentes máquinas.

Parece que vale a pena repetir a lista da minha resposta de “Qualquer motivo para sobrecarregar global novo e excluir?” aqui – veja essa resposta (ou, na verdade, outras respostas a essa pergunta ) para uma discussão mais detalhada, referências e outras razões. Esses motivos geralmente se aplicam a sobrecargas de operadores locais, bem como a sobrecargas padrão / globais e a sobrecargas ou ganchos C malloc / calloc / realloc / free .

Nós sobrecarregamos os operadores globais new e delete onde eu trabalho por vários motivos:

  • Agrupando todas as pequenas alocações – diminui a sobrecarga, diminui a fragmentação, pode aumentar o desempenho de aplicativos com alocação pequena
  • enquadrando alocações com um tempo de vida conhecido – ignore todas as liberações até o final desse período e, em seguida, liberte todas elas juntas (reconhecidamente, fazemos isso mais com sobrecargas de operadores locais do que globais)
  • ajuste de alinhamento – para limites de cacheline, etc
  • preenchimento de alocação – ajudando a expor o uso de variables ​​não inicializadas
  • preenchimento gratuito – ajudando a expor o uso de memory excluída anteriormente
  • atrasado livre – aumentando a eficácia do preenchimento livre, ocasionalmente aumentando o desempenho
  • sentinelas ou fenceposts – ajudando a expor overruns de buffer, underruns e o ocasional ponteiro wild
  • redirecionando alocações – para considerar NUMA, áreas de memory especiais, ou mesmo para manter sistemas separados separados na memory (por exemplo, linguagens de script incorporadas ou DSLs)
  • garbage collection ou limpeza – novamente útil para essas linguagens de script incorporadas
  • verificação de heap – você pode percorrer a estrutura de dados do heap a cada N alocações / liberações para garantir que tudo fique bem
  • contabilidade , incluindo rastreamento de vazamentos e instantâneos de uso / statistics (pilhas, faixas de alocação, etc)