As linguagens que tentam detectar overflows definiram historicamente a semântica associada de maneiras que restringiam severamente o que de outra forma seriam otimizações úteis. Entre outras coisas, embora muitas vezes seja útil realizar cálculos em uma sequência diferente daquela especificada no código, a maioria das linguagens que interceptam transbordamentos garantem que determinado código como:
for (int i=0; i<100; i++)
{
Operation1();
x+=i;
Operation2();
}
se o valor inicial de x causar um estouro na 47ª
passar pelo loop, Operation1 executará 47 vezes e Operation2
executará 46. Na ausência de tal garantia, se nada mais
dentro do loop usa x, e nada usará o valor de x seguindo
uma exceção lançada por Operation1 ou Operation2, o código poderia ser substituído
com:
x+=4950;
for (int i=0; i<100; i++)
{
Operation1();
Operation2();
}
Infelizmente, executar essas otimizações garantindo a semântica correta nos casos em que um estouro teria ocorrido dentro do loop é
difícil - essencialmente exigir algo como:
if (x < INT_MAX-4950)
{
x+=4950;
for (int i=0; i<100; i++)
{
Operation1();
Operation2();
}
}
else
{
for (int i=0; i<100; i++)
{
Operation1();
x+=i;
Operation2();
}
}
Se considerarmos que muitos códigos do mundo real usam loops mais
envolvidos, será óbvio que otimizar o código enquanto preserva
semântica de estouro é difícil. Além disso, devido a problemas de armazenamento em cache, é totalmente possível que o aumento no tamanho do código faça com que o programa geral seja executado mais lentamente, mesmo que haja menos operações no caminho normalmente executado.
O que seria necessário para tornar a detecção de estouro barato seria um
conjunto definido de semânticas de detecção de estouro mais lentas que tornariam fácil para o código relatar se uma computação foi executada sem estouros que possam ter afetado os resultados (*), mas sem sobrecarregar o compilador com detalhes além disso. Se uma especificação de idioma estivesse focada na redução do custo da detecção de excesso para o mínimo necessário para alcançar o que foi mencionado acima, ela poderia ser muito menos dispendiosa do que nos idiomas existentes. Eu não tenho conhecimento de quaisquer esforços para facilitar a detecção eficiente de estouro, no entanto.
(*) Se um idioma prometer que todos os estouros serão informados, uma expressão como x*y/y
não poderá ser simplificada para x
, a menos que seja garantido que x*y
não estourar. Da mesma forma, mesmo que o resultado de uma computação seja ignorado, uma linguagem que prometa informar todos os estouros precisará executá-la de qualquer maneira, para que possa realizar a verificação de estouro. Como o estouro em tais casos não pode resultar em um comportamento aritmeticamente incorreto, um programa não precisaria executar essas verificações para garantir que nenhum estouro de informações tenha causado resultados potencialmente imprecisos.
A propósito, os overflows em C são especialmente ruins. Embora quase todas as plataformas de hardware que suportam C99 usem a semântica silenciosa envolvente de complemento de dois, está na moda para os compiladores modernos gerar código que pode causar efeitos colaterais arbitrários em caso de estouro. Por exemplo, dado algo como:
#include <stdint.h>
uint32_t test(uint16_t x, uint16_t y) { return x*y & 65535u; }
uint32_t test2(uint16_t q, int *p)
{
uint32_t total=0;
q|=32768;
for (int i = 32768; i<=q; i++)
{
total+=test(i,65535);
*p+=1;
}
return total;
}
O GCC gerará código para o test2, que incrementa incondicionalmente (* p) uma vez e retorna 32768, independentemente do valor passado para q. Pelo seu raciocínio, o cálculo de (32769 * 65535) & 65535u causaria um estouro e, portanto, não há necessidade de o compilador considerar qualquer caso em que (q | 32768) produziria um valor maior do que 32768. Mesmo que não haja razão para que o cálculo de (32769 * 65535) & 65535u deve se preocupar com os bits superiores do resultado, o gcc usará o estouro assinado como justificativa para ignorar o loop.