高效的整数比较功能

时间:2012-06-12 12:12:34

标签: c assembly x86 inline-assembly

compare函数是一个函数,它接受两个参数ab,并返回一个描述其顺序的整数。如果a小于b,则结果为负整数。如果a大于b,则结果为正整数。否则,ab相等,结果为零。

此功能通常用于参数化标准库中的排序和搜索算法。

为角色实现compare功能非常简单;你只需减去参数:

int compare_char(char a, char b)
{
    return a - b;
}

这是有效的,因为通常假设两个字符之间的差异适合整数。 (请注意,对于sizeof(char) == sizeof(int)。)

的系统,此假设不适用

这个技巧无法比较整数,因为两个整数之间的差异通常不适合整数。例如,INT_MAX - (-1) = INT_MIN表明INT_MAX小于-1(从技术上讲,溢出导致未定义的行为,但让我们假设模运算)。

那么我们如何才能有效地为整数实现比较函数呢?这是我的第一次尝试:

int compare_int(int a, int b)
{
    int temp;
    int result;
    __asm__ __volatile__ (
        "cmp %3, %2 \n\t"
        "mov $0, %1 \n\t"

        "mov $1, %0 \n\t"
        "cmovg %0, %1 \n\t"

        "mov $-1, %0 \n\t"
        "cmovl %0, %1 \n\t"
    : "=r"(temp), "=r"(result)
    : "r"(a), "r"(b)
    : "cc");
    return result;
}

可以在少于6条指令中完成吗?是否有一种更直接的方式更有效?

7 个答案:

答案 0 :(得分:95)

这个没有分支,并且没有溢出或下溢:

return (a > b) - (a < b);

使用gcc -O2 -S,这将归结为以下六条指令:

xorl    %eax, %eax
cmpl    %esi, %edi
setl    %dl
setg    %al
movzbl  %dl, %edx
subl    %edx, %eax

以下是一些用于对各种比较实现进行基准测试的代码:

#include <stdio.h>
#include <stdlib.h>

#define COUNT 1024
#define LOOPS 500
#define COMPARE compare2
#define USE_RAND 1

int arr[COUNT];

int compare1 (int a, int b)
{
    if (a < b) return -1;
    if (a > b) return 1;
    return 0;
}

int compare2 (int a, int b)
{
    return (a > b) - (a < b);
}

int compare3 (int a, int b)
{
    return (a < b) ? -1 : (a > b);
}

int compare4 (int a, int b)
{
    __asm__ __volatile__ (
        "sub %1, %0 \n\t"
        "jno 1f \n\t"
        "cmc \n\t"
        "rcr %0 \n\t"
        "1: "
    : "+r"(a)
    : "r"(b)
    : "cc");
    return a;
}

int main ()
{
    for (int i = 0; i < COUNT; i++) {
#if USE_RAND
        arr[i] = rand();
#else
        for (int b = 0; b < sizeof(arr[i]); b++) {
            *((unsigned char *)&arr[i] + b) = rand();
        }
#endif
    }

    int sum = 0;

    for (int l = 0; l < LOOPS; l++) {
        for (int i = 0; i < COUNT; i++) {
            for (int j = 0; j < COUNT; j++) {
                sum += COMPARE(arr[i], arr[j]);
            }
        }
    }

    printf("%d=0\n", sum);

    return 0;
}

我的64位系统上使用gcc -std=c99 -O2编译的结果为正整数(USE_RAND=1):

compare1: 0m1.118s
compare2: 0m0.756s
compare3: 0m1.101s
compare4: 0m0.561s

在C-only解决方案中,我建议的解决方案是最快的。尽管只编译了5条指令,user315052的解决方案仍然较慢。减速可能是因为,尽管只有少一条指令,但有条件指令(cmovge)。

总的来说,当使用正整数时,FredOverflow的4指令汇编实现速度最快。但是,此代码仅对整数范围RAND_MAX进行基准测试,因此4-instuction测试存在偏差,因为它分别处理溢出,并且这些在测试中不会发生;速度可能是由于分支预测成功所致。

对于全范围的整数(USE_RAND=0),4指令解决方案实际上非常慢(其他都相同):

compare4: 0m1.897s

答案 1 :(得分:53)

以下事实证明对我来说相当有效:

return (a < b) ? -1 : (a > b);

使用gcc -O2 -S,这将归结为以下五条指令:

xorl    %edx, %edx
cmpl    %esi, %edi
movl    $-1, %eax
setg    %dl
cmovge  %edx, %eax

作为Ambroz Bizjak's excellent companion answer的后续行动,我不相信他的程序测试了上面发布的相同汇编代码。而且,当我更仔细地研究编译器输出时,我注意到编译器没有生成与我们的任何一个答案中发布的相同的指令。所以,我拿了他的测试程序,手工修改了程序集输出以匹配我们发布的内容,并比较了结果的时间。 两个版本似乎大致相同。

./opt_cmp_branchless: 0m1.070s
./opt_cmp_branch:     0m1.037s

我正在发布每个程序的程序集,以便其他程序可以尝试相同的实验,并确认或反驳我的观察结果。

以下是带有cmovge指令((a < b) ? -1 : (a > b))的版本:

        .file   "cmp.c"
        .text
        .section        .rodata.str1.1,"aMS",@progbits,1
.LC0:
        .string "%d=0\n"
        .text
        .p2align 4,,15
.globl main
        .type   main, @function
main:
.LFB20:
        .cfi_startproc
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        pushq   %rbx
        .cfi_def_cfa_offset 24
        .cfi_offset 3, -24
        movl    $arr.2789, %ebx
        subq    $8, %rsp
        .cfi_def_cfa_offset 32
.L9:
        leaq    4(%rbx), %rbp
.L10:
        call    rand
        movb    %al, (%rbx)
        addq    $1, %rbx
        cmpq    %rbx, %rbp
        jne     .L10
        cmpq    $arr.2789+4096, %rbp
        jne     .L9
        xorl    %r8d, %r8d
        xorl    %esi, %esi
        orl     $-1, %edi
.L12:
        xorl    %ebp, %ebp
        .p2align 4,,10
        .p2align 3
.L18:
        movl    arr.2789(%rbp), %ecx
        xorl    %eax, %eax
        .p2align 4,,10
        .p2align 3
.L15:
        movl    arr.2789(%rax), %edx
        xorl    %ebx, %ebx
        cmpl    %ecx, %edx
        movl    $-1, %edx
        setg    %bl
        cmovge  %ebx, %edx
        addq    $4, %rax
        addl    %edx, %esi
        cmpq    $4096, %rax
        jne     .L15
        addq    $4, %rbp
        cmpq    $4096, %rbp
        jne     .L18
        addl    $1, %r8d
        cmpl    $500, %r8d
        jne     .L12
        movl    $.LC0, %edi
        xorl    %eax, %eax
        call    printf
        addq    $8, %rsp
        .cfi_def_cfa_offset 24
        xorl    %eax, %eax
        popq    %rbx
        .cfi_def_cfa_offset 16
        popq    %rbp
        .cfi_def_cfa_offset 8
        ret
        .cfi_endproc
.LFE20:
        .size   main, .-main
        .local  arr.2789
        .comm   arr.2789,4096,32
        .section        .note.GNU-stack,"",@progbits

以下版本使用无分支方法((a > b) - (a < b)):

        .file   "cmp.c"
        .text
        .section        .rodata.str1.1,"aMS",@progbits,1
.LC0:
        .string "%d=0\n"
        .text
        .p2align 4,,15
.globl main
        .type   main, @function
main:
.LFB20:
        .cfi_startproc
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        pushq   %rbx
        .cfi_def_cfa_offset 24
        .cfi_offset 3, -24
        movl    $arr.2789, %ebx
        subq    $8, %rsp
        .cfi_def_cfa_offset 32
.L9:
        leaq    4(%rbx), %rbp
.L10:
        call    rand
        movb    %al, (%rbx)
        addq    $1, %rbx
        cmpq    %rbx, %rbp
        jne     .L10
        cmpq    $arr.2789+4096, %rbp
        jne     .L9
        xorl    %r8d, %r8d
        xorl    %esi, %esi
.L19:
        movl    %ebp, %ebx
        xorl    %edi, %edi
        .p2align 4,,10
        .p2align 3
.L24:
        movl    %ebp, %ecx
        xorl    %eax, %eax
        jmp     .L22
        .p2align 4,,10
        .p2align 3
.L20:
        movl    arr.2789(%rax), %ecx
.L22:
        xorl    %edx, %edx
        cmpl    %ebx, %ecx
        setg    %cl
        setl    %dl
        movzbl  %cl, %ecx
        subl    %ecx, %edx
        addl    %edx, %esi
        addq    $4, %rax
        cmpq    $4096, %rax
        jne     .L20
        addq    $4, %rdi
        cmpq    $4096, %rdi
        je      .L21
        movl    arr.2789(%rdi), %ebx
        jmp     .L24
.L21:
        addl    $1, %r8d
        cmpl    $500, %r8d
        jne     .L19
        movl    $.LC0, %edi
        xorl    %eax, %eax
        call    printf
        addq    $8, %rsp
        .cfi_def_cfa_offset 24
        xorl    %eax, %eax
        popq    %rbx
        .cfi_def_cfa_offset 16
        popq    %rbp
        .cfi_def_cfa_offset 8
        ret
        .cfi_endproc
.LFE20:
        .size   main, .-main
        .local  arr.2789
        .comm   arr.2789,4096,32
        .section        .note.GNU-stack,"",@progbits

答案 2 :(得分:15)

好的,我设法将其归结为四条指令:)基本思路如下:

一半时间,差异足够小,可以放入整数。在这种情况下,只需返回差异。否则,将第一个向右移动。关键问题是当时有什么位转移到MSB。

让我们看两个极端的例子,为简单起见,使用8位而不是32位:

 10000000 INT_MIN
 01111111 INT_MAX
---------
000000001 difference
 00000000 shifted

 01111111 INT_MAX
 10000000 INT_MIN
---------
111111111 difference
 11111111 shifted

将进位位移入第一种情况会产生0(尽管INT_MIN不等于INT_MAX)而第二种情况会产生一些负数(尽管INT_MAX不小比INT_MIN)。

但是如果我们在进行转换之前翻转进位,我们会得到合理的数字:

 10000000 INT_MIN
 01111111 INT_MAX
---------
000000001 difference
100000001 carry flipped
 10000000 shifted

 01111111 INT_MAX
 10000000 INT_MIN
---------
111111111 difference
011111111 carry flipped
 01111111 shifted

我确信有一个深刻的数学原因,为什么有意义地翻转进位,但我还没有看到它。

int compare_int(int a, int b)
{
    __asm__ __volatile__ (
        "sub %1, %0 \n\t"
        "jno 1f \n\t"
        "cmc \n\t"
        "rcr %0 \n\t"
        "1: "
    : "+r"(a)
    : "r"(b)
    : "cc");
    return a;
}

我已经使用一百万个随机输入以及INT_MIN,-INT_MAX,INT_MIN / 2,-1,0,1,INT_MAX / 2,INT_MAX / 2 + 1,INT_MAX的每个组合测试了代码。所有测试都通过了你能证我错了吗?

答案 3 :(得分:10)

为了我的价值,我整理了一个SSE2实现。 vec_compare1使用与compare2相同的方法,但只需要三条SSE2算术指令:

#include <stdio.h>
#include <stdlib.h>
#include <emmintrin.h>

#define COUNT 1024
#define LOOPS 500
#define COMPARE vec_compare1
#define USE_RAND 1

int arr[COUNT] __attribute__ ((aligned(16)));

typedef __m128i vSInt32;

vSInt32 vec_compare1 (vSInt32 va, vSInt32 vb)
{
    vSInt32 vcmp1 = _mm_cmpgt_epi32(va, vb);
    vSInt32 vcmp2 = _mm_cmpgt_epi32(vb, va);
    return _mm_sub_epi32(vcmp2, vcmp1);
}

int main ()
{
    for (int i = 0; i < COUNT; i++) {
#if USE_RAND
        arr[i] = rand();
#else
        for (int b = 0; b < sizeof(arr[i]); b++) {
            *((unsigned char *)&arr[i] + b) = rand();
        }
#endif
    }

    vSInt32 vsum = _mm_set1_epi32(0);

    for (int l = 0; l < LOOPS; l++) {
        for (int i = 0; i < COUNT; i++) {
            for (int j = 0; j < COUNT; j+=4) {
                vSInt32 v1 = _mm_loadu_si128(&arr[i]);
                vSInt32 v2 = _mm_load_si128(&arr[j]);
                vSInt32 v = COMPARE(v1, v2);
                vsum = _mm_add_epi32(vsum, v);
            }
        }
    }

    printf("vsum = %vd\n", vsum);

    return 0;
}

此时间为0.137秒。

比较2与相同CPU和编译器的时间是0.674秒。

所以SSE2的实现速度提高了4倍,这可能是预期的(因为它是4宽SIMD)。

答案 4 :(得分:3)

此代码没有分支并使用5条指令。在最近的英特尔处理器上,它可能胜过其他无分支的替代品,其中cmov *指令非常昂贵。缺点是非对称返回值(INT_MIN + 1,0,1)。

int compare_int (int a, int b)
{
    int res;

    __asm__ __volatile__ (
        "xor %0, %0 \n\t"
        "cmpl %2, %1 \n\t"
        "setl %b0 \n\t"
        "rorl $1, %0 \n\t"
        "setnz %b0 \n\t"
    : "=q"(res)
    : "r"(a)
    , "r"(b)
    : "cc"
    );

    return res;
}

此变体不需要初始化,因此它只使用4条指令:

int compare_int (int a, int b)
{
    __asm__ __volatile__ (
        "subl %1, %0 \n\t"
        "setl %b0 \n\t"
        "rorl $1, %0 \n\t"
        "setnz %b0 \n\t"
    : "+q"(a)
    : "r"(b)
    : "cc"
    );

    return a;
}

答案 5 :(得分:0)

也许你可以使用以下想法(在伪代码中;没有编写asm-code,因为我对语法不满意):

  1. 减去数字(result = a - b
  2. 如果没有溢出,完成(jo指令和分支预测应该在这里工作得很好)
  3. 如果有溢出,请使用任何强大的方法(return (a < b) ? -1 : (a > b)
  4. 编辑:以获得更简单:如果有溢出,请翻转结果的符号,而不是第3步

答案 6 :(得分:-2)

您可以考虑将整数提升为64位值。