Can consecutive truncating integer divisions be replaced with a multiplication?
在具有有理数的小学数学中,表达式
当
你可以假设乘法不会溢出(如果是的话,显然它们不是等价的)。
答案是"是"(至少对于非负整数)。这是从除法算法得出的,该算法表明对于任何正整数
在
1 2 | a = b*q_1 + r_1 // here q_1 = a/b and 0 <= r_1 <= b-1 q_1 = c*q_2 + r_2 // here q_2 = q_1/c = a/b/c and 0 <= r_2 <= c-1 |
但是之后
1 | a = b*q_1 + r_1 = b*(c*q_2 + r_2) + r_1 = (b*c)*q_2 + b*r_2 + r1 |
注意
由此得出
是的,关于整数。有人已经发布(并删除了?)一个如何在浮点上工作的示例。 (虽然它可能足够接近您的应用程序。)
@JohnColeman有一个理论论证,但这是一个来自实验的论证。如果您运行此代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | #include <stdio.h> #include <stdlib.h> #include <limits.h> #define LIMIT 100 #define NUM_LIMIT 2000 int main() { for(int div1 = -LIMIT; div1 < LIMIT; div1++) { for(int div2 = -LIMIT; div2 < LIMIT; div2++) { if(div1 == 0 || div2 == 0) continue; for(int numerator = -NUM_LIMIT; numerator < NUM_LIMIT; numerator++) { int a = (numerator / div1) / div2; int b = numerator / (div1 * div2); if(a != b) { printf("%d %d ", a, b); exit(1); } } } } return 0; } |
它尝试双向划分,如果它们不同则报告错误。以下列方式运行它不会报告错误:
- div1,div2从-5到5,分子从-INT_MAX / 100到INT_MAX / 100
- div1,div2从-2000到2000,分子从-2000到2000
因此,我敢打赌,这将适用于任何整数。 (再次假设div1 * div2不会溢出。)
通过实验观察有助于了解如何应用数学。
软件不会改变数学,身份或其他操作,以简化或使用变量更改方程。软件执行要求的操作。固定点完美地完成它们,除非你溢出。
从数学而不是软件,我们知道b或c都不能为零。软件添加的是b * c也不能为零。如果它溢出则结果是错误的。
a / b / c =(a / b)*(1 / c)=(a * 1)/(bc)= a /(bc)来自小学,你可以实现a / b / c或a /( b * c)用您的编程语言,这是您的选择。大多数情况下,如果你坚持整数,结果是不正确的,因为没有表示分数。如果使用浮点,相同的原因,没有足够的位来存储无限大或小的数字,那么结果是不正确的时间,就像纯数学一样。那么你在哪里遇到这些限制?您可以通过简单的实验开始看到这一点。
编写一个程序,遍历0到15之间所有可能的数字组合,a,b和c计算a / b / c和a /(b * c)并进行比较。由于这些是4位数字,因此如果您想查看编程语言将如何处理,那么中间值也不能超过4位。只打印零除以及它们不匹配的区别。
马上你会看到允许任何值为零会导致一个非常无趣的实验,你要么得到很多除以零或0 / something_not_zero不是一个有趣的结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | 1 2 8 : divide by zero 1 3 11 : 1 0 1 4 4 : divide by zero 1 4 8 : divide by zero 1 4 12 : divide by zero 1 5 13 : 1 0 1 6 8 : divide by zero 1 7 7 : 1 0 1 8 2 : divide by zero 1 8 4 : divide by zero 1 8 6 : divide by zero 1 8 8 : divide by zero 1 8 10 : divide by zero 1 8 12 : divide by zero 1 8 14 : divide by zero 1 9 9 : 1 0 1 10 8 : divide by zero 1 11 3 : 1 0 1 12 4 : divide by zero 1 12 8 : divide by zero 1 12 12 : divide by zero 1 13 5 : 1 0 1 14 8 : divide by zero 1 15 15 : 1 0 2 2 8 : divide by zero 2 2 9 : 1 0 2 3 6 : 1 0 2 3 11 : 2 0 |
所以到目前为止答案匹配。对于小的a或特别是a = 1,这是有道理的,结果要么是0还是1.两条路径都会让你到达那里。
1 | 1 2 8 : divide by zero |
至少对于a = 1,b = 1。 c = 1给出1其余的给出0的结果。
2 * 8 = 16或0x10这是太多的位,所以它溢出,结果是0x0除以零,这样你就必须寻找无论是什么,浮点数或固定点。
1 | 1 3 11 : 1 0 |
第一个有趣的
1 /(3 * 11)= 1 / 0x21表示1/1 = 1;
1/3 = 0,0 / 11 = 0。
所以他们不匹配。 3 * 11溢出。
这继续下去。
所以制作更大的数字可能会让这更有趣吗?无论如何,一个小变量将使结果大部分时间为0。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | 15 2 8 : divide by zero 15 2 9 : 7 0 15 2 10 : 3 0 15 2 11 : 2 0 15 2 12 : 1 0 15 2 13 : 1 0 15 2 14 : 1 0 15 2 15 : 1 0 15 3 6 : 7 0 15 3 7 : 3 0 15 3 8 : 1 0 15 3 9 : 1 0 15 3 10 : 1 0 15 3 11 : 15 0 15 3 12 : 3 0 15 3 13 : 2 0 15 3 14 : 1 0 15 3 15 : 1 0 15 4 4 : divide by zero 15 4 5 : 3 0 15 4 6 : 1 0 15 4 7 : 1 0 15 4 8 : divide by zero 15 4 9 : 3 0 15 2 9 : 7 0 |
15 /(2 * 9)= 15 / 0x12 = 15/2 = 7。
15/2 = 7; 7/9 = 0;
1 2 | 15 3 10 : 1 0 15 3 11 : 15 0 |
这两个案件溢出并不有趣。
所以改变你的程序只显示结果不匹配的程序,但也没有溢出的b * c ....没有输出。使用4位值与8位对比128位进行此操作之间没有任何魔力或区别....它只是允许您有更多可能有效的结果。
0xF * 0xF = 0xE1,您可以很容易地看到这在二进制中进行长乘法,最坏的情况是覆盖所有可能的N位值,您需要2 * N位来存储结果而不会溢出。因此,对于除法而言,由N / 2个比特分母限制的N比特分子可以覆盖每个具有N比特结果的所有固定点值。 0xFFFF / 0xFF = 0x101。 0xFFFF / 0x01 = 0xFFFF。
因此,如果你想做这个数学并且可以确保没有数字超过N位,那么如果你使用N * 2位数进行数学计算。你不会有任何乘法溢出,你仍然有零除以担心。
为了通过实验证明这一点,尝试a,b,c的0到15之间的所有组合,但是用8位变量而不是4进行数学运算(在每次除法之前检查除以零并将这些组合抛出)并且结果总是匹配。
那么"更好"吗?乘法和除法可以使用大量逻辑在单个时钟中实现,或者使用指数级更少的多个时钟实现,尽管您的处理器文档说它是单个时钟它可能仍然是多个,因为有管道并且它们可以隐藏2或4个循环进入一些管道并节省大量芯片的房地产。或者某些处理器根本没有划分以节省空间。一些来自arm的cortex-m内核可以编译为单个时钟或多个时钟分频节省空间,只有当有人进行乘法运算时才会受到影响(谁会在代码中乘以多个???或者分割???)。当你做这样的事情时,你会看到
1 | x = x / 5; |
取决于目标和编译器以及优化设置,这些设置可以/将被实现为x = x *(1/5)加上一些其他动作以使其工作。
1 2 3 4 5 6 7 8 9 10 11 12 | unsigned int fun ( unsigned int x ) { return(x/5); } 0000000000000000 <fun>: 0: 89 f8 mov %edi,%eax 2: ba cd cc cc cc mov $0xcccccccd,%edx 7: f7 e2 mul %edx 9: 89 d0 mov %edx,%eax b: c1 e8 02 shr $0x2,%eax e: c3 retq |
在该目标上可以使用除法以及乘法,但乘法被认为更好,可能是因为时钟,也许是其他。
所以你可能希望考虑到这一点。
如果做a / b / c你必须检查两次除以零,但是如果做一个/(b + c)你只需要检查一次。对于每个alu指令的1或接近1个时钟数,检查除以零比数学本身更昂贵。因此,乘法理想情况下表现更好,但也有可能的例外情况。
您可以使用带符号的数字重复所有这些操作。同样的道理也是如此。如果它适用于4位,它将适用于8和16以及32和64和128等等......
1 2 3 | 7 * 7 = 0x31 -8 * -8 = 0x40 7 * -8 = 0xC8 |
这应该涵盖极端,所以如果你使用两倍于最坏情况的位数,你就不会溢出。在每次除法之前,您仍然需要检查除以零,因此乘法解决方案只会导致一次检查为零。如果你需要的位数加倍,那么你就不必检查乘法是否有溢出。
这里没有魔法,这是基本的数学解决方案。什么时候我会溢出,使用铅笔和纸张而没有编程语言(或者我所做的计算器使它更快)你可以看到什么时候。您还可以使用更多的小学数学。 N位的b的msbit是b [n-1] * 2 ^(n-1)对吗?对于4位数,无符号,msbit为0x8,即1 * 2 ^(4-1);对于b的其余部分(b [3] * 2 ^ 3)+(b [2] * 2 ^ 2)+(b [1] * 2 ^ 1)+(b [0] * 2 ^ 0);对于C来说也是如此,所以当我们使用小学数学将它们相乘时,我们从(b [3] c [3])(2 ^(3 + 3))开始,如果你坐下来工作,你的最坏情况不能超过2 ^ 8。也可以这样看待它:
1 2 3 4 5 6 7 8 9 | abcd * 1111 ========= abcd abcd abcd + abcd ========= xxxxxxx |
7位加上进位的可能性使其总共为8位。所有简单的小学数学,看看潜在的问题是什么。
实验将显示没有失败的位模式(除以零不计数对a / b / c = a /(b * c)也不起作用)。 John Colemans回答从另一个角度来看它可能有助于感觉所有位模式都能正常工作。虽然这都是正数。只要你检查所有溢出,这也适用于负数。
好。