HW11中对ageVar采用缓存优化的等价性证明(包括溢出情况)
概要
我们知道,第三次作业里age上限变为2000,而如果缓存年龄的平方和,2000*2000*800 > 2147483647,会溢出。但是实际上,我们仍然能通过缓存得到正确的结果。这是因为,计算机内进行的二进制运算其实每一步都进行了 (&0xffff\_ffff) 操作,有交换律、结合律、平方公式成立。即使在溢出的情况下,两个式子仍然是等价的。本文试着利用二进制运算和无符号数运算的关系,以及无符号数运算的性质,来证明这一点。
引论
无符号数的运算和二进制的运算
我们知道,在机器语言里,不管一个字是有符号数还是无符号数,采用的四则运算指令都是相同的(MIPS中add和addu的区别也只是是否会触发算术溢出异常,而具体的运算方式仍是一样的)。
我们现在Java程序里,做的其实是上述计算机里的二进制运算,和数学世界的运算是有所不同的。我们将计算机二进制的“四则运算”分别记为 (dot{+} quad dot{-} quad dot{*} quad dot{/}) 。
这四种运算肯定不是随便定义的,和现实中的数学运算肯定有着一定的关系。这里我们尝试从无符号数的角度,建立计算机二进制运算和无符号数运算之间的关系。(我们在这里不讨论除法,因为这和我们的主题无关 因为除法很麻烦作者根本不会(x))
为此,我们引入一些记号。同一个32位二进制数据,既可以表示一个无符号整数,又可以表示为一个有符号整数。我们将某个数据记为 (d) ,记 (U(d)) 为按无符号数解释 (d) 得到的数字,(T(d)) 为按二的补码有符号数解释 (d) 得到的数字。我们将相应的逆过程,将一个数字按照有/无符号数解读并转化为二进制数据的过程,记作 (UD(x)) / (TD(x))。
根据我们已有的知识,在未发生溢出的情况下,计算机二进制运算的结果 和 把二进制数据解读为无符号数进行运算的结果 是相同的。现在我们主要需要考虑的内容是:数学中的无符号数是无位数限制的。结合这一点,我们可以得到:
(U(a dot{+} b) = (U(a) + U(b)) \% 0x1\_0000\_0000)
(U(a dot{-} b) = (U(a) - U(b)) \% 0x1\_0000\_0000)
(U(a dot{*} b) = (U(a) * U(b)) \% 0x1\_0000\_0000)
其中a,b为两个二进制数据。
进一步
(a dot{+} b = UD((U(a) + U(b)) \% 0x1\_0000\_0000))
(a dot{-} b = UD((U(a) - U(b)) \% 0x1\_0000\_0000))
(a dot{*} b = UD((U(a) * U(b)) \% 0x1\_0000\_0000))
其中a,b为两个二进制数据。
取模运算
此处参考了取模运算的性质 By varinic
(取模和取余似乎还略有不同,我不太懂这个,所以用了本文几乎全程使用无符号数来避开这个问题。
基本性质
若p|(a-b),则a≡b (% p)。例如 11 ≡ 4 (% 7), 18 ≡ 4(% 7)
(a % p)=(b % p)意味a≡b (% p)
对称性:a≡b (% p)等价于b≡a (% p)
传递性:若a≡b (% p)且b≡c (% p) ,则a≡c (% p)
运算规则
模运算与基本四则运算有些相似,但是除法例外。其规则如下:
(a + b) % p = (a % p + b % p) % p (1)
(a - b) % p = (a % p - b % p) % p (2)
(a * b) % p = (a % p * b % p) % p (3)
a ^ b % p = ((a % p)^b) % p (4)
结合律:
((a+b) % p + c) % p = (a + (b+c) % p) % p (5)
((a*b) % p * c)% p = (a * (b*c) % p) % p (6)
交换律:
(a + b) % p = (b+a) % p (7)
(a * b) % p = (b * a) % p (8)
分配律:
((a +b)% p * c) % p = ((a * c) % p + (b * c) % p) % p (9)
证明
我们想要证明的是:我们利用缓存的算式和JML对ageVar的算式是等价的。
注意,JML中的加法也是计算机里的二进制加法。
我们以下提到的变量都指他们的二进制数据。
缓存:
JML:
我们只需要证明分式的上半部分恒等。
为了方便,我们记(M(U(d)) = U(d) \% 0x1\_0000\_0000)
证明平方式对二进制运算成立
首先,针对每一个小单元
(这什么鬼玩意)
我们注意到 (U(UD(x)) = x),x为一个无符号数,配合利用(1)(2)(3)三条运算规律可以去掉多余的(M())记号,化简一下:
再次利用(1)(2)(3)三条运算规律,在适当位置增加冗余的(M())记号来运用我们的无符号数-二进制数互换。我们就有了:
P.S.:写到后面我发现直接证二进制运算有分配律,加上后面的交换律结合律不就直接成了(瘫
证明交换律对二进制运算成立
证明结合律对二进制运算成立
即证得
之后就是简单的变形了,就不再赘述。
总结
有了交换律,结合律和平方公式,我们很容易就能将JML给出的计算公式转化为我们用的利用了缓存的公式。而且这个转化过程经过数学证明是恒等的。不用管是否溢出,同样的二进制age数据,在二进制运算的条件下,用JML和缓存方法得到的ageVar数据一定是相等的。就算ageVar本身溢出了,我们得到的溢出后结果也会是一样的。
本文通篇使用无符号整数,是因为无符号整数运算和二进制数据运算的转换比较好处理,学过数论的大佬们也许可以用有符号整数的运算导出来类似的结果吧(咱是不会了。
至于担心age是负数的时候能不能用的,肯定是能用的,因为无符号数在这里就是一个介质。我们知道无符号数的运算性质,知道无符号数和二进制数据之间的关系,然后通过这些来求二进制数据的运算规律。换一个别的,应该也是能做的(不过可能更麻烦点)。
顺便,有的人说用了long可能会炸,其实是因为先做了除法再转成int。直观的例子就是一个刚好溢出int一点点,但是/n之后在int范围内的结果的数,long先做除法再转int就会是正的,long先转int再除就是负的了。数学上来说,因为除法的模p运算性质不像加减乘那么好,而long相当于%0x1_0000_0000_0000_0000和int不同,就炸了。
咱数学学的不好,也没搞过数论啊这些,所以如果有错还请不要过分嘲笑(正色(x)),发现问题的话就麻烦在讨论区提点一手,我会及时更正。