许多编程语言,包括 C 和 C++ 的旧标准,都保证了除法规则,即
a = b * (a / b) + a % b
即使 a/b 和 a%b 的确切值未定义。[0] 这可以被利用来计算在许多语言和平台中使用(等价于)的所需结果以下代码:
int divF(int a, int b) { return a / b - (a % b < 0); }
这是来自@TomasPetricek 答案的版本。但是,它仅适用于b > 0!
以下代码适用于任何b != 0:[1]
int sign(int x) { return (x > 0) - (x < 0); }
int divF2(int a, int b) { return a / b - (sign(a % b) == -sign(b)); }
但是,四舍五入的除法(又名地板除法,又名Knuth 的除法)并不总是可取的。有人认为[2] 欧几里得除法是最常用的除法。 b > 0 向下取整,b < 0 向上取整。它具有很好的属性,即对于所有a 和b,兼容定义的余数的值始终为非负,与它们的符号无关。此外,它与二进制补码机器上的位移和掩码相一致,用于二次除数的幂。是的,计算起来也更快:
int divE(int a, int b) {
int c = a % b < 0;
return a / b + (b < 0 ? c : -c);
}
所有三个版本都在 amd64 上使用 clang -O3 生成无分支代码。但是,在二进制补码架构上,以下可能会稍微快一些:
int divE2(int a, int b) { return a / b + (-(a % b < 0) & (b < 0 ? 1 : -1)); }
生成的代码
divF:
cdq
idiv esi
sar edx, 31
add eax, edx
divF2:
cdq
idiv esi
xor ecx, ecx
test edx, edx
setg cl
sar edx, 31
add edx, ecx
xor ecx, ecx
test esi, esi
setg cl
shr esi, 31
sub esi, ecx
xor ecx, ecx
cmp edx, esi
sete cl
sub eax, ecx
Chazz:
cdq
idiv esi
test edx, edx
cmove edi, esi
xor edi, esi
sar edi, 31
add eax, edi
divE:
cdq
idiv esi
mov ecx, edx
shr ecx, 31
sar edx, 31
test esi, esi
cmovs edx, ecx
add eax, edx
divE2:
cdq
idiv esi
sar edx, 31
shr esi, 31
lea ecx, [rsi + rsi]
add ecx, -1
and ecx, edx
add eax, ecx
基准
simple truncating division:
2464805950: 1.90 ns -- base
euclidean division:
2464831322: 2.13 ns -- divE
2464831322: 2.13 ns -- divE2
round to -inf for all b:
1965111352: 2.58 ns -- Chazz
1965111352: 2.64 ns -- divF2
1965111352: 5.02 ns -- Warty
round to -inf for b > 0, broken for b < 0:
1965143330: 2.13 ns -- ben135
1965143330: 2.13 ns -- divF
1965143330: 2.13 ns -- Tomas
4112595000: 5.79 ns -- runevision
round to -inf, broken for b < 0 or some edge-cases:
4112315315: 2.24 ns -- DrAltan
1965115133: 2.45 ns -- Cam
1965111351: 7.76 ns -- LegsDrivenCat
在 FreeBSD 12.2 上使用 clang -O3 编译,i7-8700K CPU @ 3.70GHz。第一列是产生相同结果的校验和分组算法。 base 是用于衡量测试开销的最简单的截断除法。
试验台:
static const int N = 1000000000;
int rng[N][2], nrng;
void push(int a, int b) { rng[nrng][0] = a, rng[nrng][1] = b, ++nrng; }
SN_NOINLINE void test_case(int (*func)(), const char *name) {
struct timespec t0, t1;
clock_gettime(CLOCK_PROF, &t0);
int sum = func();
clock_gettime(CLOCK_PROF, &t1);
double elapsed = (t1.tv_sec - t0.tv_sec)*1.e9 + (t1.tv_nsec - t0.tv_nsec);
printf("%10u: %5.2f ns -- %s\n", sum, elapsed/N, name);
}
#define ALL_TESTS(X) X(base) X(divF) X(divF2) X(divE) X(divE2) X(ben135) X(DrAltan) X(Cam) X(Tomas) X(Warty) X(Chazz) X(runevision) X(LegsDrivenCat)
#define LOOP_TEST(X) \
SN_NOINLINE int loop_##X() { \
int sum = 0; \
for(int i = 0; i < N; ++i) sum += X(rng[i][0], rng[i][1]); \
return sum; \
} \
/**/
ALL_TESTS(LOOP_TEST)
int main() {
srandom(6283185);
push(INT_MAX, 1); push(INT_MAX, -1); push(INT_MIN, 1);
push(INT_MAX, 2); push(INT_MAX, -2); push(INT_MIN, 2); push(INT_MIN, -2);
while(nrng < N) {
int a = random() - 0x40000000, b;
do b = (random() >> 16) - 0x4000; while(b == 0);
push(a,b);
}
#define CALL_TEST(X) test_case(loop_##X, #X);
ALL_TESTS(CALL_TEST)
}
脚注/参考文献
- 如今,它们在 C 和 C++ 中定义为通过截断舍入。所以负数向上舍入,正数向下舍入。这就是硬件除数所做的。这很糟糕,因为它是最没用的四舍五入规则。
- Daan Leijen. Division and Modulus for Computer Scientists. December 2001.
- Raymond T. Boute. The Euclidean definition of the functions div and mod. April 1992.