효율적인 정수 비교 기능
compare
는 두 의 인수를 입니다.a
★★★★★★★★★★★★★★★★★」b
순서를 나타내는 정수를 반환합니다. ifa
작다b
결과는 음의 정수입니다. ifa
크다b
그 결과 몇 가지 양의 정수가 됩니다. 않으면, 「」가 됩니다.a
★★★★★★★★★★★★★★★★★」b
0으로 하다
이 함수는 표준 라이브러리에서 정렬 및 검색 알고리즘을 매개 변수화하는 데 자주 사용됩니다.
의 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=0
4개의 명령 솔루션은 실제로 매우 느립니다(동일합니다).
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 코드를 쓰지 않았습니다).
- 숫자를 빼다(
result = a - b
) - 오버플로가 없는 경우 완료(
jo
여기서는 명령과 분기 예측이 매우 효과적입니다.) - 오버플로우가 발생했을 경우는, 임의의 견고한 방법을 사용합니다(
return (a < b) ? -1 : (a > b)
)
편집:
보다 심플하게 하기 위해서: 오버플로가 발생했을 경우는, 순서 3이 아니고, 결과의 기호를 뒤집습니다. 를 클릭합니다.
정수를 64비트 값으로 승격하는 것을 고려할 수 있습니다.
언급URL : https://stackoverflow.com/questions/10996418/efficient-integer-compare-function
'programing' 카테고리의 다른 글
빈 테이블 'Primary' 키에 대해 '1' 항목이 중복됩니다. (0) | 2022.09.08 |
---|---|
Vuex는 키를 사용하여 상태의 개체에 액세스합니다. (0) | 2022.09.08 |
pycurl 설치 시 "curl-config를 실행할 수 없습니다: [Errno 2] 해당 파일 또는 디렉토리가 없습니다" (0) | 2022.09.08 |
JavaScript 배열에 포함된 최대 수를 찾으려면 어떻게 해야 합니까? (0) | 2022.09.08 |
슬롯 분할 문제를 사용하는 요소 UI 테이블 열 "정의되지 않은 속성 'column_name'을 읽을 수 없습니다" (0) | 2022.09.08 |