床 /ユークリッド整数部門を効率的に実装します
-
29-09-2019 - |
質問
床の除算は、結果が常に床に張られている場合(-∞に向かって)、0には向いていません。
C/C ++で床またはユークリッド整数部門またはユークリッド整数部門を効率的に実装することは可能ですか?
(明らかな解決策は、配当のサインを確認することです)
解決
5年後にこの質問を再検討しています。これは私にとっても関連しているからです。 X86-64の2つのPure-Cバージョンと2つのインラインアセンブリバージョンでいくつかのパフォーマンス測定を行いましたが、結果は興味深いかもしれません。
床の分割のテスト済みバリアントは次のとおりです。
- しばらくの間使用してきた実装。
- 上記のわずかなバリアントは、1つの部門のみを使用しているものです。
- 前のものですが、インラインアセンブリで手で実装されています。と
- a
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 -march=native -Ofast
GCC 4.9.2を使用し、私のコアi5-2400での結果は次のとおりでした。結果は、実行ごとにかなり再現可能です - 少なくとも、常に同じ順序で着陸します。
- バリアント0:7.21秒
- バリアント1:7.26秒
- バリアント2:6.73秒
- バリアント3:4.32秒
だから CMOV
少なくとも、実装は他の人を水から吹き飛ばします。私が驚いているのは、バリアント2が純粋なCバージョン(バリアント1)をかなり広いマージンでアウトすることです。コンパイラは、少なくとも私のものと同じくらい効率的なコードを放出できるはずだと思いました。
比較のために、他のいくつかのプラットフォームを次に示します。
AMD Athlon 64 X2 4200+、GCC 4.7.2:
- バリアント0:26.33秒
- バリアント1:25.38秒
- バリアント2:25.19秒
- バリアント3:22.39秒
Xeon E3-1271 V3、GCC 4.9.2:
- バリアント0:5.95秒
- バリアント1:5.62秒
- バリアント2:5.40秒
- バリアント3:3.44秒
最後のメモとして、私はおそらく、 CMOV
現実の世界では、他のバージョンのブランチはおそらくこのベンチマークほど完全にランダムではないため、ブランチ予測子が合理的な仕事をすることができれば、分岐バージョンの方が良くなる可能性があるためです。ただし、その現実は、実際に使用されているデータにかなり依存しているため、一般的なベンチマークを実行することはおそらく無意味です。
他のヒント
ここに示されているアイデアをベンチマークするためのテストプログラムを書きました。
#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
C/C ++で床またはユークリッド整数部門またはユークリッド整数部門を効率的に実装することは可能ですか?
はい。
(明らかな解決策は、配当のサインを確認することです)
私は完全に同意し、非常に速い代替案があると信じるのが難しいと思うでしょう。
ただメモ:x86 sar
命令は、2人の力に関しては床の分割を実行します。
IEEE -754は、必要な丸めモードの1つとして-INFに向かってラウンドを指定しているため、質問に対する答えはイエスだと思います。しかし、おそらく、コンパイラを書いている場合に手順をどのように実装するかを知りたいのか、特定のコンパイラを使用して操作を実行する方法を知ることができるかどうかを説明することができますか?