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


89

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

Натиснута клавіша: 1. Змінна ascii49. Значення '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

Відповіді:


160

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

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

Однак арифметика за модулем вимагає, щоб усі цифри були точними. У вашому випадку ви обчислюєте 49 103 , результат якого складається з 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 разів швидше для цієї операції.
Mark Peters

8
+1 З редагуванням. ModPow не просто швидкий, він чисельно стабільний!
Рей

2
@maker Ні, відповідь безглузда , не недійсна .
Коді Грей

3
@ makerofthings7: Я в принципі з вами згоден. Однак неточність властива арифметиці з плаваючою точкою, і вважається більш практичним очікувати від розробників обізнаності про ризики, ніж введення обмежень на операції загалом. Якби хтось хотів бути по-справжньому "безпечним", тоді мові також потрібно було б заборонити порівняння рівності з плаваючою комою, щоб уникнути несподіваних результатів, таких як 1.0 - 0.9 - 0.1 == 0.0оцінка до 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;
}

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

1230824813134842807283798520430636310264067713738977819859474030746648511411697029659004340261471771152928833391663821316264359104254030819694748088798262075483562075061997649

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

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


20
Хороша відповідь. Справжній сенс полягає в тому, що це жахлива хеш-функція. ОП потрібно переосмислити рішення та використовувати більш відповідний алгоритм.
david.pfx

1
Ісаак Ньютон: Чи можливо, що місяць притягується до землі так само, як яблуко до землі? @ david.pfx: Справжній сенс полягає в тому, що це жахливий спосіб збирати яблука. Ньютону потрібно переосмислити рішення і, можливо, найняти людину зі сходами.
jwg

2
Коментар @jwg Девіда отримав стільки прихильників з причини. Оригінальне запитання чітко показало, що алгоритм використовувався для хешування паролів, і це дійсно жахливий алгоритм для цієї мети - надзвичайно ймовірно, що він може зламатися між версіями .NET framework, як це вже було продемонстровано. Будь-яка відповідь, в якій не згадується, що ОП має замінити його алгоритм, а не "виправити", це робить йому погану послугу.
Кріс

@Chris Дякую за коментар, який я відредагував, включивши пропозицію Девіда. Я сказав не так сильно, як ви, тому що система OP може бути іграшкою чи викинутим шматочком коду, який він створює для власної розваги. Дякую!
dasblinkenlight

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 тощо, а потім результат = 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

2
@Supercat: Ви абсолютно праві. Поліпшити алгоритм легко, що актуально, якщо він обчислюється дуже часто або показники великі. Але головне повідомлення полягає в тому, що його можна і потрібно обчислювати з використанням цілочисельної арифметики.
Рональд
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.