Eu estava navegando em algum código C ++ e encontrei algo assim:
(a + (b & 255)) & 255
O duplo E me incomodou, então pensei em:
(a + b) & 255
( a
e b
são inteiros sem sinal de 32 bits)
Rapidamente escrevi um script de teste (JS) para confirmar minha teoria:
for (var i = 0; i < 100; i++) {
var a = Math.ceil(Math.random() * 0xFFFF),
b = Math.ceil(Math.random() * 0xFFFF);
var expr1 = (a + (b & 255)) & 255,
expr2 = (a + b) & 255;
if (expr1 != expr2) {
console.log("Numbers " + a + " and " + b + " mismatch!");
break;
}
}
Embora o script tenha confirmado minha hipótese (ambas as operações são iguais), ainda não confio nele, porque 1) aleatório e 2) não sou matemático, não tenho ideia do que estou fazendo .
Além disso, desculpe pelo título Lisp-y. Sinta-se à vontade para editá-lo.
Math.random()
um número inteiro ou duplo em [0,1)? Não acho que seu roteiro (melhor que eu saiba) reflete o problema que você colocou.&
e+
em números inteiros sem sinal em C e C ++.Respostas:
Eles são os mesmos. Aqui está uma prova:
Primeiro observe a identidade
(A + B) mod C = (A mod C + B mod C) mod C
Vamos reafirmar o problema considerando
a & 255
como substitutoa % 256
. Isso é verdade desdea
não tem sinal.assim
(a + (b & 255)) & 255
é(a + (b % 256)) % 256
É o mesmo que
(a % 256 + b % 256 % 256) % 256
(apliquei a identidade declarada acima: observe quemod
e%
são equivalentes para tipos não assinados).Isso simplifica o
(a % 256 + b % 256) % 256
que se torna(a + b) % 256
(reaplicar a identidade). Você pode então colocar o operador bit a bit de volta para dar(a + b) & 255
completando a prova.
fonte
A=0xFFFFFFFF, B=1, C=3
. A primeira identidade não é válida. (Overflow não vai ser um problema para aritmética sem sinal, mas é uma coisa um pouco diferente.)(a + (b & 255)) & 255
é o mesmo que(a + (b % 256)) % N % 256
, ondeN
é maior que o valor máximo sem sinal. (a última fórmula deve ser interpretada como aritmética de inteiros matemáticos)Na adição, subtração e multiplicação posicional de números sem sinal para produzir resultados sem sinal, os dígitos mais significativos da entrada não afetam os dígitos menos significativos do resultado. Isso se aplica tanto à aritmética binária quanto à aritmética decimal. Também se aplica à aritmética com sinais de "complemento de dois", mas não à aritmética com sinais de magnitude de sinal.
No entanto, temos que ter cuidado ao pegar regras da aritmética binária e aplicá-las a C (acredito que C ++ tem as mesmas regras que C nessas coisas, mas não tenho 100% de certeza) porque a aritmética C tem algumas regras misteriosas que podem nos enganar acima. A aritmética sem sinal em C segue regras de agrupamento binário simples, mas o estouro aritmético com sinal é um comportamento indefinido. Pior, em algumas circunstâncias, C irá "promover" automaticamente um tipo não assinado para int (assinado).
O comportamento indefinido em C pode ser especialmente insidioso. Um compilador burro (ou um compilador em um nível de otimização baixo) provavelmente fará o que você espera com base em seu conhecimento de aritmética binária, enquanto um compilador otimizador pode quebrar seu código de maneiras estranhas.
Portanto, voltando à fórmula da questão, a igualdade depende dos tipos de operando.
Se eles forem inteiros sem sinal cujo tamanho é maior ou igual ao tamanho de,
int
então o comportamento de estouro do operador de adição é bem definido como um agrupamento binário simples. O fato de mascararmos ou não os 24 bits altos de um operando antes da operação de adição não tem impacto sobre os bits baixos do resultado.Se eles forem inteiros não assinados cujo tamanho é menor que
int
então eles serão promovidos a (assinados)int
. O estouro de inteiros assinados é um comportamento indefinido, mas pelo menos em todas as plataformas que encontrei, a diferença de tamanho entre os diferentes tipos de inteiros é grande o suficiente para que uma única adição de dois valores promovidos não cause o estouro. Portanto, novamente podemos recorrer ao argumento aritmético simplesmente binário para considerar as declarações equivalentes.Se eles forem inteiros assinados cujo tamanho é menor que int, então novamente o estouro não pode acontecer e em implementações de complemento de dois, podemos confiar no argumento aritmético binário padrão para dizer que eles são equivalentes. Em implementações de magnitude de sinal ou complemento de uns, eles não seriam equivalentes.
OTOH se
a
eb
fossem inteiros assinados cujo tamanho era maior ou igual ao tamanho de int, então, mesmo em implementações de complemento de dois, há casos em que uma instrução seria bem definida enquanto a outra seria um comportamento indefinido.fonte
Lema:
a & 255 == a % 256
para não assinadoa
.Sem sinal
a
pode ser reescrita comom * 0x100 + b
alguns unsignedm
,b
,0 <= b < 0xff
,0 <= m <= 0xffffff
. Resulta de ambas as definições quea & 255 == b == a % 256
.Além disso, precisamos de:
(a + b) mod n = [(a mod n) + (b mod n)] mod n
(a + b) ==> (a + b) % (2 ^ 32)
Portanto:
Então sim, é verdade. Para inteiros sem sinal de 32 bits.
E quanto a outros tipos inteiros?
2^64
a2^32
.int
. Issoint
definitivamente não transbordará nem será negativo em nenhuma dessas operações, portanto, todas permanecem válidas.a+b
oua+(b&255)
estouro, é um comportamento indefinido. Portanto, a igualdade não pode ser mantida - há casos em que o(a+b)&255
comportamento é indefinido, mas(a+(b&255))&255
não é.fonte
Sim,
(a + b) & 255
está bem.Lembra da adição na escola? Você adiciona números dígito a dígito e adiciona um valor de transporte à próxima coluna de dígitos. Não há como uma coluna posterior (mais significativa) de dígitos influenciar uma coluna já processada. Por causa disso, não faz diferença se você zerar os dígitos apenas no resultado ou também primeiro em um argumento.
O que foi dito acima nem sempre é verdade, o padrão C ++ permite uma implementação que quebraria isso.
Tal Deathstation 9000 : - ) teria que usar 33 bits
int
, se o OP significasseunsigned short
"inteiros sem sinal de 32 bits". Se issounsigned int
fosse feito, o DS9K teria que usar um bit de 32 bitsint
e um de 32 bitsunsigned int
com um bit de preenchimento. (Os inteiros não assinados devem ter o mesmo tamanho que suas contrapartes assinadas de acordo com §3.9.1 / 3, e bits de preenchimento são permitidos em §3.9.1 / 1.) Outras combinações de tamanhos e bits de preenchimento também funcionam.Pelo que eu posso dizer, esta é a única maneira de quebrá-lo, porque:
int
pode representar todos os valores do tipo de origem (§4.5 / 1), portanto,int
deve ter pelo menos 32 bits contribuindo para o valor, mais um bit de sinal.int
não pode ter mais bits de valor (sem contar o bit de sinal) do que 32, caso contrário, uma adição não pode estourar.fonte
2^N-1
. (N pode nem ser necessário para ser um múltiplo de CHAR_BIT, eu esqueci, mas tenho certeza que o padrão requer que o wraparound aconteça módulo algum poder de 2.) Eu acho que a única maneira de você obter estranheza é promovendo a um tipo assinado que é largo o suficiente para segurara
ou,b
mas não largo o suficiente para segurara+b
em todos os casos.Você já tem a resposta inteligente: aritmética sem sinal é aritmética de módulo e, portanto, os resultados serão válidos, você pode provar isso matematicamente ...
Uma coisa legal sobre computadores, porém, é que eles são rápidos. Na verdade, eles são tão rápidos que enumerar todas as combinações válidas de 32 bits é possível em um período de tempo razoável (não tente com 64 bits).
Então, no seu caso, eu pessoalmente gosto de apenas jogá-lo em um computador; levo menos tempo para me convencer de que o programa está correto do que para me convencer de que a prova matemática está correta e que não supervisionei um detalhe na especificação 1 :
Isso enumera todos os valores possíveis de
a
eb
no espaço de 32 bits e verifica se a igualdade é mantida ou não. Caso contrário, ele imprime o gabinete que não funcionou, que você pode usar como uma verificação de integridade.E, de acordo com Clang : a igualdade é mantida .
Além disso, dado que as regras aritméticas são agnósticas quanto à largura de bits (acima
int
da largura de bit), essa igualdade se manterá para qualquer tipo de inteiro sem sinal de 32 bits ou mais, incluindo 64 bits e 128 bits.Nota: Como um compilador pode enumerar todos os padrões de 64 bits em um período de tempo razoável? Eu não posso. Os loops foram otimizados. Caso contrário, todos nós teríamos morrido antes do término da execução.
Inicialmente, provei isso apenas para inteiros sem sinal de 16 bits; infelizmente C ++ é uma linguagem insana para a qual pequenos inteiros (larguras de bits menores que
int
) são convertidos primeiroint
.E mais uma vez, de acordo com Clang : a igualdade é válida .
Bem, aí está :)
1 É claro que, se um programa acionar inadvertidamente o comportamento indefinido, isso não provaria muito.
fonte
int
: pequenos inteiros são primeiro convertidos paraint
(uma regra estranha). Então, na verdade, tenho que fazer a demonstração com 32 bits (e depois se estende para 64 bits, 128 bits, ...).int
que pode representar todas as possibilidadesuint16_t
, mas ondea+b
pode estourar. Isso é apenas um problema para tipos não assinados mais estreitos do queint
; C requer que osunsigned
tipos sejam inteiros binários, então o wraparound acontece módulo uma potência de 2A resposta rápida é: ambas as expressões são equivalentes
a
eb
são inteiros sem sinal de 32 bits, o resultado é o mesmo, mesmo em caso de estouro. A aritmética sem sinal garante isso: um resultado que não pode ser representado pelo tipo inteiro sem sinal resultante é o módulo reduzido do número que é um maior que o maior valor que pode ser representado pelo tipo resultante.A resposta longa é: não existem plataformas conhecidas onde essas expressões sejam diferentes, mas a Norma não garante isso, por causa das regras de promoção integral.
Se o tipo de
a
eb
(inteiros de 32 bits sem sinal) tiver uma classificação superiorint
, o cálculo será executado como sem sinal, módulo 2 32 , e produzirá o mesmo resultado definido para ambas as expressões para todos os valores dea
eb
.Por outro lado, se o tipo de
a
eb
for menor queint
, ambos são promovidos paraint
e o cálculo é realizado usando aritmética assinada, em que o estouro invoca um comportamento indefinido.Se
int
tiver pelo menos 33 bits de valor, nenhuma das expressões acima pode estourar, então o resultado está perfeitamente definido e tem o mesmo valor para ambas as expressões.Se
int
tiver exatamente 32 bits de valor, o cálculo pode estourar para ambas as expressões, por exemplo, valoresa=0xFFFFFFFF
eb=1
causaria um estouro em ambas as expressões. Para evitar isso, você precisa escrever((a & 255) + (b & 255)) & 255
.A boa notícia é que essas plataformas não existem 1 .
1 Mais precisamente, essa plataforma real não existe, mas pode-se configurar um DS9K para exibir esse comportamento e ainda estar em conformidade com o C Standard.
fonte
a
é menor queint
(2)int
tem 32 bits de valor (3)a=0xFFFFFFFF
. Isso não pode ser verdade.int
, onde há 32 bits de valor e um bit de sinal.Idêntico assumindo nenhum estouro . Nenhuma das versões é realmente imune a transbordamento, mas a versão dupla e mais resistente a ele. Não conheço um sistema em que um estouro, neste caso, seja um problema, mas posso ver o autor fazendo isso, caso haja um.
fonte
int
tenha 33 bits de largura, o resultado é o mesmo, mesmo em caso de estouro. A aritmética sem sinal garante isso: um resultado que não pode ser representado pelo tipo inteiro sem sinal resultante é o módulo reduzido do número que é um maior que o maior valor que pode ser representado pelo tipo resultante.Sim, você pode provar isso com aritmética, mas há uma resposta mais intuitiva.
Ao adicionar, cada bit influencia apenas aqueles mais significativos do que ele mesmo; nunca aqueles menos significativos.
Portanto, o que quer que você faça com os bits mais altos antes da adição não mudará o resultado, desde que você mantenha apenas os bits menos significativos do que o bit mais baixo modificado.
fonte
A prova é trivial e deixada como um exercício para o leitor
Mas, para realmente legitimar isso como uma resposta, sua primeira linha de código diz pegue os últimos 8 bits de
b
** (todos os bits mais altos dob
conjunto para zero) e adicione isso aa
e, em seguida, pegue apenas os últimos 8 bits do resultado configurando todos os mais altos bits para zero.A segunda linha diz adicionar
a
eb
e tirar os últimos 8 bits com todos os bits mais altos zero.Apenas os últimos 8 bits são significativos no resultado. Portanto, apenas os últimos 8 bits são significativos na (s) entrada (s).
** últimos 8 bits = 8 LSB
Também é interessante notar que a saída seria equivalente a
Como acima, apenas os 8 LSB são significativos, mas o resultado é um
unsigned int
com todos os outros bits zero. Oa + b
irá transbordar, produzindo o resultado esperado.fonte