Eu estou pensando sobre o uso de código como o seguinte
int result = 0;
int factor = 1;
for (...) {
result = ...
factor *= 10;
}
return result;
Se o loop for iterado ao longo do n
tempo, factor
será multiplicado 10
exatamente pelo n
tempo. No entanto, factor
só é usado depois de multiplicado pelo 10
total de n-1
vezes. Se assumirmos que factor
nunca estouramos, exceto na última iteração do loop, mas podemos estourar na última iteração do loop, esse código deve ser aceitável? Nesse caso, o valor de factor
provavelmente nunca seria usado após o estouro.
Estou discutindo se códigos como esse devem ser aceitos. Seria possível colocar a multiplicação dentro de uma instrução if e simplesmente não fazer a multiplicação na última iteração do loop, quando ele pode transbordar. A desvantagem é que ele atrapalha o código e adiciona uma ramificação desnecessária que precisaria verificar todas as iterações anteriores do loop. Eu também poderia iterar o loop menos uma vez e replicar o corpo do loop uma vez após o loop, novamente, isso complica o código.
O código real em questão é usado em um loop interno rígido que consome grande parte do tempo total da CPU em um aplicativo gráfico em tempo real.
Respostas:
Compiladores assumem que um programa C ++ válido não contém UB. Considere, por exemplo:
Se,
x == nullptr
então, derreferenciá-lo e atribuir um valor é UB. Portanto, a única maneira de isso terminar em um programa válido é quandox == nullptr
nunca produzirá true e o compilador pode assumir a regra como se, o acima é equivalente a:Agora no seu código
A última multiplicação de
factor
não pode acontecer em um programa válido (o estouro assinado é indefinido). Daí também a tarefa deresult
não poder acontecer. Como não há como ramificar antes da última iteração, a iteração anterior também não pode acontecer. Eventualmente, a parte do código correta (ou seja, nenhum comportamento indefinido acontece) é:fonte
INT_MAX >= 10000000000
, com uma função diferente chamada no caso em queINT_MAX
for menor.i <= n
loops são sempre não infinitos, comoi<n
loops. E promovaint i
a largura do ponteiro em um loop, em vez de precisar refazer a assinatura para possível indexação da matriz de agrupamento aos primeiros elementos da matriz 4G.O comportamento do
int
estouro é indefinido.Não importa se você lê
factor
fora do corpo do loop; se ele estourou até então, o comportamento do seu código foi depois, e um tanto paradoxalmente, antes que o estouro seja indefinido.Um problema que pode surgir para manter esse código é que os compiladores estão ficando cada vez mais agressivos quando se trata de otimização. Em particular, eles estão desenvolvendo um hábito em que assumem que um comportamento indefinido nunca acontece. Para que isso aconteça, eles podem remover o
for
loop completamente.Você não pode usar um
unsigned
tipo parafactor
embora então você precisa se preocupar com a conversão indesejada deint
queunsigned
em expressões contendo tanto?fonte
factor
é "usada" na tarefa de volta a si mesma.Pode ser interessante considerar otimizadores do mundo real. Desenrolar o loop é uma técnica conhecida. A idéia básica de desenrolar o loop op é que
pode ser melhor implementado nos bastidores como
Este é o caso fácil, com um limite fixo. Mas os compiladores modernos também podem fazer isso por limites variáveis:
torna-se
Obviamente isso só funciona se o compilador souber que N <= 3. E é aí que voltamos à pergunta original. Como o compilador sabe que o estouro assinado não ocorre , ele sabe que o loop pode executar no máximo 9 vezes em arquiteturas de 32 bits.
10^10 > 2^32
. Portanto, ele pode fazer um loop de 9 iterações. Mas o máximo pretendido foi de 10 iterações! .O que pode acontecer é que você obtenha um salto relativo em uma instrução de montagem (9-N) com N = 10, portanto, um deslocamento de -1, que é a própria instrução de salto. Opa Essa é uma otimização de loop perfeitamente válida para C ++ bem definido, mas o exemplo dado se transforma em um loop infinito apertado.
fonte
Qualquer excesso de número inteiro assinado resulta em comportamento indefinido, independentemente de o valor excedido ser ou não lido ou não.
Talvez no seu caso de uso você possa tirar a primeira iteração do loop, transformando isso
nisso
Com a otimização ativada, o compilador pode desenrolar o segundo loop acima em um salto condicional.
fonte
factor *= 10;
Este é UB; em termos de ISO C ++, todo o comportamento de todo o programa é completamente não especificado para uma execução que eventualmente atinge o UB. O exemplo clássico é que, na medida em que o padrão C ++ se importa, ele pode fazer com que os demônios voem do seu nariz. (Eu recomendo não usar uma implementação em que os demônios nasais são uma possibilidade real). Veja outras respostas para mais detalhes.
Os compiladores podem "causar problemas" em tempo de compilação para os caminhos de execução que eles podem ver levando a UB visível em tempo de compilação, por exemplo, suponha que esses blocos básicos nunca sejam atingidos.
Consulte também O que todo programador C deve saber sobre comportamento indefinido (blog do LLVM). Conforme explicado lá, o UB de estouro assinado permite que os compiladores provem que os
for(... i <= n ...)
loops não são infinitos, mesmo para desconhecidosn
. Ele também permite que eles "promovam" contadores de loop int para a largura do ponteiro, em vez de refazer a extensão do sinal. (Portanto, a conseqüência do UB nesse caso poderia ser acessar fora dos elementos baixos de 64k ou 4G de uma matriz, se você estivesse esperando uma quebra automática assinadai
em seu intervalo de valores.)Em alguns casos, os compiladores emitem uma instrução ilegal como x86
ud2
para um bloco que provavelmente causa o UB, se alguma vez for executado. (Observe que uma função nem sempre pode ser chamada, portanto, em geral, os compiladores não podem ficar furiosos e interromper outras funções, ou mesmo caminhos possíveis através de uma função que não atinge UB. todas as entradas que não levam ao UB.)Provavelmente, a solução mais eficiente é descascar manualmente a última iteração para
factor*=10
evitar o desnecessário .Ou se o corpo do loop for grande, considere simplesmente usar um tipo não assinado para
factor
. Em seguida, você pode deixar o multiplicador sem sinal estourar e ele fará um empacotamento bem definido com uma potência de 2 (o número de bits de valor no tipo não assinado).Isso é bom mesmo se você usá-lo com tipos assinados, especialmente se sua conversão não assinada-> nunca exceder.
A conversão entre o complemento não assinado e o do 2 assinado é livre (o mesmo padrão de bits para todos os valores); o módulo para int -> unsigned especificado pelo padrão C ++ simplifica o uso do mesmo padrão de bits, diferente do complemento ou sinal / magnitude.
E não assinado-> assinado é igualmente trivial, embora seja definido pela implementação para valores maiores que
INT_MAX
. Se você não estiver usando o enorme resultado não assinado da última iteração, não terá com que se preocupar. Mas, se estiver, consulte A conversão de não assinado em assinado é indefinida? . O caso do valor não se encaixa é definido pela implementação , o que significa que uma implementação deve escolher algum comportamento; os sãos apenas truncam (se necessário) o padrão de bits não assinado e o usam como assinado, porque isso funciona para valores dentro do intervalo da mesma maneira, sem trabalho extra. E definitivamente não é UB. Assim, grandes valores não assinados podem se tornar inteiros com sinal negativo. por exemplo, depois deint x = u;
gcc e clang não otimizarx>=0
como sempre sendo verdade, mesmo sem-fwrapv
, porque eles definiram o comportamento.fonte
Se você puder tolerar algumas instruções adicionais de montagem no loop, em vez de
você pode escrever:
para evitar a última multiplicação.
!factor
não introduzirá um ramo:Este código
também resulta em montagem sem ramificação após a otimização:
(Compilado com o GCC 8.3.0
-O3
)fonte
factor
ligeiramente a latência da cadeia de dependência transportada por loop . Ou não: quando ele compila para 2x LEA é quase tão eficiente quanto LEA + ADD fazerf *= 10
comof*5*2
, comtest
latência escondida pelo primeiroLEA
. Mas custa UOPs extras dentro do loop por isso há uma possível desvantagem rendimento (ou pelo menos uma questão hyperthreading de uso)Você não mostrou o que está entre parênteses da
for
declaração, mas vou assumir que é algo como isto:Você pode simplesmente mover a verificação do incremento do contador e da terminação do loop para o corpo:
O número de instruções de montagem no loop permanecerá o mesmo.
Inspirado na apresentação de Andrei Alexandrescu "A velocidade é encontrada nas mentes das pessoas".
fonte
Considere a função:
De acordo com a Justificativa publicado, os autores do padrão teria esperado que, se esta função foi chamado em (por exemplo) um computador de 32 bits comum com argumentos de 0xC000 e 0xC000, promovendo os operandos de
*
quesigned int
faria com que a computação para produzir -0x10000000 , que quando convertidos emunsigned
produziriam0x90000000u
- a mesma resposta como se eles tivessemunsigned short
promovido aunsigned
. No entanto, o gcc às vezes otimiza essa função de maneiras que se comportariam sem sentido se ocorrer um estouro. Qualquer código em que alguma combinação de entradas possa causar um estouro deve ser processado com a-fwrapv
opção, a menos que seja aceitável permitir que criadores de entrada malformada deliberadamente executem código arbitrário de sua escolha.fonte
Por que não isso:
fonte
...
corpo do loop parafactor = 1
oufactor = 10
, a apenas 100 e superior. Você precisaria descascar a primeira iteração e ainda começarfactor = 1
se quiser que isso funcione.Existem muitas faces diferentes do comportamento indefinido e o aceitável depende do uso.
Isso, por si só, é um pouco incomum, mas, seja como for ... se esse for realmente o caso, o UB provavelmente estará dentro do domínio "permitido, aceitável" . A programação gráfica é notória por hacks e coisas feias. Desde que "funcione" e não demore mais que 16,6 ms para produzir um quadro, geralmente ninguém se importa. Mas, esteja ciente do que significa invocar o UB.
Primeiro, existe o padrão. Desse ponto de vista, não há nada para discutir e nenhuma maneira de justificar, seu código é simplesmente inválido. Não há ifs ou whens, apenas não é um código válido. Você também pode dizer que está com o dedo do meio levantado do seu ponto de vista e, em 95-99% das vezes, é bom que você vá.
Em seguida, há o lado do hardware. Existem algumas arquiteturas incomuns e estranhas , onde isso é um problema. Estou dizendo "incomum, estranho" porque, na única arquitetura que compõe 80% de todos os computadores (ou nas duas arquiteturas que juntas compõem 95% de todos os computadores), o estouro é um "sim, tanto faz, não importa" coisa no nível do hardware. Você com certeza consegue um resultado de lixo (embora ainda previsível), mas nada de ruim acontece.
Isso não éEm todas as arquiteturas, é bem possível que você fique preso em excesso (embora, vendo como você fala de um aplicativo gráfico, as chances de estar em uma arquitetura tão estranha sejam bastante pequenas). A portabilidade é um problema? Se for, você pode querer se abster.
Por último, existe o lado do compilador / otimizador. Uma razão pela qual o estouro é indefinido é que simplesmente deixá-lo como era mais fácil lidar com o hardware era uma vez. Mas outro motivo é que, por exemplo,
x+1
é garantido que sempre será maior quex
, e o compilador / otimizador pode explorar esse conhecimento. Agora, no caso mencionado anteriormente, é sabido que os compiladores agem dessa maneira e simplesmente eliminam os blocos completos (existia uma exploração do Linux há alguns anos atrás, que se baseava no fato de o compilador ter retirado completamente algum código de validação por causa disso).No seu caso, duvido seriamente que o compilador faça algumas otimizações especiais e ímpares. No entanto, o que você sabe, o que eu sei. Em caso de dúvida, experimente. Se funcionar, você está pronto para ir.
(E, finalmente, é claro que existe uma auditoria de código, você pode ter que perder seu tempo discutindo isso com um auditor, se tiver azar.)
fonte