Оператор 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, размещенную в других ответах.