L'operatore modulo (%) fornisce un risultato diverso per le diverse versioni di .NET in C #


89

Sto crittografando l'input dell'utente per generare una stringa per la password. Ma una riga di codice fornisce risultati diversi nelle diverse versioni del framework. Codice parziale con valore del tasto premuto dall'utente:

Tasto premuto: 1. La variabile asciiè 49. Il valore di "e" e "n" dopo alcuni calcoli:

e = 103, 
n = 143,

Math.Pow(ascii, e) % n

Risultato del codice sopra:

  • In .NET 3.5 (C #)

    Math.Pow(ascii, e) % n

    9.0.

  • In .NET 4 (C #)

    Math.Pow(ascii, e) % n

    77.0.

Math.Pow() fornisce il risultato corretto (lo stesso) in entrambe le versioni.

Qual è la causa e c'è una soluzione?


12
Naturalmente, entrambe le risposte alla domanda sono sbagliate. Il fatto che sembri non preoccuparti di questo è, beh, preoccupante.
David Heffernan

34
È necessario tornare indietro di diversi passaggi. "Sto crittografando l'input dell'utente per generare una stringa per la password" questa parte è già dubbia. Cosa vuoi fare veramente? Vuoi memorizzare una password in forma crittografata o hash? Vuoi usarlo come entropia per generare un valore casuale? Quali sono i tuoi obiettivi di sicurezza?
CodesInChaos

49
Sebbene questa domanda illustri un problema interessante con l'aritmetica in virgola mobile, se l'obiettivo dell'OP è "crittografare l'input dell'utente per generare una stringa per la password", non penso che il rollio della tua crittografia sia una buona idea, quindi non lo consiglierei implementando effettivamente una qualsiasi delle risposte.
Harrison Paine

18
Bella dimostrazione del motivo per cui altre lingue vietano l'uso di %numeri in virgola mobile.
Ben Voigt

5
Sebbene le risposte siano buone, nessuna di esse risponde alla domanda su cosa è cambiato tra .NET 3.5 e 4 che sta causando il comportamento diverso.
msell

Risposte:


160

Math.Powfunziona su numeri in virgola mobile a doppia precisione; quindi, non dovresti aspettarti che più delle prime 15-17 cifre del risultato siano accurate:

Tutti i numeri in virgola mobile hanno anche un numero limitato di cifre significative, che determina anche la precisione con cui un valore in virgola mobile approssima un numero reale. Un Doublevalore ha fino a 15 cifre decimali di precisione, sebbene internamente venga mantenuto un massimo di 17 cifre.

Tuttavia, l'aritmetica del modulo richiede che tutte le cifre siano accurate. Nel tuo caso, stai calcolando 49103 , il cui risultato è composto da 175 cifre, rendendo l'operazione modulo priva di significato in entrambe le tue risposte.

Per calcolare il valore corretto, è necessario utilizzare l'aritmetica a precisione arbitraria, come fornito dalla BigIntegerclasse (introdotta in .NET 4.0).

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

Modifica : come sottolineato da Mark Peters nei commenti seguenti, dovresti usare il BigInteger.ModPowmetodo, che è inteso specificamente per questo tipo di operazione:

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

20
+1 per aver sottolineato il vero problema, vale a dire che il codice nella domanda è chiaramente sbagliato
David Heffernan

36
Vale la pena notare che BigInteger fornisce un metodo ModPow () che esegue (nel mio test rapido solo ora) circa 5 volte più velocemente per questa operazione.
Mark Peters

8
+1 Con la modifica. ModPow non è solo veloce, è numericamente stabile!
Ray

2
@maker No, la risposta è priva di significato , non valida .
Cody Grey

3
@ makerofthings7: in linea di principio sono d'accordo con te. Tuttavia, l'imprecisione è inerente all'aritmetica in virgola mobile ed è ritenuto più pratico aspettarsi che gli sviluppatori siano consapevoli dei rischi, piuttosto che imporre restrizioni sulle operazioni in generale. Se si vuole essere veramente "sicuri", il linguaggio dovrebbe anche vietare i confronti di uguaglianza in virgola mobile, per evitare risultati inaspettati come la 1.0 - 0.9 - 0.1 == 0.0valutazione false.
Douglas

72

A parte il fatto che la tua funzione di hashing non è molto buona * , il problema più grande con il tuo codice non è che restituisce un numero diverso a seconda della versione di .NET, ma che in entrambi i casi restituisce un numero completamente privo di significato: la risposta corretta al problema è

49 103 mod 143 = è 114. ( collegamento in Wolfram Alpha )

Puoi usare questo codice per calcolare questa risposta:

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;
}

Il motivo per cui il tuo calcolo produce un risultato diverso è che per produrre una risposta, usi un valore intermedio che elimina la maggior parte delle cifre significative del numero 49103 : solo le prime 16 delle sue 175 cifre sono corrette!

1230824813134842807283798520430636310264067713738977819859474030746648511411697029659004340261471771152928833391663821316264359104254030819694748088798262075483562075061997649

Le restanti 159 cifre sono tutte sbagliate. L'operazione mod, tuttavia, cerca un risultato che richieda che ogni singola cifra sia corretta, comprese le ultime. Pertanto, anche il più piccolo miglioramento della precisione di Math.Powquello potrebbe essere stato implementato in .NET 4, comporterebbe una drastica differenza nel calcolo, che essenzialmente produce un risultato arbitrario.

* Poiché questa domanda parla dell'innalzamento di numeri interi a potenze elevate nel contesto dell'hashing delle password, potrebbe essere un'ottima idea leggere questo link di risposta prima di decidere se il tuo approccio attuale dovrebbe essere cambiato per uno potenzialmente migliore.


20
Buona risposta. Il vero punto è che questa è una terribile funzione hash. OP deve ripensare la soluzione e utilizzare un algoritmo più appropriato.
david.pfx

1
Isaac Newton: È possibile che la luna sia attratta dalla terra nello stesso modo in cui la mela è attratta dalla terra? @ david.pfx: Il vero punto è che questo è un modo terribile per raccogliere le mele. Newton deve ripensare alla soluzione e forse assumere un uomo con una scala.
jwg

2
Il commento di @jwg David ha ottenuto così tanti voti positivi per un motivo. La domanda originale ha chiarito che l'algoritmo veniva utilizzato per l'hash delle password, ed è davvero un algoritmo terribile per quello scopo: è estremamente probabile che si interrompa tra le versioni del framework .NET, come è già stato dimostrato. Qualsiasi risposta che non menziona che l'OP deve sostituire il suo algoritmo piuttosto che "aggiustarlo" gli sta rendendo un disservizio.
Chris

@ Chris Grazie per il commento, ho modificato per includere il suggerimento di David. Non l'ho detto così forte come te, perché il sistema di OP potrebbe essere un giocattolo o un pezzo di codice usa e getta che costruisce per il proprio divertimento. Grazie!
dasblinkenlight

27

Quello che vedi è un errore di arrotondamento in doppio. Math.Powfunziona con double e la differenza è la seguente:

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

1.2308248131348429E+174

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

1.2308248131348427E+174

Notare l'ultima cifra prima Ee che sta causando la differenza nel risultato. Non è l'operatore modulo (%) .


3
vacca sacra è questa l'UNICA risposta alla domanda dei PO? Ho letto tutta la meta "bla bla domanda sbagliata sulla sicurezza, ne so più di te n00b" e mi chiedevo ancora "perché la discrepanza costante tra 3.5 e 4.0? Hai mai sbattuto la punta del piede su una roccia mentre guardavi la luna e chiesto" che tipo di roccia è questo? "Solo per sentirsi dire" Il tuo vero problema non è guardare i tuoi piedi "o" Cosa ti aspetti quando indossi sandali fatti in casa di notte? !!! "GRAZIE!
Michael Paulukonis

1
@MichaelPaulukonis: Questa è una falsa analogia. Lo studio delle rocce è una ricerca legittima; eseguire operazioni aritmetiche a precisione arbitraria utilizzando tipi di dati a precisione fissa è semplicemente sbagliato. Lo paragonerei a un reclutatore di software che chiede perché i cani sono peggio dei gatti nello scrivere C #. Se sei uno zoologo, la domanda potrebbe avere qualche merito; per tutti gli altri è inutile.
Douglas

24

La precisione in virgola mobile può variare da macchina a macchina e anche sulla stessa macchina .

Tuttavia, .NET crea una macchina virtuale per le tue app ... ma ci sono modifiche da versione a versione.

Pertanto non dovresti fare affidamento su di esso per produrre risultati coerenti. Per la crittografia, utilizzare le classi fornite dal Framework anziché eseguire il rollio delle proprie.


10

Ci sono molte risposte sul modo in cui il codice è cattivo. Tuttavia, per quanto riguarda il motivo per cui il risultato è diverso ...

Le FPU di Intel utilizzano internamente il formato a 80 bit per ottenere una maggiore precisione per risultati intermedi. Quindi, se un valore è nel registro del processore, ottiene 80 bit, ma quando viene scritto nello stack viene memorizzato a 64 bit .

Mi aspetto che la versione più recente di .NET abbia un ottimizzatore migliore nella sua compilazione Just in Time (JIT), quindi mantiene un valore in un registro piuttosto che scriverlo nello stack e poi leggerlo dallo stack.

È possibile che il JIT ora possa restituire un valore in un registro anziché nello stack. Oppure passare il valore alla funzione MOD in un registro.

Vedi anche Domanda sull'overflow dello stack Quali sono le applicazioni / i vantaggi di un tipo di dati a precisione estesa a 80 bit?

Altri processori, ad esempio ARM, daranno risultati diversi per questo codice.


6

Forse è meglio calcolarlo da soli usando solo l'aritmetica dei numeri interi. Qualcosa di simile a:

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

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

È possibile confrontare le prestazioni con le prestazioni della soluzione BigInteger pubblicata nelle altre risposte.


7
Ciò richiederebbe 103 moltiplicazioni e riduzioni del modulo. Si può fare di meglio calcolando e2 = e * e% n, e4 = e2 * e2% n, e8 = e4 * e4% n, ecc. E quindi risultato = e * e2% n * e4% n * e32% n * e64% n. Un totale di 11 moltiplicazioni e riduzioni del modulo. Data la dimensione dei numeri coinvolti, si potrebbe eliminare qualche ulteriore riduzione del modulo, ma sarebbe minore rispetto alla riduzione di 103 operazioni a 11.
supercat

2
@supercat Bella matematica, ma in pratica rilevante solo se stai eseguendo questo su un tostapane.
alextgordon

7
@alextgordon: o se si prevede di utilizzare valori di esponente più grandi. Espandere il valore dell'esponente ad es. 65521 richiederebbe circa 28 moltiplicazioni e riduzioni del modulo se si utilizza la riduzione della forza, contro 65.520 se non lo si fa.
supercat

+1 per fornire una soluzione accessibile in cui è chiaro esattamente come viene eseguito il calcolo.
jwg

2
@Supercat: hai perfettamente ragione. È facile migliorare l'algoritmo, il che è rilevante se viene calcolato molto spesso o se gli esponenti sono grandi. Ma il messaggio principale è che può e deve essere calcolato utilizzando l'aritmetica dei numeri interi.
Ronald
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.