关于C#:连续截断整数除法可以用乘法替换吗?

Can consecutive truncating integer divisions be replaced with a multiplication?

在具有有理数的小学数学中,表达式(a / b) / c通过基本代数操作等效于a / (b * c)

/在C和大多数其他语言中截断整数除法时是否相同? 也就是说,我可以用所有除数的乘积用一个除法代替一系列的除法吗?

你可以假设乘法不会溢出(如果是的话,显然它们不是等价的)。


答案是"是"(至少对于非负整数)。这是从除法算法得出的,该算法表明对于任何正整数a,d,对于具有0 <= q <= d-1的唯一非负整数q,r,我们有a = dq+r。在这种情况下,q在整数除法中是a/d

a/b/c(带整数除法)中,我们可以分两步考虑它:

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

注意0 <= b*r_2 + r_1 <= b*(c-1) + b-1 = bc - 1

由此得出q_2 a/(b*c)。因此a/b/c = a/(b*c)


是的,关于整数。有人已经发布(并删除了?)一个如何在浮点上工作的示例。 (虽然它可能足够接近您的应用程序。)

@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回答从另一个角度来看它可能有助于感觉所有位模式都能正常工作。虽然这都是正数。只要你检查所有溢出,这也适用于负数。

好。