Cでは、なぜsigned int
がunsigned int
より速いのですか?確かに、私はこれがこのWebサイトで何度も尋ねられ、答えられたことを知っています(以下のリンク)。しかし、ほとんどの人は違いはないと言いました。コードを書いて、誤って大きなパフォーマンスの違いを見つけました。
コードの「署名なし」バージョンが「署名済み」バージョンよりも遅いのはなぜですか(同じ数をテストする場合でも)? (私はx86-64 Intelプロセッサーを持っています)。
同様のリンク
コンパイルコマンド:gcc -Wall -Wextra -pedantic -O3 -Wl,-O3 -g0 -ggdb0 -s -fwhole-program -funroll-loops -pthread -pipe -ffunction-sections -fdata-sections -std=c11 -o ./test ./test.c && strip --strip-all --strip-unneeded --remove-section=.note --remove-section=.comment ./test
signed int
バージョン注:すべての数値に対してsigned int
を明示的に宣言しても違いはありません。
int isprime(int num) {
// Test if a signed int is prime
int i;
if (num % 2 == 0 || num % 3 == 0)
return 0;
else if (num % 5 == 0 || num % 7 == 0)
return 0;
else {
for (i = 11; i < num; i += 2) {
if (num % i == 0) {
if (i != num)
return 0;
else
return 1;
}
}
}
return 1;
}
unsigned int
バージョンint isunsignedprime(unsigned int num) {
// Test if an unsigned int is prime
unsigned int i;
if (num % (unsigned int)2 == (unsigned int)0 || num % (unsigned int)3 == (unsigned int)0)
return 0;
else if (num % (unsigned int)5 == (unsigned int)0 || num % (unsigned int)7 == (unsigned int)0)
return 0;
else {
for (i = (unsigned int)11; i < num; i += (unsigned int)2) {
if (num % i == (unsigned int)0) {
if (i != num)
return 0;
else
return 1;
}
}
}
return 1;
}
以下のコードを使用して、これをファイルでテストします。
int main(void) {
printf("%d\n", isprime(294967291));
printf("%d\n", isprime(294367293));
printf("%d\n", isprime(294967293));
printf("%d\n", isprime(294967241)); // slow
printf("%d\n", isprime(294967251));
printf("%d\n", isprime(294965291));
printf("%d\n", isprime(294966291));
printf("%d\n", isprime(294963293));
printf("%d\n", isprime(294927293));
printf("%d\n", isprime(294961293));
printf("%d\n", isprime(294917293));
printf("%d\n", isprime(294167293));
printf("%d\n", isprime(294267293));
printf("%d\n", isprime(294367293)); // slow
printf("%d\n", isprime(294467293));
return 0;
}
結果(time ./test
):
Signed - real 0m0.949s
Unsigned - real 0m1.174s
署名されていないバージョンは常に10〜20%遅いコードを生成するので、あなたの質問は本当に興味深いものです。しかし、コードには複数の問題があります。
0
_、_2
_、_3
_および_5
_に対して_7
_を返しますが、これは正しくありません。i < num
_に対してのみ実行されるため、テストif (i != num) return 0; else return 1;
は完全に役に立ちません。そのようなテストは小さな素数テストには役立ちますが、それらを特別にケーシングすることは実際には役に立ちません。clock()
関数を使用して、I/Oを介在させずにCPU集中関数の時間を測定する必要があります。sqrt(num)
ではなく_num / 2
_回実行されるため、まったく非効率的です。コードを簡略化して、いくつかの正確なベンチマークを実行してみましょう。
_#include <stdio.h>
#include <time.h>
int isprime_slow(int num) {
if (num % 2 == 0)
return num == 2;
for (int i = 3; i < num; i += 2) {
if (num % i == 0)
return 0;
}
return 1;
}
int unsigned_isprime_slow(unsigned int num) {
if (num % 2 == 0)
return num == 2;
for (unsigned int i = 3; i < num; i += 2) {
if (num % i == 0)
return 0;
}
return 1;
}
int isprime_fast(int num) {
if (num % 2 == 0)
return num == 2;
for (int i = 3; i * i <= num; i += 2) {
if (num % i == 0)
return 0;
}
return 1;
}
int unsigned_isprime_fast(unsigned int num) {
if (num % 2 == 0)
return num == 2;
for (unsigned int i = 3; i * i <= num; i += 2) {
if (num % i == 0)
return 0;
}
return 1;
}
int main(void) {
int a[] = {
294967291, 0, 294367293, 0, 294967293, 0, 294967241, 1, 294967251, 0,
294965291, 0, 294966291, 0, 294963293, 0, 294927293, 1, 294961293, 0,
294917293, 0, 294167293, 0, 294267293, 0, 294367293, 0, 294467293, 0,
};
struct testcase { int (*fun)(); const char *name; int t; } test[] = {
{ isprime_slow, "isprime_slow", 0 },
{ unsigned_isprime_slow, "unsigned_isprime_slow", 0 },
{ isprime_fast, "isprime_fast", 0 },
{ unsigned_isprime_fast, "unsigned_isprime_fast", 0 },
};
for (int n = 0; n < 4; n++) {
clock_t t = clock();
for (int i = 0; i < 30; i += 2) {
if (test[n].fun(a[i]) != a[i + 1]) {
printf("%s(%d) != %d\n", test[n].name, a[i], a[i + 1]);
}
}
test[n].t = clock() - t;
}
for (int n = 0; n < 4; n++) {
printf("%21s: %4d.%03dms\n", test[n].name, test[n].t / 1000), test[n].t % 1000);
}
return 0;
}
_
OS/Xで_clang -O2
_を使用してコンパイルされたコードは、次の出力を生成します。
_ isprime_slow: 788.004ms
unsigned_isprime_slow: 965.381ms
isprime_fast: 0.065ms
unsigned_isprime_fast: 0.089ms
_
これらのタイミングは、別のシステムでのOPの観察された動作と一致していますが、より効率的な反復テストによって劇的な改善が見られます:10000倍より速く!
質問について、なぜ符号なしで関数が遅いのですか?、生成されたコード( gcc 7.2 -O2 )を見てみましょう。
_isprime_slow(int):
...
.L5:
movl %edi, %eax
cltd
idivl %ecx
testl %edx, %edx
je .L1
.L4:
addl $2, %ecx
cmpl %esi, %ecx
jne .L5
.L6:
movl $1, %edx
.L1:
movl %edx, %eax
ret
unsigned_isprime_slow(unsigned int):
...
.L19:
xorl %edx, %edx
movl %edi, %eax
divl %ecx
testl %edx, %edx
je .L22
.L18:
addl $2, %ecx
cmpl %esi, %ecx
jne .L19
.L20:
movl $1, %eax
ret
...
.L22:
xorl %eax, %eax
ret
_
内部ループは非常によく似ており、同じ数の命令、同じような命令です。ただし、いくつかの潜在的な説明は次のとおりです。
cltd
はeax
レジスタの符号をedx
レジスタに拡張します。これは、eax
が直前の命令によって変更されるため、命令遅延を引き起こす可能性があります_movl %edi, %eax
_。しかし、これは署名されたバージョンを署名されていないバージョンより遅くし、速くはしません。idivl
命令はdivl
命令よりもサイクル数が少ない可能性があります。実際、符号付き除算は、符号なし除算よりも1ビット少ない精度で動作しますが、この小さな変更の場合、差は非常に大きくなります。idivl
のシリコン実装により多くの労力が費やされたと思います(Intelでの長年のコーディング統計により測定)。この驚くべき結果から、いくつかの教訓が得られます。
符号付き整数のオーバーフローは定義されていないため、コンパイラーは符号付き整数を含むコードに対して多くの仮定と最適化を行うことができます。符号なし整数オーバーフローはラップアラウンドするように定義されているため、コンパイラーはそれほど最適化できません。 http://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html#signed_overflow および http://www.airsも参照してください。 .com/blog/archives/12 。
AMD/Intelの命令仕様 から(K7の場合):
Instruction Ops Latency Throughput
DIV r32/m32 32 24 23
IDIV r32 81 41 41
IDIV m32 89 41 41
I7の場合、レイテンシとスループットはIDIVL
とDIVL
で同じですが、µopにはわずかな違いがあります。
-O3アセンブリコードは私のマシンの署名(DIVLとIDIVL)によってのみ異なるので、これは違いを説明するかもしれません。
有意な時間差を示す場合と示さない場合がある代替のWiki候補テスト。
#include <stdio.h>
#include <time.h>
#define J 10
#define I 5
int main(void) {
clock_t c1,c2,c3;
for (int j=0; j<J; j++) {
c1 = clock();
for (int i=0; i<I; i++) {
isprime(294967241);
isprime(294367293);
}
c2 = clock();
for (int i=0; i<I; i++) {
isunsignedprime(294967241);
isunsignedprime(294367293);
}
c3 = clock();
printf("%d %d %d\n", (int)(c2-c1), (int)(c3-c2), (int)((c3-c2) - (c2-c1)));
fflush(stdout);
}
return 0;
}
出力例
2761 2746 -15
2777 2777 0
2761 2745 -16
2793 2808 15
2792 2730 -62
2746 2730 -16
2746 2730 -16
2776 2793 17
2823 2808 -15
2793 2823 30