Operator modulo (%) daje różne wyniki dla różnych wersji platformy .NET w C #

89

Szyfruję dane wejściowe użytkownika w celu wygenerowania ciągu dla hasła. Ale wiersz kodu daje różne wyniki w różnych wersjach frameworka. Kod częściowy z wartością klawisza naciśniętego przez użytkownika:

Wciśnięty klawisz: 1. Zmienna asciito 49. Wartość „e” i „n” po pewnych obliczeniach:

e = 103, 
n = 143,

Math.Pow(ascii, e) % n

Wynik powyższego kodu:

  • W .NET 3.5 (C #)

    Math.Pow(ascii, e) % n
    

    daje 9.0.

  • W .NET 4 (C #)

    Math.Pow(ascii, e) % n
    

    daje 77.0.

Math.Pow() daje poprawny (taki sam) wynik w obu wersjach.

Jaka jest przyczyna i czy istnieje rozwiązanie?

Rajiv
źródło
12
Oczywiście obie odpowiedzi w pytaniu są błędne. Fakt, że wydajesz się tym nie przejmować, jest, no cóż, niepokojący.
David Heffernan
34
Musisz cofnąć się o kilka kroków. „Szyfruję dane wejściowe użytkownika w celu wygenerowania ciągu dla hasła” ta część jest już wątpliwa. Co tak naprawdę chcesz robić? Czy chcesz przechowywać hasło w postaci zaszyfrowanej lub zaszyfrowanej? Czy chcesz użyć tego jako entropii do wygenerowania losowej wartości? Jakie są Twoje cele związane z bezpieczeństwem?
CodesInChaos
49
Chociaż to pytanie ilustruje interesujący problem z arytmetyką zmiennoprzecinkową, jeśli celem OP jest „szyfrowanie danych wejściowych użytkownika w celu wygenerowania ciągu dla hasła”, nie sądzę, aby toczenie własnego szyfrowania było dobrym pomysłem, więc nie polecam faktycznie wdrażając którąkolwiek z odpowiedzi.
Harrison Paine
18
Niezła demonstracja, dlaczego inne języki zabraniają używania %z liczbami zmiennoprzecinkowymi.
Ben Voigt
5
Chociaż odpowiedzi są dobre, żadna z nich nie odpowiada na pytanie, co zmieniło się między .NET 3.5 a 4, co jest przyczyną różnych zachowań.
msell

Odpowiedzi:

160

Math.Powdziała na liczbach zmiennoprzecinkowych o podwójnej precyzji; dlatego nie należy oczekiwać, że dokładność wyniku będzie większa niż pierwsze 15–17 cyfr :

Wszystkie liczby zmiennoprzecinkowe mają również ograniczoną liczbę cyfr znaczących, które również określają, jak dokładnie wartość zmiennoprzecinkowa aproksymuje liczbę rzeczywistą. DoubleWartość ma maksymalnie 15 cyfr po przecinku precyzji, chociaż maksymalnie 17 cyfr jest utrzymywana wewnętrznie.

Jednak arytmetyka modulo wymaga, aby wszystkie cyfry były dokładne. W twoim przypadku obliczasz 49 103 , którego wynik składa się z 175 cyfr, przez co operacja modulo jest bez znaczenia w obu twoich odpowiedziach.

Aby obliczyć poprawną wartość, należy skorzystać z arytmetyki o dowolnej precyzji, jak zapewnia BigIntegerklasa (wprowadzona w .NET 4.0).

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

Edycja : Jak zauważył Mark Peters w komentarzach poniżej, powinieneś użyć BigInteger.ModPowmetody, która jest przeznaczona specjalnie dla tego rodzaju operacji:

int val = (int)BigInteger.ModPow(49, 103, 143);   // gives 114
Douglas
źródło
20
+1 za wskazanie prawdziwego problemu, a mianowicie, że kod w pytaniu jest po prostu błędny
David Heffernan
36
Warto zauważyć, że BigInteger udostępnia metodę ModPow (), która wykonuje (w moim krótkim teście właśnie teraz) około 5 razy szybciej dla tej operacji.
Mark Peters
8
+1 Wraz z edycją. ModPow jest nie tylko szybki, ale także stabilny numerycznie!
Ray
2
@maker Nie, odpowiedź jest bez znaczenia , nie jest nieprawidłowa .
Cody Gray
3
@ makerofthings7: W zasadzie zgadzam się z tobą. Jednak nieprecyzyjność jest nieodłącznym elementem arytmetyki zmiennoprzecinkowej i uważa się, że bardziej praktyczne jest oczekiwanie, że deweloperzy będą świadomi zagrożeń, niż nakładanie ograniczeń na operacje w ogóle. Gdyby ktoś chciał być naprawdę „bezpieczny”, wówczas język musiałby również zabronić porównywania zmiennoprzecinkowych równości, aby uniknąć nieoczekiwanych wyników, takich jak 1.0 - 0.9 - 0.1 == 0.0ocenianie do false.
Douglas
72

Pomijając fakt, że twoja funkcja haszująca nie jest zbyt dobra * , największym problemem z twoim kodem nie jest to, że zwraca inną liczbę w zależności od wersji .NET, ale w obu przypadkach zwraca całkowicie bezsensowną liczbę: prawidłowa odpowiedź na problem brzmi

49 103 mod 143 = jest 114. ( link do Wolfram Alpha )

Możesz użyć tego kodu do obliczenia tej odpowiedzi:

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

Powodem, dla którego obliczenia dają inny wynik, jest to, że w celu uzyskania odpowiedzi używasz wartości pośredniej, która usuwa większość znaczących cyfr liczby 49 103 : tylko pierwsze 16 z 175 cyfr jest poprawnych!

1230824813134842807283798520430636310264067713738977819859474030746648511411697029659004340261471771152928833391663821316264359104254030819694748088798262075483562075061997649

Pozostałe 159 cyfr jest błędnych. Jednak operacja mod szuka wyniku, który wymaga, aby każda cyfra była poprawna, w tym ostatnia. Dlatego nawet najmniejsza poprawa precyzji Math.Powmogłaby zostać zaimplementowana w .NET 4, spowodowałaby drastyczną różnicę w obliczeniach, co w zasadzie daje arbitralny wynik.

* Ponieważ to pytanie dotyczy podniesienia liczb całkowitych do wysokich potęg w kontekście haszowania haseł, może być bardzo dobrym pomysłem przeczytanie tego linku odpowiedzi przed podjęciem decyzji, czy obecne podejście powinno zostać zmienione na potencjalnie lepsze.

Sergey Kalinichenko
źródło
20
Dobra odpowiedź. Prawda jest taka, że ​​jest to okropna funkcja skrótu. OP musi ponownie przemyśleć rozwiązanie i użyć bardziej odpowiedniego algorytmu.
david.pfx
1
Isaac Newton: Czy to możliwe, że księżyc jest przyciągany do ziemi w taki sam sposób, jak jabłko przyciągane jest do ziemi? @ david.pfx: Prawda jest taka, że ​​to okropny sposób zbierania jabłek. Newton musi przemyśleć rozwiązanie i być może zatrudnić człowieka z drabiną.
jwg
2
Komentarz @jwg Davida nie bez powodu otrzymał tak wiele głosów pozytywnych. Z pierwotnego pytania jasno wynikało, że algorytm był używany do haszowania haseł i jest to rzeczywiście okropny algorytm do tego celu - jest bardzo prawdopodobne, że zerwie między wersjami frameworka .NET, jak już wykazano. Każda odpowiedź, która nie wspomina, że ​​OP musi zastąpić swój algorytm zamiast „naprawić”, wyrządza mu krzywdę.
Chris,
@Chris Dzięki za komentarz, zredagowałem, uwzględniając sugestię Davida. Nie wyraziłem tego tak mocno, jak ty, ponieważ system OP może być zabawką lub wyrzuconym fragmentem kodu, który tworzy dla własnej rozrywki. Dzięki!
Sergey Kalinichenko
27

To, co widzisz, to podwójny błąd zaokrąglenia. Math.Powdziała z double a różnica jest jak poniżej:

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

1.2308248131348429E+174

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

1.2308248131348427E+174

Zwróć uwagę na ostatnią cyfrę przed Ei to powoduje różnicę w wyniku. To nie jest operator modułu (%) .

Habib
źródło
3
święta krowa czy to JEDYNA odpowiedź na pytanie PO? Przeczytałem wszystkie meta „bla, bla, złe pytanie dotyczące bezpieczeństwa. Wiem więcej niż ty, n00b” i nadal zastanawiałem się, „dlaczego ta konsekwentna rozbieżność między 3,5 a 4,0? jest to? ”Tylko do powiedzenia„ Twoim prawdziwym problemem jest to, że nie patrzysz na swoje stopy ”lub„ Czego oczekujesz nosząc domowe sandały w nocy? !!! ”DZIĘKI!
Michael Paulukonis
1
@MichaelPaulukonis: To fałszywa analogia. Badanie skał jest uprawnionym zajęciem; wykonywanie arytmetyki o dowolnej precyzji przy użyciu typów danych o stałej precyzji jest po prostu błędne. Porównałbym to do osoby zajmującej się rekrutacją oprogramowania, która pyta, dlaczego psy są gorsze od kotów w pisaniu C #. Jeśli jesteś zoologiem, to pytanie może mieć jakąś wartość; dla wszystkich innych jest to bezcelowe.
Douglas,
24

Precyzja zmiennoprzecinkowa może się różnić w zależności od maszyny, a nawet na tej samej maszynie .

Jednak .NET tworzy maszynę wirtualną dla twoich aplikacji ... ale są zmiany z wersji na wersję.

Dlatego nie powinieneś polegać na nim, aby uzyskać spójne wyniki. Do szyfrowania użyj klas, które zapewnia Framework, zamiast toczenia własnych.

Joe
źródło
10

Istnieje wiele odpowiedzi dotyczących sposobu, w jaki kod jest zły. Jednak, dlaczego wynik jest inny…

Jednostki FPU Intela używają wewnętrznie formatu 80-bitowego , aby uzyskać większą precyzję wyników pośrednich. Więc jeśli wartość znajduje się w rejestrze procesora, otrzymuje 80 bitów, ale kiedy jest zapisywana na stosie, jest przechowywana na 64 bitach .

Oczekuję, że nowsza wersja .NET ma lepszy optymalizator w kompilacji Just in Time (JIT), więc zachowuje wartość w rejestrze zamiast zapisywać ją na stosie, a następnie odczytywać ją z powrotem ze stosu.

Może się zdarzyć, że JIT może teraz zwrócić wartość w rejestrze, a nie na stosie. Lub przekaż wartość do funkcji MOD w rejestrze.

Zobacz także pytanie o przepełnienie stosu Jakie są zastosowania / zalety typu danych o rozszerzonej precyzji 80-bitowej?

Inne procesory, np. ARM, dadzą inne wyniki dla tego kodu.

Ian Ringrose
źródło
6

Może najlepiej jest obliczyć to samodzielnie, używając tylko arytmetyki całkowitej. Coś jak:

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

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

Możesz porównać wydajność z wydajnością rozwiązania BigInteger opublikowanego w innych odpowiedziach.

Ronald
źródło
7
Wymagałoby to 103 mnożeń i redukcji modułów. Można zrobić lepiej, obliczając e2 = e * e% n, e4 = e2 * e2% n, e8 = e4 * e4% n itd., A następnie wynik = e * e2% n * e4% n * e32% n * e64% n. Łącznie 11 mnożenia i redukcji modułu. Biorąc pod uwagę wielkość zaangażowanych liczb, można by wyeliminować kilka dodatkowych redukcji modułu, ale byłoby to niewielkie w porównaniu ze zmniejszeniem 103 operacji do 11.
superkat
2
@supercat Niezła matematyka, ale w praktyce ma znaczenie tylko wtedy, gdy używasz tego na tosterze.
alextgordon
7
@alextgordon: Lub jeśli planujesz użyć większych wartości wykładników. Zwiększenie wartości wykładnika do np. 65521 wymagałoby około 28 mnożeń i redukcji modułu, jeśli ktoś stosuje redukcję siły, w porównaniu z 65 520, jeśli tego nie robi.
supercat
+1 za udostępnienie rozwiązania, w którym jest jasne, w jaki sposób wykonywane są obliczenia.
jwg
2
@Supercat: masz całkowitą rację. Łatwo jest ulepszyć algorytm, co jest istotne, jeśli albo jest obliczany bardzo często, albo wykładniki są duże. Ale głównym przesłaniem jest to, że można i należy je obliczyć za pomocą arytmetyki liczb całkowitych.
Ronald,