¿Por qué es (a% 256) diferente de (a & 0xFF)?

145

Siempre supuse que al hacer (a % 256)el optimizador, naturalmente, usaría una operación eficiente a nivel de bits, como si escribiera (a & 0xFF).

Al probar en el explorador del compilador 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

Y al probar el otro 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 me estoy perdiendo algo por completo. ¿Algunas ideas?

Elad Weiss
fuente
64
0xFF es 255 no 256.
Rishikesh Raje
186
@RishikeshRaje: ¿Entonces? %tampoco lo es &.
usr2564301
27
@RishikeshRaje: Estoy seguro de que el OP es muy consciente de eso. Se usan con diferentes operaciones.
Saludos y hth. - Alf
28
Por interés, ¿obtienes mejores resultados si numes así unsigned?
Betsabé el
20
@RishikeshRaje Bitwise y 0xFF es equivalente al módulo 2 ^ 8 para enteros sin signo.
2501

Respuestas:

230

No es lo mismo. Intente num = -79y obtendrá resultados diferentes de ambas operaciones. (-79) % 256 = -79, mientras que (-79) & 0xffes un número positivo.

Usando unsigned int, las operaciones son las mismas, y el código probablemente será el mismo.

PD- Alguien comentó

No deberían ser iguales, a % bse define como a - b * floor (a / b).

Así no se define en C, C ++, Objective-C (es decir, todos los lenguajes donde se compilaría el código de la pregunta).

gnasher729
fuente
Los comentarios no son para discusión extendida; Esta conversación se ha movido al chat .
Martijn Pieters
52

Respuesta corta

-1 % 256cede -1y no 255cual es -1 & 0xFF. Por lo tanto, la optimización sería incorrecta.

Respuesta larga

C ++ tiene la convención que (a/b)*b + a%b == aparece bastante natural. a/bsiempre devuelve el resultado aritmético sin la parte fraccionaria (truncando hacia 0). Como consecuencia, a%btiene el mismo signo que ao es 0.

La división -1/256rinde 0y, por -1%256lo tanto, debe ser -1para satisfacer la condición anterior ( (-1%256)*256 + -1%256 == -1). Esto es obviamente diferente de lo -1&0xFFque es 0xFF. Por lo tanto, el compilador no puede optimizar la forma que desea.

La sección relevante en el estándar C ++ [expr.mul §4] a partir de N4606 establece:

Para operandos integrales, el /operador produce el cociente algebraico con cualquier parte fraccional descartada; si el cociente a/bes representable en el tipo del resultado, (a/b)*b + a%bes igual a a[...].

Habilitando la optimización

Sin embargo, usando unsignedtipos, la optimización sería completamente correcta , satisfaciendo la convención anterior:

unsigned(-1)%256 == 0xFF

Ver también esto .

Otros idiomas

Esto se maneja de manera muy diferente en diferentes lenguajes de programación, ya que puede consultar Wikipedia .

Ralph Tandetzky
fuente
50

Desde C ++ 11, num % 256tiene que ser no positivo si numes negativo.

Por lo tanto, el patrón de bits dependería de la implementación de tipos con signo en su sistema: para un primer argumento negativo, el resultado no es la extracción de los 8 bits menos significativos.

Sería un asunto diferente si numen su caso lo fuera unsigned: en estos días casi esperaría que un compilador haga la optimización que usted cita.

Betsabé
fuente
66
Casi pero no del todo. Si numes negativo, entonces num % 256es cero o negativo (también conocido como no positivo).
Nayuki
55
Qué IMO es un error en el estándar: la operación del módulo matemático debería tomar el signo del divisor, 256 en este caso. Para entender por qué considerar eso (-250+256)%256==6, pero (-250%256)+(256%256)debe ser, según el estándar, "no positivo", y por lo tanto no 6. Romper la asociatividad de esa manera tiene efectos secundarios en la vida real: por ejemplo, al calcular la representación de "alejamiento" en coordenadas enteras, uno tiene que desplazar la imagen para que todas las coordenadas no sean negativas.
Michael
2
@Michael Modulus nunca ha sido distributivo sobre la suma ("asociativo" es el nombre incorrecto para esta propiedad), incluso si sigue la definición matemática al pie de la letra. Por ejemplo, (128+128)%256==0pero (128%256)+(128%256)==256. Quizás haya una buena objeción al comportamiento especificado, pero no me queda claro que sea el que usted dijo.
Daniel Wagner
1
@DanielWagner, tienes razón, por supuesto, hablé mal con "asociativo". Sin embargo, si uno mantiene el signo del divisor y calcula todo en aritmética modular, la propiedad distributiva se mantiene; en tu ejemplo lo hubieras hecho 256==0. La clave es tener exactamente Nlos valores posibles en el módulo Naritmético, que solo es posible si todos los resultados están en el rango 0,...,(N-1), no -(N-1),...,(N-1).
Michael
66
@Michael: Excepto que% no es un operador de módulo, es un operador restante .
Joren
11

No tengo una visión telepática del razonamiento del compilador, pero en el caso de %que sea necesario tratar con valores negativos (y las rondas de división hacia cero), mientras que &el resultado es siempre los 8 bits más bajos.

La sarinstrucción me suena como "desplazamiento aritmético a la derecha", llenando los bits desocupados con el valor del bit de signo.

Saludos y hth. - Alf
fuente
0

Matemáticamente hablando, el módulo se define como el siguiente:

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

Esto aquí debería aclararlo para ti. Podemos eliminar el piso para enteros porque la división de enteros es equivalente al piso (a / b). Sin embargo, si el compilador usara un truco general como usted sugiere, tendría que funcionar para todos ay todos b. Desafortunadamente, este no es el caso. Hablando matemáticamente, su truco es 100% correcto para enteros sin signo (veo que una respuesta indica que los enteros con signo se rompen, pero puedo confirmar o negar esto ya que -a% b debería ser positivo). Sin embargo, ¿puedes hacer este truco para todos los b? Probablemente no. Es por eso que el compilador no lo hace. Después de todo, si el módulo se escribiera fácilmente como una operación bit a bit, entonces simplemente agregaríamos un circuito de módulo como para la suma y las otras operaciones.

usuario64742
fuente
44
Creo que estás confundiendo "piso" con "truncado". Las primeras computadoras usaban la división truncada porque a menudo es más fácil de calcular que la división en pisos, incluso en los casos en que las cosas se dividen de manera uniforme. He visto muy pocos casos en los que la división truncada fue más útil de lo que hubiera sido la división en pisos, pero muchos idiomas siguen el ejemplo de FORTRAN de usar la división truncada.
supercat
@supercat Matemáticamente hablando, el piso está truncado. Ambos tienen el mismo efecto. Es posible que no se implementen de la misma manera en una computadora, pero hacen lo mismo.
user64742
55
@TheGreatDuck: No son lo mismo para los números negativos. El piso de -2.3es -3, mientras que si truncas -2.3a un entero obtienes -2. Ver en.wikipedia.org/wiki/Truncation . "para números negativos, el truncamiento no se redondea en la misma dirección que la función de piso". Y el comportamiento de %los números negativos es precisamente la razón por la que el OP está viendo el comportamiento descrito.
Mark Dickinson
@ MarkDickinson Estoy bastante seguro de que el módulo en c ++ da valores positivos para divisores positivos, pero no voy a discutir.
user64742
1
@TheGreatDuck: vea el ejemplo: cpp.sh/3g7h (Tenga en cuenta que C ++ 98 no definió cuál de las dos variantes posibles se usó, pero que los estándares más recientes sí, así que es posible que haya utilizado una implementación de C ++ en el pasado que lo hizo de manera diferente ...)
Periata Breatta