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?
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
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
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ć?
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).
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;
}
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):
- odjąć liczby (
result = a - b
) - Jeśli nie ma przepełnienia, done (
jo
instrukcja i przewidywanie gałęzi powinny działać bardzo dobrze tutaj) - 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.
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.
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