连续的整数除法可以用乘法来代替吗?

3
在有理数的小学数学中,表达式(a / b) / c通过基本的代数运算等价于a / (b * c)
/是像C和大多数其他语言中截断整数除法时,是否也是如此呢?也就是说,我能否通过所有除数的乘积来替换一系列除法以进行单个除法?
您可以假设乘法不会溢出(如果溢出,则显然它们不等效)。

2
编程语言不会改变数学规则。 - old_timer
1
@老程序员 - 我不确定您的意思。数学规则取决于操作的语义。我在学校学习的规则通常适用于实数或有理数,而不适用于“截断除法”-因此并不完全清楚哪些适用(特别是像a / b * b == a这样的规则肯定不适用)。所以,如果您愿意,您可以将此问题解释为关于C中实现的某种特定类型的数学问题。还要考虑到大多数“数学”规则根本不适用于浮点数学(我在这里没有提问)。 - BeeOnRope
1
100 / 2 / 5 = 10。100 / (2*5) = 10;只要没有分数,在纸上和编程语言中都是正确的。在数学中唯一的问题是分数,如果限制为整数,但是除法仍然有效,直到你插入数字。 - old_timer
1
“显然,它是等效的”(https://ideone.com/HQk2sQ)... 但不要把这个评论当作“真理”接受。请参阅约翰·科尔曼的答案。 - pmg
1
@chqrlie - 关于INT_MIN-1的好观点,尽管在这种情况下UB是在“正确的方向”上 - 也就是说,转换后的版本是定义的,而原始版本不是。因此,至少在这个问题上,转换是“安全”的:毕竟,如果原始形式是UB,你不能真正期望转换后的版本有什么特别之处(如果你对反向转换感兴趣,那当然是一个问题)。 - BeeOnRope
显示剩余13条评论
3个回答

6
答案是“是的”(至少对于非负整数)。这是由于 除法算法,该算法指出对于任何正整数 a,d,我们都有 a = dq+r,其中唯一的非负整数 q,r 满足 0 <= q <= d-1。在这种情况下,q 在整数除法中就是 a/d
在使用整数除法的 a/b/c 中,我们可以将其看作两个步骤:
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

但是

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)

谢谢,这正是我寻找的证明。对于负数,假设是向0或者向负无穷方向舍入一致,是否也有类似的情况呢? - BeeOnRope

0

是的,在整数上。有人已经发布了一个(并删除了?)如何在浮点数上可能不起作用的示例。(尽管它可能足够接近你的应用场景。)

@JohnColeman提出了理论论证,但这里有一个实验论证。如果您运行此代码:

#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\n", a, b);
                    exit(1);
                }
            }
        }
    }
    return 0;
}

它尝试两种方式进行除法计算,并在它们不同时报告错误。以下方式运行不会报告错误:

  • div1,div2从-5到5,分子从-INT_MAX/100到INT_MAX/100
  • div1,div2从-2000到2000,分子从-2000到2000

因此,我敢打赌这将适用于任何整数。(再次假设div1*div2不会溢出。)


2
“我敢打赌这对于任何整数都有效”几乎不足以作为证明。 - chqrlie
我也使用了“试一些数字”的方法,既在脑海中,也用上述程序这样的程序。问题是,除非你可以彻底地做到它,否则我不认为它是“明确的”。如果能将其缩减到每个检查1纳秒,那么对于16位数字来说,彻底做到是“可能的”(需要几天时间搜索48位空间),但即使是32位数字也不行。此外,我猜C语言对于除法有着实现定义的行为,所以你可能会在你的机器上运行成功,但并不总是按照标准。 - BeeOnRope

-2

从实验的角度来看,可以更好地理解如何应用数学。

软件不会改变数学、恒等式或其他操作,以简化或改变使用变量的方程。软件执行所要求的操作。除非溢出,否则定点运算可以完美执行这些操作。

从数学而非软件中我们知道,为了使其有效,b 或 c 都不能为零。软件添加的是 b*c 也不能为零。如果溢出,则结果是错误的。

a / b / c = (a/b) * (1/c) = (a*1) / (b*c) = a / (b*c) 这是小学的内容,您可以在编程语言中实现 a/b/c 或 a/(b*c):这是您的选择。大多数情况下,如果您坚持使用整数,则由于无法表示分数,结果是不正确的。如果使用浮点数,由于同样的原因(无法存储无限大或无限小的数字),结果也往往是不正确的。那么您在哪里遇到这些限制呢?通过一个简单的实验,您就可以开始看到这一点。

编写一个程序,遍历0到15之间a、b和c的所有可能组合,计算a/b/c和a/(b*c),并进行比较。由于这些是四位数字,记得中间值也不能超过4位,如果想要查看编程语言对此的处理结果。仅打印除以零的情况和不匹配的情况。
立即您会发现,允许任何一个值为零都会导致非常无聊的实验,或者你会得到很多除以零的情况,或者0/不为零的数也不是一个有趣的结果。
 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  2  8 : divide by zero

至少对于a=1,b=1。c=1给出1,其余的结果为0。

2*8 = 16或0x10,位数太多导致溢出,结果为0x0,除以零,因此必须寻找无论是浮点数还是定点数。

 1  3 11 :  1  0

第一个有趣的:

1 / (3*11) = 1 / 0x21 which means 1/1 = 1;
1 / 3 = 0, 0 / 11 = 0.
so they don't match.  3*11 overflowed.

而这个继续进行。

那么使 ra 成为一个更大的数字可能会使它更有趣吗?一个小的 a 变量大部分时间都会导致结果为 0。

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;

15  3 10 :  1  0
15  3 11 : 15  0

两种情况都溢出了,不是很有趣。

所以,修改您的程序,仅显示结果不匹配但b*c也没有溢出的情况...没有输出。使用4位值与8位值或128位值之间没有魔法或区别...它只允许您拥有更多可能有效的结果。

0xF * 0xF = 0xE1,您可以轻松地在二进制中进行长乘法运算,最坏情况下需要2*N位来存储结果,以覆盖所有可能的N位值。因此,对于分母为N/2位数的N位分子进行反向除法,可以涵盖每个固定点值的N位结果。0xFFFF / 0xFF = 0x101。0xFFFF / 0x01 = 0xFFFF。

因此,如果您想要执行此数学运算,并且可以确保没有任何数字超过N位,则如果使用N*2位数进行数学运算,则不会有任何乘法溢出,但仍然需要担心除以零的问题。

为了在实验中演示这一点,尝试使用从0到15的所有a、b、c组合进行计算,但要使用8位变量进行数学运算而不是4位(在每次除法之前检查是否存在除以零的情况,并将这些组合扔掉),结果总是匹配的。
那么有没有更好的方法呢?乘法和除法都可以使用大量逻辑在单个时钟周期内实现,也可以使用指数级别少的多个时钟周期来实现,尽管处理器文档可能说它是单个时钟周期,但它可能仍然是多个时钟周期,因为有流水线,它们可以将2或4个时钟周期隐藏在某些管道中并节省大量芯片空间。或者有些处理器根本不执行除法以节省空间。来自ARM的一些Cortex-M内核可以编译为单个时钟周期或多个时钟周期的除法,只会在执行乘法(谁在他们的代码中执行乘法?或除法?)时带来一定的损失。当你做这些事情的时候,你会看到。
x = x / 5;

根据目标、编译器和优化设置,可以/将会实现 x = x * (1/5) 加上一些其他的操作来使其工作。

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,您必须检查两次除以零,但如果进行a /(b + c),您只需要检查一次。对于每个ALU指令1或近似1的时钟数,检查除以零的成本比数学本身更高。因此,乘法表现理想,但也有可能有例外。

您可以使用带符号的数字重复所有内容。同样适用。如果适用于4位,则适用于8位、16位、32位、64位、128位等等...

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。也可以这样看:

     abcd
*    1111
=========
     abcd
    abcd
   abcd
+ abcd
=========
  xxxxxxx

7位加上可能的进位,总共8位。所有简单的数学计算都可以看出潜在的问题。

实验将显示没有失败的位模式(除以零不算,对于a/b/c = a/(b*c)也不适用)。约翰·科尔曼(John Coleman)的答案从另一个角度来看可能有助于感觉所有位模式都能工作。虽然那些都是正数。这同样适用于负数,只要您检查所有溢出即可。


请注意,如果您想进行四舍五入,则非常容易,因为基数为2,您只需要将数字的大小加倍 (((a<<1)/(b*c)) +1)>>1; 但现在您又必须再次注意溢出...或者不需要吗? - old_timer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接