(a / b) / c
通过基本的代数运算等价于a / (b * c)
。当
/
是像C和大多数其他语言中截断整数除法时,是否也是如此呢?也就是说,我能否通过所有除数的乘积来替换一系列除法以进行单个除法?您可以假设乘法不会溢出(如果溢出,则显然它们不等效)。
(a / b) / c
通过基本的代数运算等价于a / (b * c)
。/
是像C和大多数其他语言中截断整数除法时,是否也是如此呢?也就是说,我能否通过所有除数的乘积来替换一系列除法以进行单个除法?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)
。是的,在整数上。有人已经发布了一个(并删除了?)如何在浮点数上可能不起作用的示例。(尽管它可能足够接近你的应用场景。)
@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不会溢出。)
从实验的角度来看,可以更好地理解如何应用数学。
软件不会改变数学、恒等式或其他操作,以简化或改变使用变量的方程。软件执行所要求的操作。除非溢出,否则定点运算可以完美执行这些操作。
从数学而非软件中我们知道,为了使其有效,b 或 c 都不能为零。软件添加的是 b*c 也不能为零。如果溢出,则结果是错误的。
a / b / c = (a/b) * (1/c) = (a*1) / (b*c) = a / (b*c)
这是小学的内容,您可以在编程语言中实现 a/b/c 或 a/(b*c):这是您的选择。大多数情况下,如果您坚持使用整数,则由于无法表示分数,结果是不正确的。如果使用浮点数,由于同样的原因(无法存储无限大或无限小的数字),结果也往往是不正确的。那么您在哪里遇到这些限制呢?通过一个简单的实验,您就可以开始看到这一点。
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位(在每次除法之前检查是否存在除以零的情况,并将这些组合扔掉),结果总是匹配的。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)的答案从另一个角度来看可能有助于感觉所有位模式都能工作。虽然那些都是正数。这同样适用于负数,只要您检查所有溢出即可。
a / b * b == a
这样的规则肯定不适用)。所以,如果您愿意,您可以将此问题解释为关于C中实现的某种特定类型的数学问题。还要考虑到大多数“数学”规则根本不适用于浮点数学(我在这里没有提问)。 - BeeOnRopeINT_MIN
和-1
的好观点,尽管在这种情况下UB是在“正确的方向”上 - 也就是说,转换后的版本是定义的,而原始版本不是。因此,至少在这个问题上,转换是“安全”的:毕竟,如果原始形式是UB,你不能真正期望转换后的版本有什么特别之处(如果你对反向转换感兴趣,那当然是一个问题)。 - BeeOnRope