Оператор modulo (%) дает другой результат для разных версий .NET в С#

Я шифрую пользовательский ввод для генерации строки для пароля. Но строка кода дает разные результаты в разных версиях фреймворка. Частичный код со значением клавиши, нажатой пользователем:

Клавиша нажата: 1. Переменная ascii равна 49. Значение "e" и "n" после некоторого вычисления:

e = 103, 
n = 143,

Math.Pow(ascii, e) % n

Результат приведенного выше кода:

  • В .NET 3.5 (С#)

    Math.Pow(ascii, e) % n
    

    дает 9.0.

  • В .NET 4 (С#)

    Math.Pow(ascii, e) % n
    

    дает 77.0.

Math.Pow() дает правильный (тот же) результат в обеих версиях.

В чем причина, и есть ли решение?

Ответ 1

Math.Pow работает с числами с плавающей запятой с двойной точностью; таким образом, вы не должны ожидать больше, чем первых 15-17 цифр результата, чтобы быть точным:

Все числа с плавающей запятой также имеют ограниченное число значащих цифр, что также определяет, насколько точно значение с плавающей запятой аппроксимирует действительное число. Значение A Double имеет до 15 десятичных цифр точности, хотя не более 17 цифр поддерживается внутри.

Однако по модулю арифметики все цифры должны быть точными. В вашем случае вы вычисляете 49 103 результат которого состоит из 175 цифр, что делает модульную операцию бессмысленной в обоих ваших ответах.

Чтобы выработать правильное значение, вы должны использовать арифметику произвольной точности, предоставляемую классом BigInteger (представленный в .NET 4.0).

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

Забастовкa >

Изменить. Как отметил Марк Питерс в комментариях ниже, вы должны использовать метод BigInteger.ModPow, который предназначен специально для такого рода операций:

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

Ответ 2

Помимо того, что ваша функция хеширования не очень хорошая * самая большая проблема с вашим кодом заключается не в том, что она возвращает другое число в зависимости от версии .NET, а в том, что в обоих случаях он возвращает совершенно бессмысленное число: правильный ответ на проблему -

49 103 mod 143 = is 114. (ссылка на Wolfram Alpha)

Вы можете использовать этот код для вычисления этого ответа:

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

Причина, по которой ваши вычисления приводят к другому результату, заключается в том, что для получения ответа вы используете промежуточное значение, которое уносит большинство значимых цифр из числа 49 103: только первые 16 из его 175 цифр правильно!

1230824813134842807283798520430636310264067713738977819859474030746648511411697029659004340261471771152928833391663821316264359104254030819694748088798262075483562075061997649

Остальные 159 цифр ошибочны. Однако операция mod ищет результат, который требует, чтобы каждая цифра была правильной, включая самые последние. Таким образом, даже минимальное улучшение точности Math.Pow, которое могло быть реализовано в .NET 4, приведет к резкому отличию вашего вычисления, которое по существу производит произвольный результат.

* Так как этот вопрос говорит о том, чтобы поднять целые числа до больших мощностей в контексте хэширования паролей, может быть очень хорошей идеей прочитать этот ответ link, прежде чем решить, должен ли ваш текущий подход быть изменен для потенциально лучшего.

Ответ 3

То, что вы видите, - это округление ошибки в double. Math.Pow работает с двойным, и разница такова:

.NET 2.0 и 3.5 = > var powerResult = Math.Pow(ascii, e); возвращает:

1.2308248131348429E+174

.NET 4.0 и 4.5 = > var powerResult = Math.Pow(ascii, e); возвращает:

1.2308248131348427E+174

Обратите внимание на последнюю цифру до E, и это вызывает разницу в результате. Это не оператор модуля (%).

Ответ 4

Точность с плавающей точкой может варьироваться от машины к машине и даже на одном компьютере.

Однако .NET создает виртуальную машину для ваших приложений... но есть изменения от версии к версии.

Поэтому вы не должны полагаться на него для получения согласованных результатов. Для шифрования используйте классы, которые предоставляет Framework, а не сворачивайте свои собственные.

Ответ 5

Есть много ответов о том, как плохо код. Однако, почему результат отличается...

Intel FPU используют формат 80-бит внутри, чтобы получить больше точности для промежуточных результатов. Поэтому, если значение находится в регистре процессора, оно получает 80 бит, но когда оно записывается в стек, оно сохраняется в 64 бит.

Я ожидаю, что более новая версия .NET имеет лучший оптимизатор в своей компиляции Just in Time (JIT), поэтому она сохраняет значение в регистре, а не записывает его в стек, а затем считывает его из стека.

Возможно, JIT теперь может возвращать значение в регистре, а не в стеке. Или передайте значение в функцию MOD в регистре.

См. также вопрос о переполнении стека Каковы приложения/преимущества 80-битного расширенного типа данных с высокой точностью?

Другие процессоры, например. ARM даст разные результаты для этого кода.

Ответ 6

Может быть, лучше всего вычислить его, используя только целочисленную арифметику. Что-то вроде:

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

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

Вы можете сравнить производительность с производительностью решения BigInteger, размещенную в других ответах.