【发布时间】:2010-11-04 23:43:54
【问题描述】:
地板除法是当结果总是向下(朝向-∞)而不是朝向0时:
是否可以在 C/C++ 中有效地实现整数除法或欧式整数除法?
(显而易见的解决方案是检查被除数的符号)
【问题讨论】:
标签: c++ c math division integer-division
地板除法是当结果总是向下(朝向-∞)而不是朝向0时:
是否可以在 C/C++ 中有效地实现整数除法或欧式整数除法?
(显而易见的解决方案是检查被除数的符号)
【问题讨论】:
标签: c++ c math division integer-division
我编写了一个测试程序来对这里提出的想法进行基准测试:
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <windows.h>
#define N 10000000
#define M 100
int dividends[N], divisors[N], results[N];
__forceinline int floordiv_signcheck(int a, int b)
{
return (a<0 ? a-(b-1) : a) / b;
}
__forceinline int floordiv_signcheck2(int a, int b)
{
return (a - (a<0 ? b-1 : 0)) / b;
}
__forceinline int floordiv_signmultiply(int a, int b)
{
return (a + (a>>(sizeof(a)*8-1))*(b-1)) / b;
}
__forceinline int floordiv_floatingpoint(int a, int b)
{
// I imagine that the call to floor can be replaced to a cast
// if you can get FPU rounding control to work (I couldn't).
return floor((double)a / b);
}
void main()
{
for (int i=0; i<N; i++)
{
dividends[i] = rand();
do
divisors[i] = rand();
while (divisors[i]==0);
}
LARGE_INTEGER t0, t1;
QueryPerformanceCounter(&t0);
for (int j=0; j<M; j++)
for (int i=0; i<N; i++)
results[i] = floordiv_signcheck(dividends[i], divisors[i]);
QueryPerformanceCounter(&t1);
printf("signcheck : %9llu\n", t1.QuadPart-t0.QuadPart);
QueryPerformanceCounter(&t0);
for (int j=0; j<M; j++)
for (int i=0; i<N; i++)
results[i] = floordiv_signcheck2(dividends[i], divisors[i]);
QueryPerformanceCounter(&t1);
printf("signcheck2 : %9llu\n", t1.QuadPart-t0.QuadPart);
QueryPerformanceCounter(&t0);
for (int j=0; j<M; j++)
for (int i=0; i<N; i++)
results[i] = floordiv_signmultiply(dividends[i], divisors[i]);
QueryPerformanceCounter(&t1);
printf("signmultiply : %9llu\n", t1.QuadPart-t0.QuadPart);
QueryPerformanceCounter(&t0);
for (int j=0; j<M; j++)
for (int i=0; i<N; i++)
results[i] = floordiv_floatingpoint(dividends[i], divisors[i]);
QueryPerformanceCounter(&t1);
printf("floatingpoint: %9llu\n", t1.QuadPart-t0.QuadPart);
}
结果:
signcheck : 61458768
signcheck2 : 61284370
signmultiply : 61625076
floatingpoint: 287315364
所以,根据我的结果,检查标志是最快的:
(a - (a<0 ? b-1 : 0)) / b
【讨论】:
printf 用于上一个答案。 printf 有点慢,不知道是不是慢到影响你的结果。
double 而不是float,因为那里可能也会发生转化。
double 会使浮点运行得更快一些,但不会快很多。否则,结果并没有太大变化。
rand() 被指定为只返回正整数。
五年后我将重新审视这个问题,因为这对我也很重要。我对 x86-64 的两个纯 C 版本和两个内联汇编版本进行了一些性能测量,结果可能很有趣。
已测试的地板除法变体是:
CMOV 版本。以下是我的基准程序:
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#ifndef VARIANT
#define VARIANT 3
#endif
#if VARIANT == 0
#define floordiv(a, b) (((a) < 0)?((((a) + 1) / (b)) - 1):((a) / (b)))
#elif VARIANT == 1
#define floordiv(a, b) ((((a) < 0)?((a) - ((b) - 1)):(a)) / (b))
#elif VARIANT == 2
#define floordiv(a, b) ({ \
int result; \
asm("test %%eax, %%eax; jns 1f; sub %1, %%eax;" \
"add $1, %%eax; 1: cltd; idivl %1;" \
: "=a" (result) \
: "r" (b), \
"0" (a) \
: "rdx"); \
result;})
#elif VARIANT == 3
#define floordiv(a, b) ({ \
int result; \
asm("mov %%eax, %%edx; sub %1, %%edx; add $1, %%edx;" \
"test %%eax, %%eax; cmovs %%edx, %%eax; cltd;" \
"idivl %1;" \
: "=a" (result) \
: "r" (b), \
"0" (a) \
: "rdx"); \
result;})
#endif
double ntime(void)
{
struct timeval tv;
gettimeofday(&tv, NULL);
return(tv.tv_sec + (((double)tv.tv_usec) / 1000000.0));
}
void timediv(int n, int *p, int *q, int *r)
{
int i;
for(i = 0; i < n; i++)
r[i] = floordiv(p[i], q[i]);
}
int main(int argc, char **argv)
{
int n, i, *q, *p, *r;
double st;
n = 10000000;
p = malloc(sizeof(*p) * n);
q = malloc(sizeof(*q) * n);
r = malloc(sizeof(*r) * n);
for(i = 0; i < n; i++) {
p[i] = (rand() % 1000000) - 500000;
q[i] = (rand() % 1000000) + 1;
}
st = ntime();
for(i = 0; i < 100; i++)
timediv(n, p, q, r);
printf("%g\n", ntime() - st);
return(0);
}
我使用 GCC 4.9.2 使用 gcc -march=native -Ofast 编译了这个,结果在我的 Core i5-2400 上如下。每次运行的结果都具有相当的可重复性——至少它们总是以相同的顺序降落。
所以CMOV 的实现至少将其他人从水中吹了出来。令我惊讶的是,变体 2 以相当大的优势超越了它的纯 C 版本(变体 1)。我原以为编译器应该能够发出至少和我一样高效的代码。
这里有一些其他平台,供比较:
AMD 速龙 64 X2 4200+,GCC 4.7.2:
至强 E3-1271 v3,GCC 4.9.2:
作为最后一点,我或许应该警告不要过于认真地对待CMOV 版本的明显性能优势,因为在现实世界中,其他版本中的分支可能不会像在这个基准测试中那样完全随机,并且如果分支预测器可以做一个合理的工作,分支版本可能会变得更好。但是,实际情况在很大程度上取决于实际使用的数据,因此尝试进行任何通用基准测试可能毫无意义。
【讨论】:
/ 运算符通常会导致编译器生成额外的代码来创建接近零的不均匀性,这将使程序员有必要编写更多的额外代码。我认为最好的方法是转换为无符号,加一个偏移量,做一个除法,然后减去一个不同的偏移量,但这似乎有点恶心。
提出一些免费的分支来根据符号更正结果可能会更有效,因为分支很昂贵。
请参阅Hacker's Delight 中Chapter 2 的第 20 页,了解如何访问该标志。
【讨论】:
请注意:x86 sar 指令在涉及 2 的幂时执行地板除法。
【讨论】:
由于 IEEE-754 将向 -inf 的舍入指定为所需的舍入模式之一,我想您的问题的答案是肯定的。但是也许你可以解释一下,如果你想知道如果一个人正在编写编译器,你将如何实现这个过程,或者想知道如何使用一个特定的编译器来执行这个操作?
【讨论】:
是否可以在 C/C++ 中有效地实现下限或欧几里得整数除法?
是的。
(显而易见的解决方案是检查被除数的符号)
我完全同意,并且很难相信存在明显更快的替代方案。
【讨论】: