El operador de módulo (%) da un resultado diferente para diferentes versiones de .NET en C #

89

Estoy encriptando la entrada del usuario para generar una cadena de contraseña. Pero una línea de código da diferentes resultados en diferentes versiones del marco. Código parcial con valor de tecla presionada por usuario:

Tecla presionada: 1. La variable asciies 49. Valor de 'e' y 'n' después de algún cálculo:

e = 103, 
n = 143,

Math.Pow(ascii, e) % n

Resultado del código anterior:

  • En .NET 3.5 (C #)

    Math.Pow(ascii, e) % n

    da 9.0.

  • En .NET 4 (C #)

    Math.Pow(ascii, e) % n

    da 77.0.

Math.Pow() da el resultado correcto (el mismo) en ambas versiones.

¿Cuál es la causa? ¿Existe una solución?

Rajiv
fuente
12
Por supuesto, ambas respuestas en la pregunta son incorrectas. El hecho de que no parezca importarle eso es, bueno, preocupante.
David Heffernan
34
Debe retroceder varios pasos. "Estoy encriptando la entrada del usuario para generar una cadena de contraseña" esta parte ya es dudosa. ¿Qué es lo que realmente quieres hacer? ¿Desea almacenar una contraseña en forma cifrada o hash? ¿Quieres usar esto como entropía para generar un valor aleatorio? ¿Cuáles son sus objetivos de seguridad?
CodesInChaos
49
Si bien esta pregunta ilustra un problema interesante con la aritmética de punto flotante, si el objetivo del OP es "cifrar la entrada del usuario para generar una cadena para la contraseña", no creo que sea una buena idea implementar su propio cifrado, por lo que no lo recomendaría realmente implementando cualquiera de las respuestas.
Harrison Paine
18
Buena demostración de por qué otros lenguajes prohíben el uso de %números de punto flotante.
Ben Voigt
5
Si bien las respuestas son buenas, ninguna responde a la pregunta de qué ha cambiado entre .NET 3.5 y 4 que está causando el comportamiento diferente.
msell

Respuestas:

160

Math.Powtrabaja con números de coma flotante de doble precisión; por lo tanto, no debe esperar que más de los primeros 15 a 17 dígitos del resultado sean precisos:

Todos los números de punto flotante también tienen un número limitado de dígitos significativos, lo que también determina la precisión con la que un valor de punto flotante se aproxima a un número real. Un Doublevalor tiene hasta 15 dígitos decimales de precisión, aunque internamente se mantiene un máximo de 17 dígitos.

Sin embargo, la aritmética de módulo requiere que todos los dígitos sean precisos. En su caso, está calculando 49103 , cuyo resultado consta de 175 dígitos, lo que hace que la operación de módulo no tenga sentido en ambas respuestas.

Para calcular el valor correcto, debe usar aritmética de precisión arbitraria, como lo proporciona la BigIntegerclase (introducida en .NET 4.0).

int val = (int)(BigInteger.Pow(49, 103) % 143);   // gives 114

Editar : Como señaló Mark Peters en los comentarios a continuación, debe usar el BigInteger.ModPowmétodo, que está diseñado específicamente para este tipo de operación:

int val = (int)BigInteger.ModPow(49, 103, 143);   // gives 114
Douglas
fuente
20
+1 por señalar el problema real, es decir, que el código en la pregunta es simplemente incorrecto
David Heffernan
36
Vale la pena señalar que BigInteger proporciona un método ModPow () que funciona (en mi prueba rápida de ahora) aproximadamente 5 veces más rápido para esta operación.
Mark Peters
8
+1 Con la edición. ModPow no solo es rápido, ¡es numéricamente estable!
Ray
2
@maker No, la respuesta no tiene sentido , no es inválida .
Cody Gray
3
@ makerofthings7: Estoy de acuerdo contigo en principio. Sin embargo, la imprecisión es inherente a la aritmética de punto flotante, y se considera más práctico esperar que los desarrolladores estén al tanto de los riesgos que imponer restricciones a las operaciones en general. Si uno quisiera estar verdaderamente "seguro", entonces el lenguaje también necesitaría prohibir las comparaciones de igualdad de punto flotante, para evitar resultados inesperados como 1.0 - 0.9 - 0.1 == 0.0evaluar a false.
Douglas
72

Aparte del hecho de que su función hash no es muy buena * , el mayor problema con su código no es que devuelve un número diferente dependiendo de la versión de .NET, sino que en ambos casos devuelve un número completamente sin sentido: la respuesta correcta al problema es

49 103 mod 143 = es 114. ( enlace a Wolfram Alpha )

Puede usar este código para calcular esta respuesta:

private static int PowMod(int a, int b, int mod) {
    if (b == 0) {
        return 1;
    }
    var tmp = PowMod(a, b/2, mod);
    tmp *= tmp;
    if (b%2 != 0) {
        tmp *= a;
    }
    return tmp%mod;
}

La razón por la que su cálculo produce un resultado diferente es que para producir una respuesta, usa un valor intermedio que elimina la mayoría de los dígitos significativos del número 49103 : ¡solo los primeros 16 de sus 175 dígitos son correctos!

1230824813134842807283798520430636310264067713738977819859474030746648511411697029659004340261471771152928833391663821316264359104254030819694748088798262075483562075061997649

Los 159 dígitos restantes están mal. Sin embargo, la operación de modificación busca un resultado que requiera que todos los dígitos sean correctos, incluidos los últimos. Por lo tanto, incluso la más mínima mejora en la precisión de Math.Poweso puede haberse implementado en .NET 4, daría como resultado una diferencia drástica de su cálculo, que esencialmente produce un resultado arbitrario.

* Dado que esta pregunta habla de aumentar los números enteros a poderes altos en el contexto del hash de contraseñas, puede ser una muy buena idea leer este enlace de respuesta antes de decidir si su enfoque actual debe cambiarse por uno potencialmente mejor.

dasblinkenlight
fuente
20
Buena respuesta. El punto real es que esta es una función de hash terrible. OP necesita repensar la solución y utilizar un algoritmo más apropiado.
david.pfx
1
Isaac Newton: ¿Es posible que la luna se sienta atraída por la tierra de la misma manera que la manzana se siente atraída por la tierra? @ david.pfx: El punto real es que esta es una forma terrible de recoger manzanas. Newton necesita repensar la solución y quizás contratar a un hombre con una escalera.
jwg
2
El comentario de @jwg David obtuvo tantos votos a favor por una razón. La pregunta original dejó en claro que el algoritmo se estaba utilizando para codificar contraseñas y, de hecho, es un algoritmo terrible para ese propósito: es muy probable que se rompa entre las versiones del marco .NET, como ya se ha demostrado. Cualquier respuesta que no mencione que el OP necesita reemplazar su algoritmo en lugar de "arreglarlo" le está haciendo un flaco favor.
Chris
@Chris Gracias por el comentario, lo edité para incluir la sugerencia de David. No lo expresé tan fuerte como tú, porque el sistema de OP puede ser un juguete o un código desechable que él construye para su propia diversión. ¡Gracias!
dasblinkenlight
27

Lo que ves es un error de redondeo al doble. Math.Powfunciona con doble y la diferencia es la siguiente:

.NET 2.0 y 3.5 => var powerResult = Math.Pow(ascii, e);devuelve:

1.2308248131348429E+174

.NET 4.0 y 4.5 => var powerResult = Math.Pow(ascii, e);devuelve:

1.2308248131348427E+174

Observe el último dígito antes Ey eso está causando la diferencia en el resultado. No es el operador de módulo (%) .

Habib
fuente
3
vaca sagrada, ¿es esta la ÚNICA respuesta a la pregunta de los OP? Leí toda la meta "bla, bla, seguridad, pregunta incorrecta, sé más que tú, n00b" y todavía me preguntaba "¿por qué la discrepancia constante entre 3.5 y 4.0? ¿Alguna vez te golpeaste el dedo del pie con una roca mientras miraba la luna y pregunté" qué tipo de roca ¿Es esto? "Solo para que te digan" Tu problema real no es mirarte los pies "o" ¡¡¡¿Qué esperas cuando usas sandalias hechas en casa por la noche? !!! "¡¡¡GRACIAS!
Michael Paulukonis
1
@MichaelPaulukonis: Esa es una falsa analogía. El estudio de las rocas es una actividad legítima; realizar aritmética de precisión arbitraria usando tipos de datos de precisión fija es simplemente incorrecto. Compararía esto con un reclutador de software que pregunta por qué los perros son peores que los gatos al escribir C #. Si es zoólogo, la pregunta puede tener algún mérito; para todos los demás, no tiene sentido.
Douglas
24

La precisión del punto flotante puede variar de una máquina a otra, e incluso en la misma máquina .

Sin embargo, .NET crea una máquina virtual para sus aplicaciones ... pero hay cambios de una versión a otra.

Por lo tanto, no debe confiar en él para producir resultados consistentes. Para el cifrado, utilice las clases que proporciona Framework en lugar de utilizar las suyas propias.

Joe
fuente
10

Hay muchas respuestas sobre la forma en que el código es malo. Sin embargo, en cuanto a por qué el resultado es diferente ...

Las FPU de Intel utilizan el formato de 80 bits internamente para obtener más precisión en los resultados intermedios. Entonces, si un valor está en el registro del procesador, obtiene 80 bits, pero cuando se escribe en la pila, se almacena en 64 bits. .

Espero que la versión más nueva de .NET tenga un mejor optimizador en su compilación Just in Time (JIT), por lo que mantiene un valor en un registro en lugar de escribirlo en la pila y luego leerlo de la pila.

Puede ser que el JIT ahora pueda devolver un valor en un registro en lugar de en la pila. O pase el valor a la función MOD en un registro.

Consulte también la pregunta de desbordamiento de pila ¿ Cuáles son las aplicaciones / beneficios de un tipo de datos de precisión extendida de 80 bits?

Otros procesadores, por ejemplo, el ARM, darán resultados diferentes para este código.

Ian Ringrose
fuente
6

Tal vez sea mejor calcularlo usted mismo usando solo aritmética de números enteros. Algo como:

int n = 143;
int e = 103;
int result = 1;
int ascii = (int) 'a';

for (i = 0; i < e; ++i) 
    result = result * ascii % n;

Puede comparar el rendimiento con el rendimiento de la solución BigInteger publicada en las otras respuestas.

Ronald
fuente
7
Eso requeriría 103 multiplicaciones y reducciones de módulo. Uno puede hacerlo mejor calculando e2 = e * e% n, e4 = e2 * e2% n, e8 = e4 * e4% n, etc. y luego result = e * e2% n * e4% n * e32% n * e64% n. Un total de 11 multiplicaciones y reducciones de módulo. Dado el tamaño de los números involucrados, se podrían eliminar algunas reducciones de módulo más, pero eso sería menor en comparación con reducir 103 operaciones a 11.
supercat
2
@supercat Buenas matemáticas, pero en la práctica solo son relevantes si estás ejecutando esto en una tostadora.
alextgordon
7
@alextgordon: O si uno planea usar valores de exponente más grandes. Expandir el valor del exponente a, por ejemplo, 65521 tomaría alrededor de 28 multiplicaciones y reducciones de módulo si se usa la reducción de fuerza, frente a 65,520 si no se usa.
supercat
+1 por brindar una solución accesible donde está claro exactamente cómo se realiza el cálculo.
jwg
2
@Supercat: tienes toda la razón. Es fácil mejorar el algoritmo, lo cual es relevante si se calcula con mucha frecuencia o si los exponentes son grandes. Pero el mensaje principal es que puede y debe calcularse utilizando aritmética de enteros.
Ronald