Por que o estouro de número inteiro não assinado é definido, mas o excesso de número inteiro assinado não é?

209

Estouro de número inteiro não assinado é bem definido pelos padrões C e C ++. Por exemplo, o padrão C99 ( §6.2.5/9) declara

Uma computação envolvendo operandos não assinados nunca pode transbordar, porque um resultado que não pode ser representado pelo tipo inteiro não assinado resultante é reduzido pelo módulo, o número que é um maior que o maior valor que pode ser representado pelo tipo resultante.

No entanto, ambos os padrões afirmam que o excesso de número inteiro assinado é um comportamento indefinido. Mais uma vez, do padrão C99 ( §3.4.3/1)

Um exemplo de comportamento indefinido é o comportamento no overflow de número inteiro

Existe uma razão histórica ou (ainda melhor!) Técnica para essa discrepância?

Anthony Vallée-Dubois
fonte
50
Provavelmente porque há mais de uma maneira de representar números inteiros assinados. Qual o caminho não especificado no padrão, pelo menos não no C ++.
Juanchopanza
7
O que juanchopanza disse faz sentido. Pelo que entendi, o padrão C original, em grande parte, codifica a prática existente. Se todas as implementações da época concordassem com o que o "estouro" não assinado deveria fazer, esse é um bom motivo para padronizá-lo. Eles não concordaram com o que o overflow assinado deveria fazer, para que não chegasse ao padrão.
2
@DavidElliman Uma assinatura não assinada na adição também é facilmente detectável ( if (a + b < a)). O estouro na multiplicação é difícil para os tipos assinado e não assinado.
5
@ DavidElliman: Não é apenas uma questão de saber se você pode detectá-lo, mas qual é o resultado. Em uma implementação valor sinal +, MAX_INT+1 == -0, enquanto em um de complemento de dois seriaINT_MIN
David Rodríguez - dribeas

Respostas:

163

O motivo histórico é que a maioria das implementações de C (compiladores) apenas usava qualquer comportamento de estouro mais fácil de implementar com a representação inteira usada. As implementações C usavam geralmente a mesma representação usada pela CPU - portanto, o comportamento de estouro seguia da representação inteira usada pela CPU.

Na prática, são apenas as representações dos valores assinados que podem diferir de acordo com a implementação: complemento de alguém, complemento de dois, magnitude de sinal. Para um tipo não assinado, não há razão para o padrão permitir variação, porque existe apenas uma representação binária óbvia (o padrão permite apenas representação binária).

Citações relevantes:

C99 6.2.6.1:3 :

Os valores armazenados em campos de bits não assinados e objetos do tipo char não assinado devem ser representados usando uma notação binária pura.

C99 6.2.6.2:2 :

Se o bit do sinal for um, o valor deve ser modificado de uma das seguintes maneiras:

- o valor correspondente com o bit de sinal 0 é negado ( sinal e magnitude );

- o bit de sinal tem o valor - (2 N ) ( complemento de dois );

- o bit de sinal tem o valor - (2 N - 1) ( complemento de alguém ).


Atualmente, todos os processadores usam a representação de complemento de dois, mas o estouro aritmético assinado permanece indefinido e os fabricantes de compiladores querem que ele permaneça indefinido, porque usam essa indefinição para ajudar na otimização. Veja, por exemplo, este post de Ian Lance Taylor ou esta reclamação de Agner Fog e as respostas para o relatório de erros.

Pascal Cuoq
fonte
6
A nota importante aqui, no entanto, é que ainda não existem arquiteturas no mundo moderno usando nada além da aritmética assinada do complemento de 2. O fato de os padrões de linguagem ainda permitirem a implementação em, por exemplo, um PDP-1, é um artefato histórico puro.
Andy Ross
9
@AndyRoss mas ainda existem sistemas (+ compiladores OS, reconhecidamente com uma história antiga) com um complemento e novos lançamentos a partir de 2013. Um exemplo: OS 2200.
ouah
3
@ Andy Ross, você consideraria "nenhuma arquitetura ... usando nada além do complemento de 2 ..." hoje inclui a gama de DSPs e processadores embarcados?
chux - Restabelece Monica 12/08
11
@AndyRoss: Embora existam arquiteturas “no” usando algo que não seja complemento 2s (para alguma definição de “não”), definitivamente existem arquiteturas DSP que usam aritmética saturante para números inteiros assinados.
Stephen Canon
10
A aritmética assinada saturada é definitivamente compatível com o padrão. Obviamente, as instruções de empacotamento devem ser usadas para aritmética não assinada, mas o compilador sempre tem as informações para saber se a aritmética não assinada ou assinada está sendo executada, para que certamente possa escolher as instruções adequadamente.
caf
15

Além da boa resposta de Pascal (que tenho certeza de que é a principal motivação), também é possível que alguns processadores causem uma exceção no estouro de número inteiro assinado, o que obviamente causaria problemas se o compilador tivesse que "providenciar outro comportamento" ( por exemplo, use instruções extras para verificar se há um potencial estouro e calcular de maneira diferente nesse caso).

Também é importante notar que "comportamento indefinido" não significa "não funciona". Isso significa que a implementação está autorizada a fazer o que quiser nessa situação. Isso inclui fazer "a coisa certa", bem como "chamar a polícia" ou "bater". A maioria dos compiladores, quando possível, escolhe "fazer a coisa certa", assumindo que é relativamente fácil de definir (nesse caso, é). No entanto, se você estiver tendo estouros excessivos nos cálculos, é importante entender o que realmente resulta e que o compilador PODE fazer algo diferente do que você espera (e que isso pode depender muito da versão do compilador, configurações de otimização, etc.) .

Mats Petersson
fonte
23
Os compiladores não querem que você confie neles fazendo a coisa certa, e a maioria deles mostrará isso assim que você compilar int f(int x) { return x+1>x; }com otimização. O GCC e o ICC, com opções padrão, otimizam o acima para return 1;.
Pascal Cuoq 12/08
1
Para um exemplo de programa que fornece resultados diferentes quando confrontado com o intestouro, dependendo dos níveis de otimização, consulte ideone.com/cki8nM. Acho que isso demonstra que sua resposta oferece maus conselhos.
22613 Magnus Hoff
Eu alterei um pouco essa parte.
22413 Mats Matsson
Se um C fornecer um meio de declarar um número inteiro "agrupando o complemento de dois assinados", nenhuma plataforma que possa executar o C deverá ter muitos problemas para suportá-lo, pelo menos com eficiência moderada. A sobrecarga extra seria suficiente para que o código não usasse esse tipo quando o comportamento de quebra automática não for necessário, mas a maioria das operações em números inteiros de complemento de dois é idêntica à de números inteiros não assinados, exceto comparações e promoções.
supercat
1
Valores negativos precisam existir e "funcionar" para que o compilador funcione corretamente. É claro que é totalmente possível contornar a falta de valores assinados em um processador e usar valores não assinados, como complemento ou dois, o que for mais adequado. sentido com base no que é o conjunto de instruções. Normalmente, isso seria significativamente mais lento do que ter suporte de hardware, mas não é diferente dos processadores que não suportam ponto flutuante no hardware ou similar - apenas adiciona muito código extra.
Mats Petersson
10

Antes de tudo, observe que o C11 3.4.3, como todos os exemplos e notas de rodapé, não é um texto normativo e, portanto, não é relevante para citar!

O texto relevante que afirma que o excesso de números inteiros e flutuantes é um comportamento indefinido é o seguinte:

C11 6.5 / 5

Se ocorrer uma condição excepcional durante a avaliação de uma expressão (ou seja, se o resultado não for matematicamente definido ou não estiver no intervalo de valores representáveis ​​para seu tipo), o comportamento será indefinido.

Um esclarecimento sobre o comportamento de tipos inteiros não assinados especificamente pode ser encontrado aqui:

C11 6.2.5 / 9

O intervalo de valores não negativos de um tipo inteiro assinado é uma subfaixa do tipo inteiro não assinado correspondente e a representação do mesmo valor em cada tipo é a mesma. Uma computação envolvendo operandos não assinados nunca pode exceder, porque um resultado que não pode ser representado pelo tipo inteiro não assinado resultante é reduzido pelo módulo, o número que é um maior que o maior valor que pode ser representado pelo tipo resultante.

Isso torna os tipos inteiros não assinados um caso especial.

Observe também que há uma exceção se qualquer tipo for convertido em um tipo assinado e o valor antigo não puder mais ser representado. O comportamento é então meramente definido pela implementação, embora um sinal possa ser gerado.

C11 6.3.1.3

6.3.1.3 Inteiros assinados e não assinados

Quando um valor com o tipo inteiro é convertido em outro tipo de número diferente de _Bool, se o valor puder ser representado pelo novo tipo, ele permanece inalterado.

Caso contrário, se o novo tipo não for assinado, o valor será convertido adicionando ou subtraindo repetidamente um a mais que o valor máximo que pode ser representado no novo tipo até que o valor esteja no intervalo do novo tipo.

Caso contrário, o novo tipo é assinado e o valor não pode ser representado nele; o resultado é definido pela implementação ou um sinal definido pela implementação é gerado.

Lundin
fonte
6

Além dos outros problemas mencionados, ter quebra de linha não assinada faz com que os tipos inteiros não assinados se comportem como grupos algébricos abstratos (o que significa que, entre outras coisas, para qualquer par de valores Xe Y, haverá algum outro valor Zque X+Z, se for convertido corretamente , igual Ye Y-Z, se convertido corretamente, igualX) Se os valores não assinados fossem meramente tipos de local de armazenamento e não tipos de expressão intermediária (por exemplo, se não houvesse equivalente não assinado do maior tipo inteiro, e operações aritméticas em tipos não assinados se comportassem como se os tivessem convertido primeiro em tipos assinados maiores, então não haveria tanta necessidade de um comportamento de quebra definido, mas é difícil fazer cálculos em um tipo que não tenha, por exemplo, um inverso aditivo.

Isso ajuda em situações em que o comportamento wrap-around é realmente útil - por exemplo, com números de sequência TCP ou determinados algoritmos, como cálculo de hash. Também pode ajudar em situações em que é necessário detectar estouro, já que executar cálculos e verificar se estouraram é geralmente mais fácil do que verificar antecipadamente se estourariam, especialmente se os cálculos envolverem o maior tipo inteiro disponível.

supercat
fonte
Não entendo direito - por que ajuda ter um aditivo inverso? Eu realmente não posso pensar em qualquer situação em que o comportamento de estouro é realmente útil ...
sleske
@sleske: Usar decimal para legibilidade humana, se um medidor de energia indicar 0003 e a leitura anterior for 9995, isso significa que foram usadas -9992 unidades de energia ou foram utilizadas 0008 unidades de energia? Ter 0003-9995 rendimento 0008 facilita o cálculo do último resultado. Tê-lo a produzir -9992 o tornaria um pouco mais estranho. Não poder fazer isso, no entanto, tornaria necessário comparar 0003 a 9995, observe que é menor, faça a subtração reversa, subtraia o resultado de 9999 e adicione 1.
supercat
@sleske: Também é muito útil que humanos e compiladores possam aplicar as leis associativas, distributivas e comutativas da aritmética para reescrever expressões e simplificá-las; por exemplo, se a expressão a+b-cé calculada dentro de um loop, mas be csão constantes dentro desse loop, ele pode ser útil para mover cálculo de (b-c)fora do loop, mas fazer isso exigiria, entre outras coisas, que (b-c)produzem um valor que, quando adicionado a a, produzirá a+b-c, o que por sua vez requer que ctenha um aditivo inverso.
Supercat
: Obrigado pelas explicações. Se eu entendi direito, todos os seus exemplos pressupõem que você realmente deseja lidar com o estouro. Na maioria dos casos que encontrei, o excesso é indesejável e você deseja evitá-lo, porque o resultado de um cálculo com excesso não é útil. Por exemplo, para o medidor de energia, você provavelmente deseja usar um tipo que nunca transborde.
sleske
1
... de modo que seja (a+b)-cigual à representatividade a+(b-c)ou não do valor aritmético b-cdentro do tipo, a substituição será válida independentemente do intervalo de valores possível para (b-c).
Supercat
1

Talvez outro motivo para a definição da aritmética não assinada seja porque os números não assinados formam números inteiros módulo 2 ^ n, onde n é a largura do número não assinado. Os números não assinados são simplesmente números inteiros representados usando dígitos binários em vez de dígitos decimais. A realização das operações padrão em um sistema de módulo é bem compreendida.

A citação do OP se refere a esse fato, mas também destaca o fato de que há apenas uma maneira lógica e inequívoca de representar números inteiros não assinados em binário. Por outro lado, os números assinados são representados com mais freqüência usando o complemento de dois, mas outras opções são possíveis, conforme descrito na norma (seção 6.2.6.2).

A representação de complemento do Two permite que certas operações façam mais sentido no formato binário. Por exemplo, incrementar números negativos é o mesmo que para números positivos (esperados sob condições de estouro). Algumas operações no nível da máquina podem ser as mesmas para números assinados e não assinados. No entanto, ao interpretar o resultado dessas operações, alguns casos não fazem sentido - excesso positivo e negativo. Além disso, os resultados do estouro diferem dependendo da representação assinada subjacente.

yth
fonte
Para que uma estrutura seja um campo, todo elemento da estrutura que não seja a identidade aditiva deve ter um inverso multiplicativo. Uma estrutura de números inteiros congruentes mod N será um campo somente quando N for um ou iniciar [um campo degenerado quando N == 1]. Há algo que você sente que eu perdi na minha resposta?
Supercat4
Você está certo. Fiquei confuso com os módulos de potência principal. Resposta original editada.
yth
Extra confundindo aqui é que não é um campo de ordem 2 ^ n, é apenas não-ring isomorphic aos inteiros módulo 2 ^ n.
21418 Kevin Ventullo
E 2 ^ 31-1 é um Mersenne Prime (mas 2 ^ 63-1 não é primo). Assim, minha ideia original foi arruinada. Além disso, tamanhos inteiros eram diferentes na época. Então, minha ideia foi revisionista, na melhor das hipóteses.
yth
O fato de números inteiros não assinados formarem um anel (não um campo), pegar a parte de ordem baixa também gera um anel e executar operações com todo o valor e, em seguida, truncar se comportará equivalente a executar as operações apenas na parte inferior, foram IMHO quase certamente considerações.
supercat 17/03