Por que (a% 256) é diferente de (a & 0xFF)?

145

Sempre presumi que, ao fazer (a % 256)o otimizador, usaria naturalmente uma operação bit a bit eficiente, como se eu escrevesse (a & 0xFF).

Ao testar no compilador explorer gcc-6.2 (-O3):

// Type your code here, or load an example.
int mod(int num) {
    return num % 256;
}

mod(int):
    mov     edx, edi
    sar     edx, 31
    shr     edx, 24
    lea     eax, [rdi+rdx]
    movzx   eax, al
    sub     eax, edx
    ret

E ao tentar o outro código:

// Type your code here, or load an example.
int mod(int num) {
    return num & 0xFF;
}

mod(int):
    movzx   eax, dil
    ret

Parece que estou perdendo completamente alguma coisa. Alguma ideia?

Elad Weiss
fonte
64
0xFF é 255 não 256.
Rishikesh Raje
186
@RishikeshRaje: Então? %também não é &.
precisa saber é o seguinte
27
@RishikeshRaje: Estou certo de que o OP está muito ciente disso. Eles são usados ​​com operações diferentes.
Saúde e hth. - Alf
28
Fora de interesse, você obtém melhores resultados se numfor unsigned?
Bathsheba
20
@RishikeshRaje Bitwise e 0xFF são equivalentes ao módulo 2 ^ 8 para números inteiros não assinados.
2501

Respostas:

230

Não é o mesmo. Tente num = -79e você obterá resultados diferentes das duas operações. (-79) % 256 = -79, enquanto (-79) & 0xffé algum número positivo.

Usando unsigned int, as operações são as mesmas e o código provavelmente será o mesmo.

PS- Alguém comentou

Eles não devem ser os mesmos, a % bé definido como a - b * floor (a / b).

Não é assim que é definido em C, C ++, Objective-C (ou seja, todos os idiomas em que o código da pergunta seria compilado).

gnasher729
fonte
Comentários não são para discussão prolongada; esta conversa foi movida para o bate-papo .
Martijn Pieters
52

Resposta curta

-1 % 256produz -1e não o 255que é -1 & 0xFF. Portanto, a otimização estaria incorreta.

Resposta longa

C ++ tem a convenção de que (a/b)*b + a%b == a, o que parece bastante natural. a/bsempre retorna o resultado aritmético sem a parte fracionária (truncando para 0). Como conseqüência, a%btem o mesmo sinal que aou é 0.

A divisão -1/256cede 0e, portanto, -1%256deve estar -1em conformidade com a condição acima ( (-1%256)*256 + -1%256 == -1). Isto é obviamente diferente do -1&0xFFque é 0xFF. Portanto, o compilador não pode otimizar da maneira que desejar.

A seção relevante no padrão C ++ [expr.mul §4] a partir do N4606 declara:

Para operandos integrais, o /operador produz o quociente algébrico com qualquer parte fracionária descartada; se o quociente a/bé representável no tipo de resultado, (a/b)*b + a%bé igual a a[...].

Ativando a otimização

No entanto, usando unsignedtipos, a otimização estaria completamente correta , atendendo à convenção acima:

unsigned(-1)%256 == 0xFF

Veja também isso .

Outras línguas

Isso é tratado de maneira muito diferente em diferentes linguagens de programação, como você pode consultar na Wikipedia .

Ralph Tandetzky
fonte
50

Desde C ++ 11, num % 256deve ser não positivo se numfor negativo.

Portanto, o padrão de bits dependeria da implementação de tipos assinados no seu sistema: para um primeiro argumento negativo, o resultado não é a extração dos 8 bits menos significativos.

Seria diferente se numno seu caso fosse unsigned: hoje em dia eu quase esperaria que um compilador fizesse a otimização que você cita.

Bathsheba
fonte
6
Quase mas não completamente. Se numfor negativo, num % 256será zero ou negativo (também conhecido como não positivo).
Nayuki
5
Qual IMO é um erro no padrão: a operação do módulo matematicamente deve receber o sinal do divisor, 256 neste caso. Para entender por que considerar isso (-250+256)%256==6, mas (-250%256)+(256%256)deve ser, de acordo com o padrão, "não positivo" e, portanto, não 6. Quebrar associatividade como essa tem efeitos colaterais na vida real: por exemplo, ao calcular a renderização "zoom out" em coordenadas inteiras, é preciso mudar a imagem para que todas as coordenadas não sejam negativas.
Michael Michael
2
@ Michael Modulus nunca foi distributivo em adição ("associativo" é o nome errado para essa propriedade!), Mesmo se você seguir a definição matemática da letra. Por exemplo, (128+128)%256==0mas (128%256)+(128%256)==256. Talvez haja uma boa objeção ao comportamento especificado, mas não está claro para mim que foi o que você disse.
Daniel Wagner
1
@ DanielWagner, você está certo, é claro, eu errei em "associativo". No entanto, se alguém mantiver o sinal do divisor e computar tudo na aritmética modular, a propriedade distributiva se mantém; no seu exemplo você teria 256==0. A chave é ter Nvalores exatamente possíveis no módulo Naritmético, o que só é possível se todos os resultados estiverem no intervalo 0,...,(N-1), não -(N-1),...,(N-1).
Michael Michael
6
@ Michael: Exceto% não é um operador de módulo, é um operador restante .
Joren
11

Não tenho uma visão telepática do raciocínio do compilador, mas, no caso de, %há a necessidade de lidar com valores negativos (e as rodadas de divisão em direção a zero), enquanto &o resultado é sempre os 8 bits mais baixos.

A sarinstrução soa para mim como "shift aritmetic right", preenchendo os bits desocupados com o valor do bit de sinal.

Felicidades e hth. - Alf
fonte
0

Matematicamente falando, o módulo é definido como o seguinte:

a% b = a - b * piso (a / b)

Isso aqui deve esclarecer para você. Podemos eliminar o piso para números inteiros porque a divisão inteira é equivalente ao piso (a / b). No entanto, se o compilador usar um truque geral, como você sugere, ele precisará funcionar para todos os a e todos os b. Infelizmente, isso simplesmente não é o caso. Matematicamente falando, seu truque é 100% correto para números inteiros não assinados (vejo uma resposta declarar números inteiros quebrados, mas posso confirmar ou negar isso como -a% b deve ser positivo). No entanto, você pode fazer esse truque para todos os b? Provavelmente não. É por isso que o compilador não faz isso. Afinal, se o módulo fosse facilmente escrito como uma operação bit a bit, adicionaríamos um circuito de módulo como para adição e outras operações.

user64742
fonte
4
Eu acho que você está confundindo "andar" com "truncar". Os computadores antigos usavam a divisão truncada, porque geralmente é mais fácil calcular do que a divisão no piso, mesmo nos casos em que as coisas se dividem igualmente. Vi muito poucos casos em que a divisão truncada era mais útil do que a divisão com piso, mas muitos idiomas seguem o exemplo do FORTRAN de usar a divisão truncada.
Supercat 9/16
@supercat Matematicamente falando, o piso é truncado. Ambos têm o mesmo efeito. Eles podem não ser implementados da mesma forma em um computador, mas fazem a mesma coisa.
user64742
5
@TheGreatDuck: Eles não são os mesmos para números negativos. O piso de -2.3é -3, enquanto que se você truncar -2.3para um número inteiro, obtém -2. Veja en.wikipedia.org/wiki/Truncation . "para números negativos, o truncamento não arredonda na mesma direção que a função de piso". E o comportamento de %para números negativos é precisamente o motivo pelo qual o OP está vendo o comportamento descrito.
Mark Dickinson
@ MarkDickinson Tenho certeza de que o módulo em c ++ fornece valores positivos para divisores positivos, mas não vou discutir.
user64742
1
@TheGreatDuck - veja o exemplo: cpp.sh/3g7h (observe que o C ++ 98 não definiu qual das duas variantes possíveis foi usada, mas os padrões mais recentes o fazem, por isso é possível que você tenha usado uma implementação do C ++ no passado que fez isso de forma diferente ...)
Periata Breatta