Efektywna funkcja porównywania liczb całkowitych

Funkcja compare jest funkcją, która pobiera dwa argumenty a i b i zwraca liczbę całkowitą opisującą ich kolejność. Jeśli a jest mniejsze niż b, wynikiem jest jakaś ujemna liczba całkowita. Jeśli a jest większe niż b, wynikiem jest pewna dodatnia liczba całkowita. W przeciwnym razie a i b są równe, a wynik jest równy zeru.

Ta funkcja jest często używana do parametryzacji algorytmów sortowania i Wyszukiwania ze standardowych bibliotek.

Realizacja compare funkcja dla znaków jest dość prosta; po prostu odejmujesz argumenty:

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

To działa, ponieważ zakłada się, że różnica między dwoma znakami pasuje do liczby całkowitej. (Zauważ, że to założenie nie obowiązuje dla systemów, w których sizeof(char) == sizeof(int).)

Ta sztuczka nie może działać, aby porównać liczby całkowite, ponieważ różnica między dwiema liczbami całkowitymi generalnie nie pasuje do liczby całkowitej. Na przykład INT_MAX - (-1) = INT_MIN sugeruje, że INT_MAX jest mniejszy niż -1 (technicznie rzecz biorąc, przepełnienie prowadzi do nieokreślonego zachowania, ale załóżmy arytmetykę modulo).

Więc jak możemy efektywnie zaimplementować funkcję porównywania dla liczb całkowitych? Oto moja pierwsza próba:

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

Czy można to zrobić w mniej niż 6 instrukcjach? Czy istnieje mniej prosty sposób, który jest bardziej skuteczny?

Author: Jonathan Leffler, 2012-06-12

7 answers

Następujące zawsze okazały się dla mnie dość skuteczne:

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

Z gcc -O2 -S, kompiluje się do następujących pięciu instrukcji:

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

Jako kontynuację Ambroz Bizjak ' s excellent companion answer , nie byłem przekonany, że jego program przetestował ten sam kod asemblera, co został opublikowany powyżej. I, kiedy studiowałem wyjście kompilatora dokładniej, zauważyłem, że kompilator nie generuje tych samych instrukcji, które zostały zamieszczone w obu naszych odpowiedzi. Więc, wziąłem jego program testowy, ręcznie zmodyfikowałem wyjście montażowe, aby pasowało do tego, co opublikowaliśmy, i porównałem uzyskane czasy. wydaje się, że obie wersje porównują się mniej więcej identycznie.

./opt_cmp_branchless: 0m1.070s
./opt_cmp_branch:     0m1.037s
[9]}zamieszczam zbiór każdego programu w całości, aby inni mogli spróbować tego samego eksperymentu i potwierdzić lub zaprzeczyć mojej obserwacji.

Poniżej znajduje się Wersja z instrukcją 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

Poniższa wersja wykorzystuje bezgałęziową metoda ((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
 55
Author: jxh,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:02:23

Ten nie ma rozgałęzień i nie cierpi na przepełnienie lub niedotlenienie:

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

Z gcc -O2 -S, kompiluje się do następujących sześciu instrukcji:

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

Oto kod do porównywania różnych implementacji porównawczych:

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

Wyniki na moim 64-bitowym systemie, skompilowane z gcc -std=c99 -O2, dla dodatnich liczb całkowitych (USE_RAND=1):

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

Z rozwiązań tylko dla C, ten, który zaproponowałem, był najszybszy. rozwiązanie user315052 było wolniejsze mimo kompilacji do tylko 5 instrukcje. Spowolnienie jest prawdopodobne, ponieważ pomimo posiadania o jedną instrukcję mniej, istnieje instrukcja warunkowa (cmovge).

Ogólnie Rzecz Biorąc, implementacja zestawu 4-instrukcji FredOverflow była najszybsza, gdy była używana z dodatnimi liczbami całkowitymi. Jednak kod ten oznaczał tylko zakres liczb całkowitych RAND_MAX, więc test 4-instanction jest stronniczy, ponieważ obsługuje przepełnienia oddzielnie, a te nie występują w teście; prędkość może być spowodowana pomyślnym odgałęzieniem przewidywanie.

Z pełnym zakresem liczb całkowitych (USE_RAND=0), rozwiązanie 4-instrukcji jest w rzeczywistości bardzo wolne (inne są takie same):

compare4: 0m1.897s
 96
Author: Ambroz Bizjak,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-06-12 18:53:23

Ok, udało mi się sprowadzić go do czterech instrukcji :) podstawowa idea jest następująca:

W połowie czasu różnica jest na tyle mała, że zmieści się w liczbie całkowitej. W takim razie zwróć różnicę. W przeciwnym razie przesuń numer jeden w prawo. Kluczowe pytanie brzmi, co bit przenieść do MSB następnie.

Spójrzmy na dwa skrajne przykłady, używając 8 bitów zamiast 32 bitów dla uproszczenia:

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

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

Przesunięcie bitu carry w dałoby 0 dla pierwszy przypadek (chociaż INT_MIN nie jest równy INT_MAX) i pewna liczba ujemna dla drugiego przypadku (chociaż INT_MAX nie jest mniejsza niż INT_MIN).

Ale jeśli przewrócimy bit carry przed wykonaniem zmiany, otrzymamy sensowne liczby:

 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

Jestem pewien, że istnieje głęboki matematyczny powód, dla którego ma sens przewracanie bitu do przenoszenia, ale jeszcze tego nie widzę.

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

Przetestowałem kod z milionem losowych wejść plus każda kombinacja INT_MIN, -INT_MAX, INT_MIN/2, -1, 0, 1, INT_MAX/2, INT_MAX / 2+1, INT_MAX. Wszystkie testy przeszły pomyślnie. Możesz się mylić?

 16
Author: fredoverflow,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-06-12 16:00:56

Jeśli to coś warte, przygotowałem implementację SSE2. vec_compare1 używa tego samego podejścia co compare2, ale wymaga tylko trzech instrukcji arytmetycznych 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;
}

Czas na to 0.137 s.

Czas dla compare2 z tym samym procesorem i kompilatorem wynosi 0,674 s.

Więc implementacja SSE2 jest około 4x szybsza, jak można się było spodziewać (ponieważ jest to 4-szeroka SIMD).

 10
Author: Paul R,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-06-12 21:17:05

Ten kod nie ma gałęzi i używa 5 instrukcji. Może to przewyższać inne alternatywy bez gałęzi na ostatnich procesorach Intel, gdzie instrukcje cmov* są dość drogie. Wadą jest niesymetryczna wartość zwracana (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;
}

Ten wariant nie wymaga inicjalizacji, więc używa tylko 4 instrukcji:

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;
}
 3
Author: Evgeny Kluev,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-06-13 18:12:07

Może możesz użyć następującego pomysłu (w pseudo-kodzie; nie napisałem ASM-kodu, ponieważ nie jestem zadowolony ze składni):

  1. odjąć liczby (result = a - b)
  2. Jeśli nie ma przepełnienia, done (jo instrukcja i przewidywanie gałęzi powinny działać bardzo dobrze tutaj)
  3. Jeśli było przepełnienie, użyj dowolnej solidnej metody (return (a < b) ? -1 : (a > b))

Edit: dla dodatkowej prostoty: jeśli było przepełnienie, Odwróć znak wyniku, zamiast kroku 3.

 0
Author: anatolyg,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-06-12 15:02:20

Możesz rozważyć promowanie liczb całkowitych do wartości 64bit.

 -2
Author: Puppy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-06-12 12:32:24