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

89

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

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

e = 103, 
n = 143,

Math.Pow(ascii, e) % n

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

  • В .NET 3.5 (C #)

    Math.Pow(ascii, e) % n
    

    дает 9.0.

  • В .NET 4 (C #)

    Math.Pow(ascii, e) % n
    

    дает 77.0.

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

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

Раджив
источник
12
Конечно, оба ответа на вопрос неверны. То, что вас это не волнует, в общем, вызывает беспокойство.
Дэвид Хеффернан
34
Вам нужно вернуться на несколько шагов назад. «Я шифрую ввод пользователя для генерации строки пароля» эта часть уже сомнительна. Что ты на самом деле хочешь делать? Вы хотите хранить пароль в зашифрованном или хешированном виде? Вы хотите использовать это как энтропию для генерации случайного значения? Каковы ваши цели безопасности?
CodesInChaos
49
Хотя этот вопрос иллюстрирует интересную проблему с арифметикой с плавающей запятой, если целью OP является «шифрование ввода пользователя для генерации строки для пароля», я не думаю, что развертывание собственного шифрования - хорошая идея, поэтому я бы не рекомендовал фактически реализуя любой из ответов.
Харрисон Пейн
18
Хорошая демонстрация, почему другие языки запрещают использование %чисел с плавающей запятой.
Бен Фойгт
5
Хотя ответы хороши, ни один из них не отвечает на вопрос о том, что изменилось между .NET 3.5 и 4, что вызывает различное поведение.
msell 01

Ответы:

160

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

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

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

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

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

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

int val = (int)BigInteger.ModPow(49, 103, 143);   // gives 114
Дуглас
источник
20
+1 за указание на настоящую проблему, а именно на то, что код в вопросе явно неправильный
Дэвид Хеффернан
36
Стоит отметить, что BigInteger предоставляет метод ModPow (), который (в моем быстром тесте только что) выполняет эту операцию примерно в 5 раз быстрее.
Марк Петерс
8
+1 С правкой. ModPow не просто быстр, он численно стабилен!
Ray
2
@maker Нет, ответ бессмысленный , не недействительный .
Коди Грей
3
@ makerofthings7: Я согласен с вами в принципе. Однако неточность присуща арифметике с плавающей запятой, и считается более практичным ожидать, что разработчики будут осведомлены о рисках, чем налагать ограничения на операции в целом. Если кто-то хочет быть по-настоящему «безопасным», тогда язык также должен запретить сравнения на равенство с плавающей запятой, чтобы избежать неожиданных результатов, таких как 1.0 - 0.9 - 0.1 == 0.0вычисление to false.
Дуглас
72

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

49 103 мод 143 = является 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;
}

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

1230824813134842807283798520430636310264067713738977819859474030746648511411697029659004340261471771152928833391663821316264359104254030819694748088798262075483562075061997649

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

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

Сергей Калиниченко
источник
20
Хороший ответ. Дело в том, что это ужасная хеш-функция. OP необходимо переосмыслить решение и использовать более подходящий алгоритм.
david.pfx
1
Исаак Ньютон: Возможно ли, что Луна притягивается к Земле так же, как яблоко притягивается к Земле? @ david.pfx: Дело в том, что это ужасный способ собирать яблоки. Ньютону нужно переосмыслить решение и, возможно, нанять человека с лестницей.
jwg 03
2
@jwg Комментарий Дэвида не зря получил столько голосов. Исходный вопрос прояснил, что алгоритм использовался для хеширования паролей, и это действительно ужасный алгоритм для этой цели - очень вероятно, что он сломается между версиями .NET framework, как уже было продемонстрировано. Любой ответ, в котором не упоминается, что OP должен заменить свой алгоритм, а не «исправить», оказывает ему медвежью услугу.
Крис
@Chris Спасибо за комментарий, я отредактировал, чтобы включить предложение Дэвида. Я не говорил об этом так сильно, как вы, потому что система OP может быть игрушкой или одноразовым фрагментом кода, который он создает для собственного развлечения. Благодарность!
Сергей Калиниченко
27

Вы видите ошибку округления в два раза. 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которая вызывает разницу в результате. Это не оператор модуля (%) .

Хабиб
источник
3
святая корова, это ЕДИНСТВЕННЫЙ ответ на вопрос ОП? Я прочитал все мета «бла-бла, неправильный вопрос по безопасности, я знаю больше, чем вы, n00b», и все еще задавался вопросом: «Почему такое постоянное расхождение между 3,5 и 4,0? это? »Только для того, чтобы вам сказали:« Твоя настоящая проблема не в том, чтобы смотреть на твои ноги »или« Чего ты ожидаешь, надев ночью самодельные сандалии? !!! »СПАСИБО!
Майкл Паулюконис
1
@MichaelPaulukonis: Это ложная аналогия. Изучение горных пород - законное занятие; выполнение арифметики произвольной точности с использованием типов данных фиксированной точности просто неправильно. Я бы сравнил это с тем, как рекрутер программного обеспечения спрашивает, почему собаки хуже кошек пишут на C #. Если вы зоолог, в этом вопросе есть основания; для всех остальных это бессмысленно.
Дуглас
24

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

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

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

Джо
источник
10

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

FPU Intel внутренне используют 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, опубликованного в других ответах.

Рональд
источник
7
Это потребует 103 умножения и уменьшения модуля. Можно добиться большего, вычислив e2 = e * e% n, e4 = e2 * e2% n, e8 = e4 * e4% n и т.д., а затем result = e * e2% n * e4% n * e32% n * e64% n. Всего 11 умножений и редукций по модулю. Учитывая размер задействованных чисел, можно было бы исключить еще несколько сокращений модуля, но это было бы незначительно по сравнению с сокращением 103 операций до 11.
supercat
2
@supercat Хорошая математика, но на практике актуальна только в том случае, если вы запускаете это на тостере.
alextgordon
7
@alextgordon: Или если кто-то планирует использовать более высокие значения экспоненты. Расширение значения экспоненты, например, до 65521 потребует около 28 умножений и уменьшения модуля, если используется снижение прочности, по сравнению с 65 520, если не использовать.
supercat
+1 за доступное решение, где точно ясно, как производится расчет.
jwg 03
2
@Supercat: ты абсолютно прав. Алгоритм легко улучшить, что актуально, если он вычисляется очень часто или показатели большие. Но главный посыл заключается в том, что его можно и нужно рассчитывать с помощью целочисленной арифметики.
Рональд