在C中,为什么"签署int"快于#34; unsigned int"?

时间:2015-12-08 20:09:41

标签: c performance optimization unsigned signed

在C中,为什么signed intunsigned int更快?是的,我知道这个网站已被多次询问和回答(链接如下)。但是,大多数人说没有区别。我编写了代码并意外地发现了显着的性能差异。

为什么"未签名"我的代码版本比" signed"慢。版本(即使测试相同的数字)? (我有一个x86-64英特尔处理器)。

类似链接

编译命令: 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

4 个答案:

答案 0 :(得分:13)

你的问题真的很吸引人,因为无符号版本始终会产生慢10到20%的代码。然而,代码中存在多个问题:

  • 这两个函数都返回0235的{​​{1}},这是不正确的。
  • 测试7完全没用,因为循环体仅针对if (i != num) return 0; else return 1;运行。这样的测试对于小型主要测试是有用的,但特殊的外壳并不是真的有用。
  • 无符号版本的演员表是多余的。
  • 为终端生成文本输出的基准测试代码是不可靠的,您应该使用i < num函数来计算CPU密集型函数,而无需任何干预I / O.
  • 主要测试的算法完全没有效率,因为循环运行clock()次而不是num / 2

让我们简化代码并运行一些精确的基准测试:

sqrt(num)

在OS / X上使用#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; } 编译的代码生成此输出:

clang -O2

这些时间与OP在不同系统上观察到的行为一致,但显示了更高效的迭代测试带来的显着改善: 10000次更快!

关于问题为什么函数使用unsigned更慢?,让我们看看生成的代码(gcc 7.2 -O2):

         isprime_slow:  788.004ms
unsigned_isprime_slow:  965.381ms
         isprime_fast:    0.065ms
unsigned_isprime_fast:    0.089ms

内部循环非常相似,指令数量相同,类似指令。然而,这里有一些可能的解释:

  • 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 1}}。然而,这会使签名版本比未签名版本慢,而不是更快。
  • 循环&#39;初始指令可能未对齐未签名版本,但不太可能因为更改源代码中的顺序对时间没有影响。
  • 尽管有符号和无符号除法运算符的寄存器内容相同,但idivl指令可能比divl指令采用更少的周期。实际上,有符号的除法运算精度比无符号除法的精度低一点,但这种微小变化的差异似乎很大。
  • 我怀疑在idivl的芯片实现中投入了更多的努力,因为签名的划分比无符号划分更常见(根据英特尔多年的编码统计数据来衡量)。
  • 由rcgldr评论,查看英特尔工艺的指令表,对于Ivy Bridge,DIV 32位需要10个微操作,19到27个周期,IDIV 9微操作,19到26个周期。基准时间与这些时间一致。额外的微操作可能是由于DIV(64/32位)中的操作数较长而不是IDIV(63/31位)。

这个令人惊讶的结果应该教给我们一些教训:

  • 优化是一项艰难的艺术,是谦虚和拖延。
  • 正确性经常被优化打破。
  • 选择更好的算法远远超过优化。
  • 总是基准代码,不要相信你的直觉。

答案 1 :(得分:5)

由于未定义有符号整数溢出,编译器可以对涉及有符号整数的代码进行大量假设和优化。无符号整数溢出被定义为包围,因此编译器无法进行优化。另请参阅https://jsfiddle.net/52axLsfn/4/http://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html#signed_overflow

答案 2 :(得分:1)

来自Instruction specification on AMD/Intel我们(对于K7):

Instruction Ops Latency Throughput
DIV r32/m32 32  24      23
IDIV r32    81  41      41
IDIV m32    89  41      41 

对于i7,IDIVLDIVL的延迟和吞吐量相同,μops存在细微差别。

这可以解释差异,因为-O3汇编代码仅因我的机器上的签名(DIVL与IDIVL)而不同。

答案 3 :(得分:0)

替代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