对于C#中的不同.N​​ET版本,模运算符(%)给出不同的结果


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
当然,问题中的两个答案都是错误的。您似乎并不在乎的事实令人担忧。
David Heffernan 2014年

34
您需要返回几个步骤。“我正在加密用户输入以生成密码字符串”这一部分已经令人怀疑。您实际上想做什么?您要以加密形式还是哈希形式存储密码?您是否要将其用作熵以生成随机值?您的安全目标是什么?
CodesInChaos 2014年

49
尽管此问题说明了浮点算法的一个有趣问题,但如果OP的目标是“对用户输入进行加密以生成用于密码的字符串”,我认为不建议自己进行加密,因此不建议实际执行任何答案。
哈里森·潘恩2014年

18
很好的演示了为什么其他语言禁止使用%浮点数。
Ben Voigt 2014年

5
尽管答案很不错,但是没有一个人回答.NET 3.5和4之间发生了什么变化而导致了不同的行为。
msell 2014年

Answers:


160

Math.Pow处理双精度浮点数;因此,您不应期望结果的前1517位数字更加准确:

所有浮点数还具有有限数量的有效数字,这也决定了浮点值逼近实数的精度。一个Double值的精度最高为15位小数,尽管内部最多可保留17位数字。

但是,模算术要求所有数字都是准确的。在您的情况下,您正在计算49 103,其结果由175位数字组成,这使得模运算对您的两个答案都毫无意义。

若要计算正确的值,应使用BigInteger该类提供的任意精度算术(在.NET 4.0中引入)。

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

编辑:正如Mark Peters在下面的评论中所指出的,您应该使用该BigInteger.ModPow方法,该方法专门用于这种操作:

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

20
+1指出真正的问题,即问题中的代码是完全错误的
David Heffernan 2014年

36
值得注意的是,BigInteger提供了一个ModPow()方法,该方法执行此操作的速度约为5倍(在我现在的快速测试中)。
Mark Peters 2014年

8
+1进行编辑。ModPow不仅速度快,而且数值稳定!
2014年

2
@maker不,答案是没有意义的,不是无效的
科迪·格雷

3
@ makerofthings7:我原则上同意你的看法。但是,不精确性是浮点运算固有的,与一般地对操作施加限制相比,期望开发人员意识到风险是更实际的。如果要真正做到“安全”,那么该语言还需要禁止进行浮点相等比较,以避免出现意想不到的结果,例如对进行1.0 - 0.9 - 0.1 == 0.0求值false
道格拉斯

72

除了您的哈希函数不是一个很好的*之外,您的代码的最大问题还不是它根据.NET的版本返回了不同的数字,而是在两种情况下均返回了一个完全无意义的数字:问题的正确答案是

49 103 mod 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数字中的大多数有效数字:只有其175数字中的前16位是正确的!

1230824813134842807283798520430636310264067713738977819859474030746648511411697029659004340261471771152928833391663821316264359104254030819694748088798262075483562075061997649

其余的159位数字全部错误。但是,mod操作寻求的结果要求每个数字都正确,包括最后一个数字。因此,即使Math.Pow在.NET 4中可能已经实现了对精度的最小改进,也将导致计算的巨大差异,这实际上会产生任意结果。

*由于此问题是关于在密码哈希中将整数提高到高幂,因此在决定是否应将当前方法更改为可能更好的方法之前,阅读此答案链接可能是一个很好的主意。


20
好答案。真正的重点是,这是一个可怕的哈希函数。OP需要重新考虑解决方案,并使用更合适的算法。
david.pfx 2014年

1
艾萨克·牛顿(Isaac Newton):月亮是否有可能像苹果一样被吸引到地球上?@ david.pfx:真正的重点是,这是摘苹果的糟糕方法。牛顿需要重新考虑解决方案,也许雇用一个有梯子的人。
jwg 2014年

2
@jwg David的评论得到了很多支持,这是有原因的。最初的问题明确表明该算法已用于对密码进行哈希处理,并且对于该目的而言确实是一种可怕的算法-正如已经证明的那样,它极有可能在.NET框架的版本之间中断。任何没有提到OP需要替换他的算法而不是“修复”的答案都会对他造成伤害。
克里斯

@Chris感谢您的评论,我进行了编辑,以包括David的建议。我没有像您说的那么强烈,因为OP的系统可能是玩具,也可能是他为自己娱乐而创建的一次性代码。谢谢!
dasblinkenlight 2014年

27

您看到的是舍入误差为两倍。Math.Pow与double配合使用,区别如下:

.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
这是对OP问题的唯一答案吗?我阅读了所有的元数据“我对您的了解不多,但是对n00b的安全性错误的问题”却仍然想知道“为什么3.5和4.0之间始终存在差异?曾经在看着月亮的同时将脚趾踩在一块岩石上,问“哪种岩石是吗?”只能告诉我们“您真正的问题
不在乎

1
@MichaelPaulukonis:这是一个错误的比喻。研究岩石是合理的追求。使用固定精度数据类型执行任意精度算术是完全错误的。我将其与软件招聘人员进行比较,后者询问为什么在编写C#时狗比猫差。如果您是动物学家,那么这个问题可能会有所帮助。对于其他所有人,这毫无意义。
道格拉斯

24

浮点精度可能因机器而异,甚至在同一台机器上也不同

但是,.NET为您的应用程序创建了虚拟机...但是各个版本之间都有变化。

因此,您不应该依赖它来产生一致的结果。对于加密,请使用框架提供的类,而不要滚动自己的类。


10

关于代码错误的方式有很多答案。但是,为什么结果不同?

英特尔的FPU内部使用80位格式,以提高中间结果的精度。因此,如果处理器寄存器中的值是80位,但是将其写入堆栈时,它将以64位存储。

我希望.NET的较新版本在“及时”(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 2014年

7
@alextgordon:或者如果打算使用更大的指数值。如果使用强度降低,则将指数值扩展到例如65521将需要约28的乘法和模量降低,而如果不使用强度降低,则需要65,520。
2014年

+1用于提供可访问的解决方案,在其中可以清楚地清楚地知道如何进行计算。
jwg 2014年

2
@Supercat:你绝对正确。改进算法很容易,如果算法计算频率很高或指数较大,则很重要。但是主要的信息是它可以并且应该使用整数算法来计算。
罗纳德
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.