在C中,为什么 signed int
比 unsigned int
快?是的,我知道这个网站已被多次询问和回答(链接如下) . 但是,大多数人说没有区别 . 我编写了代码并意外地发现了显着的性能差异 .
为什么我的代码的“未签名”版本比“签名”版本慢(即使在测试相同的数字时)? (我有一个x86-64英特尔处理器) .
Similar links
Compile Command: 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
签署了int版本
NOTE: 如果我在所有数字上明确声明 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
4 回答
您的问题确实很有趣,因为无符号版本始终产生的代码速度慢了10%到20% . 然而,代码中存在多个问题:
这两个函数都返回
2
,2
,3
,5
和7
,这是不正确的 .测试
if (i != num) return 0; else return 1;
完全没用,因为循环体仅为i < num
运行 . 这样的测试对于小型主要测试是有用的,但是特殊的套管它们并不真正有用 .无符号版本中的强制转换是多余的 .
产生文本输出到终端的
基准测试代码是不可靠的,您应该使用
clock()
函数来计算CPU密集型功能,而无需任何干预I / O.主要测试的算法完全没有效率,因为循环运行
num / 2
次而不是sqrt(num)
.让我们简化代码并运行一些精确的基准测试:
在OS / X上使用
clang -O2
编译的代码生成此输出:这些时间与OP在不同系统上观察到的行为一致,但显示了更高效的迭代测试所带来的显着改善:更快! 10000 times !
关于为什么函数使用unsigned更慢的问题?让我们看一下生成的代码(gcc 7.2 -O2):
内部循环非常相似,指令数量相同,类似指令 . 然而,这里有一些可能的解释:
cltd
将eax
寄存器的符号扩展到edx
寄存器,这可能导致指令延迟,因为eax
被紧接的前一条指令movl %edi, %eax
修改 . 然而,这会使签名版本比未签名版本慢,而不是更快 .对于无符号版本,循环的初始指令可能未对齐,但不太可能,因为更改源代码中的顺序对时序没有影响 .
尽管有符号和无符号除法运算符的寄存器内容相同,但
idivl
指令可能比divl
指令占用的周期更少 . 实际上,带符号的除法运算精度比无符号除法的精度低一点,但这种微小变化的差异似乎很大 .我怀疑在
idivl
的硅实现中投入了更多的努力,因为签名的划分比无符号划分更常见(通过英特尔多年的编码统计来衡量) .由rcgldr评论,查看英特尔过程的指令表,对于Ivy Bridge,DIV 32位需要10个微操作,19到27个周期,IDIV 9微操作,19到26个周期 . 基准时间与这些时间一致 . 额外的微操作可能是由于DIV(64/32位)中的操作数较长而不是IDIV(63/31位) .
这个令人惊讶的结果应该教给我们一些教训:
优化是一项艰难的艺术,谦虚和拖延 .
正确性经常被优化打破 .
选择一个更好的算法远远超过优化 .
总是基准代码,不要相信你的直觉 .
由于未定义有符号整数溢出,编译器可以对涉及有符号整数的代码进行大量假设和优化 . 无符号整数溢出被定义为环绕,因此编译器将无法进行优化 . 另见http://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html#signed_overflow和http://www.airs.com/blog/archives/120 .
从Instruction specification on AMD/Intel我们(对于K7):
对于i7,
IDIVL
和DIVL
的延迟和吞吐量相同,μops存在细微差别 .这可以解释差异,因为-O3汇编代码仅相差在我的机器上签名(DIVL vs IDIVL) .
替代维基候选测试可能/可能不会显示显着的时差 .
样本输出