O compilador tem permissão para otimizar as alocações de memory de heap?

Considere o seguinte código simples que faz uso de new (estou ciente de que não há delete[] , mas não pertence a essa pergunta):

 int main() { int* mem = new int[100]; return 0; } 

O compilador tem permissão para otimizar a new chamada?

Na minha pesquisa, o g ++ (5.2.0) e o Visual Studio 2015 não otimizam a new chamada, enquanto o clang (3.0+) faz . Todos os testes foram feitos com otimizações completas ativadas (-O3 para g ++ e clang, modo Release para Visual Studio).

Não é new fazer uma chamada do sistema sob o capô, tornando impossível (e ilegal) para um compilador otimizar isso?

EDIT : agora excluí o comportamento indefinido do programa:

 #include  int main() { int* mem = new (std::nothrow) int[100]; return 0; } 

clang 3.0 não otimiza isso mais, mas versões posteriores fazem .

EDIT2 :

 #include  int main() { int* mem = new (std::nothrow) int[1000]; if (mem != 0) return 1; return 0; } 

clang sempre retorna 1 .

A história parece ser que o clang está seguindo as regras estabelecidas no N3664: Clarifying Memory Allocation, que permite ao compilador otimizar as alocações de memory, mas como Nick Lewycky aponta :

Shafik apontou que parece violar a causalidade, mas o N3664 começou como N3433, e tenho certeza que escrevemos a otimização primeiro e escrevemos o artigo depois.

Então o clang implementou a otimização que mais tarde se tornou uma proposta que foi implementada como parte do C ++ 14.

A questão básica é se esta é uma otimização válida antes do N3664 , que é uma questão difícil. Nós teríamos que ir para a regra como se coberta no esboço da seção padrão C ++ 1.9 Execução do programa que diz ( ênfase minha ):

As descrições semânticas nesta Norma definem uma máquina abstrata não determinística parametrizada. Este Padrão Internacional não impõe nenhuma exigência na estrutura de implementações em conformidade. Em particular, eles não precisam copiar ou emular a estrutura da máquina abstrata. Em vez disso, é necessário que as implementações em conformidade imitem (apenas) o comportamento observável da máquina abstrata, conforme explicado abaixo. 5

onde nota 5 diz:

Esta provisão é às vezes chamada de regra “como se” , porque uma implementação é livre para desconsiderar qualquer exigência desta Norma desde que o resultado seja como se a exigência tivesse sido obedecida, tanto quanto possível a partir do comportamento observável. do programa. Por exemplo, uma implementação real não precisa avaliar parte de uma expressão se puder deduzir que seu valor não é usado e que nenhum efeito colateral que afete o comportamento observável do programa é produzido.

Uma vez que o new poderia lançar uma exceção que teria um comportamento observável, uma vez que alteraria o valor de retorno do programa, isso pareceria argumentar contra a permissão da regra como-se .

Embora, possa-se argumentar que é um detalhe de implementação quando lançar uma exceção e, portanto, o clang poderia decidir que, mesmo nesse cenário, não causaria uma exceção e, portanto, a omissão da new chamada não violaria a regra como-se .

Também parece válido sob a regra de como se otimizar a chamada para a versão não lançada também.

Mas poderíamos ter um operador global substituto novo em uma unidade de tradução diferente, o que poderia fazer com que isso afetasse o comportamento observável, portanto, o compilador teria que provar que esse não era o caso, caso contrário, ele não seria capaz de realizar essa otimização sem violar a regra como-se . As versões anteriores do clang de fato otimizaram neste caso, como mostra este exemplo de godbolt que foi fornecido via Casey aqui , tomando este código:

 #include  extern void* operator new(std::size_t n); template T* create() { return new T(); } int main() { auto result = 0; for (auto i = 0; i < 1000000; ++i) { result += (create() != nullptr); } return result; } 

e otimizando isso para isso:

 main: # @main movl $1000000, %eax # imm = 0xF4240 ret 

Isso realmente parece muito agressivo, mas versões posteriores não parecem fazer isso.

Isso é permitido pelo N3664 .

Uma implementação pode omitir uma chamada para uma function de alocação global substituível (18.6.1.1, 18.6.1.2). Quando isso acontece, o armazenamento é fornecido pela implementação ou fornecido pela extensão da alocação de outra nova expressão.

Esta proposta é parte do padrão C ++ 14, portanto, em C ++ 14, o compilador tem permissão para otimizar uma new expressão (mesmo que ela possa lançar).

Se você der uma olhada no status de implementação do Clang, ele afirma claramente que implementa o N3664.

Se você observar este comportamento durante a compilation em C ++ 11 ou C ++ 03, você deve preencher um erro.

Observe que antes das alocações de memory dinâmica do C ++ 14 fazer parte do status observável do programa (embora eu não possa encontrar uma referência para isso no momento), então uma implementação em conformidade não foi permitida para aplicar a regra como-se neste caso.

Tenha em mente que o padrão C ++ informa o que um programa correto deve fazer, e não como deve ser feito. Não é possível dizer mais tarde, já que as novas arquiteturas podem surgir e surgem depois que o padrão é escrito e o padrão deve ser útil para elas.

new não precisa ser uma chamada do sistema sob o capô. Existem computadores utilizáveis ​​sem sistemas operacionais e sem um conceito de chamada de sistema.

Portanto, desde que o comportamento final não seja alterado, o compilador pode otimizar tudo e todos. Incluindo esse new

Existe uma ressalva.
Um operador global de substituição novo poderia ter sido definido em um bloco de tradução diferente
Nesse caso, os efeitos colaterais do novo podem ser tais que não podem ser otimizados. Mas se o compilador puder garantir que o novo operador não tenha efeitos colaterais, como seria o caso se o código lançado for o código inteiro, a otimização será válida.
Esse novo pode lançar std :: bad_alloc não é um requisito. Nesse caso, quando new é otimizado, o compilador pode garantir que nenhuma exceção será lançada e nenhum efeito colateral ocorrerá.

É perfeitamente permitido (mas não obrigatório ) para um compilador otimizar as alocações em seu exemplo original e, mais ainda, no exemplo EDIT1 por §1.9 da norma, que geralmente é referida como regra as-if :

Implementações em conformidade são necessárias para emular (apenas) o comportamento observável da máquina abstrata, conforme explicado abaixo:
[3 páginas de condições]

Uma representação mais legível por humanos está disponível em cppreference.com .

Os pontos relevantes são:

  • Você não tem voláteis, então 1) e 2) não se aplicam.
  • Você não imprime / grava nenhum dado nem solicita ao usuário, portanto, 3) e 4) não se aplicam. Mas mesmo se você o fizesse, eles claramente estariam satisfeitos com EDIT1 (discutivelmente também no exemplo original, embora de um ponto de vista puramente teórico, isso é ilegal, pois o stream e a saída do programa – teoricamente – diferem, mas veja dois parágrafos abaixo).

Uma exceção, mesmo que não seja detectada, é um comportamento bem definido (não indefinido!). No entanto, estritamente falando, no caso de new lançamentos (não vai acontecer, ver também o próximo parágrafo), o comportamento observável seria diferente, tanto pelo código de saída do programa e por qualquer saída que pode seguir mais tarde no programa.

Agora, no caso particular de uma alocação pequena singular, você pode dar ao compilador o “benefício da dúvida” que pode garantir que a alocação não falhará.
Mesmo em um sistema com muita pressão de memory, não é possível nem mesmo iniciar um processo quando você tem menos do que a granularidade de alocação mínima disponível, e o heap também foi configurado antes da chamada main . Então, se esta alocação falhar, o programa nunca iniciaria ou já teria encontrado um final descomprometido antes que o main fosse chamado.
Na medida em que, assumindo que o compilador sabe disso, mesmo que a alocação possa, teoricamente , ser descartada , é legal até otimizar o exemplo original, já que o compilador pode praticamente garantir que isso não acontecerá.

Por outro lado, não é permitido (e, como você pode observar, um erro do compilador) otimizar a alocação no seu exemplo EDIT2. O valor é consumido para produzir um efeito externamente observável (o código de retorno).
Note que se você replace new (std::nothrow) int[1000] por new (std::nothrow) int[1024*1024*1024*1024ll] (que é uma alocação de 4TiB!), Que é – em computadores atuais – garantido para falhar, ainda otimiza a chamada. Em outras palavras, ele retorna 1, embora você tenha escrito código que deve gerar 0.

@Yakk levantou um bom argumento contra isso: enquanto a memory nunca é tocada, um ponteiro pode ser retornado, e não é necessária a RAM real. Até mesmo seria legítimo otimizar a alocação em EDIT2. Não tenho certeza de quem está certo e quem está errado aqui.

Fazer uma alocação de 4 TB é praticamente garantido para falhar em uma máquina que não tenha, pelo menos, algo como uma quantidade de gigabytes de dois dígitos, simplesmente porque o SO precisa criar tabelas de páginas. Agora, é claro, o padrão C ++ não se importa com as tabelas de páginas ou com o que o sistema operacional está fazendo para fornecer memory, isso é verdade.

Mas, por outro lado, a suposição “isso funcionará se a memory não for tocada” depende exatamente de tal detalhe e de algo que o sistema operacional fornece. A suposição de que, se a RAM que não é tocada, não for realmente necessária, ela é verdadeira apenas porque o sistema operacional fornece memory virtual. E isso implica que o sistema operacional precisa criar tabelas de páginas (posso fingir que não sei sobre isso, mas isso não muda o fato de que eu dependo dele de qualquer maneira).

Portanto, acho que não é 100% correto primeiro assumir um e depois dizer “mas não nos importamos com o outro”.

Então, sim, o compilador pode assumir que uma alocação de 4TiB é em geral perfeitamente possível, desde que a memory não seja tocada, e pode-se supor que geralmente é possível ter sucesso. Pode até supor que é provável que tenha sucesso (mesmo quando não é). Mas acho que, em qualquer caso, você nunca está autorizado a assumir que algo deve funcionar quando existe a possibilidade de uma falha. E não existe apenas uma possibilidade de falha, nesse exemplo, o fracasso é a possibilidade mais provável .

O pior que pode acontecer no seu trecho é que o new lança std::bad_alloc , que é não manipulado. O que acontece, então, é definido pela implementação.

Com o melhor dos casos sendo um no-op e o pior caso não sendo definido, o compilador tem permissão para incorporá-los à inexistência. Agora, se você realmente tentar pegar a exceção possível:

 int main() try { int* mem = new int[100]; return 0; } catch(...) { return 1; } 

… então a chamada para operator new é mantida .