programing

효율적인 정수 비교 기능

bestcode 2022. 9. 8. 22:17
반응형

효율적인 정수 비교 기능

compare는 두 의 인수를 입니다.a ★★★★★★★★★★★★★★★★★」b순서를 나타내는 정수를 반환합니다. ifa 작다b결과는 음의 정수입니다. ifa 크다b그 결과 몇 가지 양의 정수가 됩니다. 않으면, 「」가 됩니다.a ★★★★★★★★★★★★★★★★★」b0으로 하다

이 함수는 표준 라이브러리에서 정렬 및 검색 알고리즘을 매개 변수화하는 데 자주 사용됩니다.

compare문자의 함수는 매우 간단합니다.인수만 빼면 됩니다.

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

, (가칭)이 되는 시스템.sizeof(char) == sizeof(int)

일반적으로 두 정수 간의 차이가 정수에 맞지 않기 때문에 이 방법은 정수를 비교하는 데 사용할 수 없습니다.를 들어, 「」라고 하는 것은,INT_MAX - (-1) = INT_MIN INT_MAX 작다-1(기술적으로, 솟아오르는 확실하지 않은 행동을 하지만 가정의 나머지 arithmetic 이어진다).

그러면 우리가 어떻게 정수의 비교 기능 효율적으로 구현할 수 있니?여기 첫번째 시도: 있다.

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지침에 끝날 수 있어요?더 효율적이다 덜 직설적인 방법은 없습니까?

이건, 오버 플로 또는 underflow을 겪지 않고, 어떤 지점을 두고 있다.

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;
}

입니다.「 」로 되어 .gcc -std=c99 -O2정수일 정수일 경우)USE_RAND=1

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

전용 사용자 315052가 느렸습니다.User315052의 해결책은 오로지 5에 대한 지침을 편찬에도 불구하고 느렸다. 적더라도 (「」, 「」, 「」, 「」, 「」, 「」)가 있기 cmovge를 참조해 주세요.

때 양의 정수와 함께 사용되는 전체적으로, FredOverflow의4-instruction 어셈블리 구현은 가장 빠른 것이었다.때문에 별개로 오버 플로를 처리하기 때문에4-instuction 시험, 그리고 이것들이 시험에서 발생하지 않아 편견을 가진 것 그러나 이 코드만, 속도를 성공적인 분기 예측에 기인할 수 있는 정수 범위 RAND_MAX benchmarked.

의 정수 「정수」)를 합니다.USE_RAND=04개의 명령 솔루션은 실제로 매우 느립니다(동일합니다).

compare4: 0m1.897s

다음 항목은 항상 저에게 매우 효율적인 것으로 입증되었습니다.

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의 훌륭한 동반자 답변에 대한 후속 조치로, 그의 프로그램이 위에 게시된 것과 동일한 어셈블리 코드를 테스트했다는 것을 확신하지 못했습니다.그리고 컴파일러 출력을 자세히 조사해보니 컴파일러가 우리의 답변과 같은 명령을 생성하지 않는 것을 알게 되었습니다.그래서 저는 그의 테스트 프로그램을 가져다가 우리가 게시한 것과 일치하도록 조립품 출력을 손으로 수정하고 그 결과를 비교했습니다.그 두 버전은 대략 비슷한 것 같다.

./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

방식(branchless method을 합니다.(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

네 가지 명령으로 요약할 수 있었습니다.기본적인 아이디어는 다음과 같습니다.

대부분의 경우, 그 차이는 정수에 들어갈 정도로 작습니다.그러면 차액을 돌려주세요.그렇지 않으면 숫자 1을 오른쪽으로 이동합니다.그렇다면 MSB로 전환할 비트가 중요한 문제입니다.

단순화를 위해 32비트가 아닌 8비트를 사용하는 두 가지 극단적인 예를 살펴보겠습니다.

 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;
}

100만 개의 랜덤 입력에 INT_MIN, -INT_MAX, INT_MIN/2, -1, 0, 1, INT_MAX/2, INT_MAX/2+1, INT_MAX의 모든 조합을 사용하여 코드를 테스트했습니다.모든 테스트에 합격했습니다.나를 잘못 사랑할 수 있니?

이 코드에는 브런치가 없으며 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;
}

SSE2의 실장을 정리합니다. vec_compare1와 같은 어프로치를 사용하다compare2단, 3개의 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초입니다.

같은 CPU와 컴파일러를 사용한 비교2의 시간은 0.674초입니다.

따라서 SSE2 구현 속도는 예상대로 약 4배 빨라집니다(4폭 SIMD이기 때문에).

다음 아이디어를 사용할 수 있습니다(의사 코드로, 구문이 익숙하지 않기 때문에 asm 코드를 쓰지 않았습니다).

  1. 숫자를 빼다(result = a - b)
  2. 오버플로가 없는 경우 완료(jo여기서는 명령과 분기 예측이 매우 효과적입니다.)
  3. 오버플로우가 발생했을 경우는, 임의의 견고한 방법을 사용합니다(return (a < b) ? -1 : (a > b))

편집: 보다 심플하게 하기 위해서: 오버플로가 발생했을 경우는, 순서 3이 아니고, 결과의 기호를 뒤집습니다. 를 클릭합니다.

정수를 64비트 값으로 승격하는 것을 고려할 수 있습니다.

언급URL : https://stackoverflow.com/questions/10996418/efficient-integer-compare-function

반응형