요약:
계산하는 가장 빠른 방법을 찾고 있습니다
(int) x / (int) y
예외없이 y==0
. 대신 임의의 결과를 원합니다.
배경:
이미지 처리 알고리즘을 코딩 할 때 종종 (누적 된) 알파 값으로 나눌 필요가 있습니다. 가장 간단한 변형은 정수 산술을 사용하는 일반 C 코드입니다. 내 문제는 일반적으로 결과 픽셀에 대해 0으로 나누기 오류가 발생한다는 것입니다 alpha==0
. 그러나 이것은 결과가 전혀 중요하지 않은 정확히 픽셀입니다. 나는 픽셀의 색상 값에 대해 신경 쓰지 않습니다 alpha==0
.
세부:
다음과 같은 것을 찾고 있습니다.
result = (y==0)? 0 : x/y;
또는
result = x / MAX( y, 1 );
x와 y는 양의 정수입니다. 코드는 중첩 루프에서 여러 번 실행되므로 조건부 분기를 제거하는 방법을 찾고 있습니다.
y가 바이트 범위를 초과하지 않으면 솔루션에 만족합니다.
unsigned char kill_zero_table[256] = { 1, 1, 2, 3, 4, 5, 6, 7, [...] 255 };
[...]
result = x / kill_zero_table[y];
그러나 이것은 분명히 더 큰 범위에서 잘 작동하지 않습니다.
마지막 질문은 다음과 같습니다. 0을 다른 정수 값으로 변경하고 다른 모든 값을 변경하지 않은 채로 변경하는 가장 빠른 비트 twiddling 해킹은 무엇입니까?
설명
나는 분기가 너무 비싸다고 100 % 확신하지 못합니다. 그러나 다른 컴파일러가 사용되므로 최적화가 거의없는 벤치마킹을 선호합니다 (실제로 의심 스럽습니다).
확실히 컴파일러는 비트 트위들 링에 관해서는 훌륭하지만 C로 “do n’t care”결과를 표현할 수 없으므로 컴파일러는 전체 범위의 최적화를 사용할 수 없습니다.
코드는 완전히 C와 호환되어야하며 주요 플랫폼은 gcc 및 clang 및 MacOS가있는 Linux 64 비트입니다.
답변
일부 주석에서 영감을 받아 펜티엄과 gcc
컴파일러 에서 브랜치를 제거했습니다.
int f (int x, int y)
{
y += y == 0;
return x/y;
}
컴파일러는 기본적으로 테스트의 조건 플래그를 추가로 사용할 수 있음을 인식합니다.
요청에 따라 어셈블리 :
.globl f
.type f, @function
f:
pushl %ebp
xorl %eax, %eax
movl %esp, %ebp
movl 12(%ebp), %edx
testl %edx, %edx
sete %al
addl %edx, %eax
movl 8(%ebp), %edx
movl %eax, %ecx
popl %ebp
movl %edx, %eax
sarl $31, %edx
idivl %ecx
ret
인기있는 질문과 답변으로 판명되었으므로 좀 더 자세히 설명하겠습니다. 위의 예는 컴파일러가 인식하는 프로그래밍 관용구를 기반으로합니다. 위의 경우 부울 표현식이 적분 산술에 사용되며이를 위해 하드웨어에서 조건 플래그를 사용합니다. 일반적으로 조건 플래그는 관용구를 사용하여 C에서만 액세스 할 수 있습니다. 그렇기 때문에 (인라인) 어셈블리에 의존하지 않고 C에서 이식 가능한 다중 정밀도 정수 라이브러리를 만드는 것이 매우 어렵습니다. 내 생각에 대부분의 괜찮은 컴파일러는 위의 관용구를 이해할 것입니다.
위의 주석 중 일부에서 언급했듯이 분기를 피하는 또 다른 방법은 술어 실행입니다. 따라서 필립의 첫 번째 코드와 내 코드를 ARM의 컴파일러와 조건부 실행 기능을 갖춘 ARM 아키텍처 용 GCC 컴파일러를 통해 실행했습니다. 두 컴파일러 모두 두 코드 샘플에서 분기를 피합니다.
ARM 컴파일러가있는 Philipp의 버전 :
f PROC
CMP r1,#0
BNE __aeabi_idivmod
MOVEQ r0,#0
BX lr
GCC가있는 Philipp의 버전 :
f:
subs r3, r1, #0
str lr, [sp, #-4]!
moveq r0, r3
ldreq pc, [sp], #4
bl __divsi3
ldr pc, [sp], #4
ARM 컴파일러를 사용한 내 코드 :
f PROC
RSBS r2,r1,#1
MOVCC r2,#0
ADD r1,r1,r2
B __aeabi_idivmod
GCC를 사용한 내 코드 :
f:
str lr, [sp, #-4]!
cmp r1, #0
addeq r1, r1, #1
bl __divsi3
ldr pc, [sp], #4
이 버전의 ARM에는 분할을위한 하드웨어가 없지만 테스트 y == 0
는 조건부 실행을 통해 완전히 구현 되기 때문에 모든 버전에는 분할 루틴에 대한 분기가 필요합니다 .
답변
GCC 4.7.2를 사용하는 Windows의 구체적인 수치는 다음과 같습니다.
#include <stdio.h>
#include <stdlib.h>
int main()
{
unsigned int result = 0;
for (int n = -500000000; n != 500000000; n++)
{
int d = -1;
for (int i = 0; i != ITERATIONS; i++)
d &= rand();
#if CHECK == 0
if (d == 0) result++;
#elif CHECK == 1
result += n / d;
#elif CHECK == 2
result += n / (d + !d);
#elif CHECK == 3
result += d == 0 ? 0 : n / d;
#elif CHECK == 4
result += d == 0 ? 1 : n / d;
#elif CHECK == 5
if (d != 0) result += n / d;
#endif
}
printf("%u\n", result);
}
의도적으로을 호출하지 않았 srand()
으므로 rand()
항상 정확히 동일한 결과를 반환합니다. 또한 -DCHECK=0
단순히 0을 계산하므로 얼마나 자주 나타나는지 분명합니다.
이제 다양한 방법으로 컴파일 및 타이밍 :
$ for it in 0 1 2 3 4 5; do for ch in 0 1 2 3 4 5; do gcc test.cc -o test -O -DITERATIONS=$it -DCHECK=$ch && { time=`time ./test`; echo "Iterations $it, check $ch: exit status $?, output $time"; }; done; done
표로 요약 할 수있는 출력을 보여줍니다.
Iterations → | 0 | 1 | 2 | 3 | 4 | 5
-------------+-------------------------------------------------------------------
Zeroes | 0 | 1 | 133173 | 1593376 | 135245875 | 373728555
Check 1 | 0m0.612s | - | - | - | - | -
Check 2 | 0m0.612s | 0m6.527s | 0m9.718s | 0m13.464s | 0m18.422s | 0m22.871s
Check 3 | 0m0.616s | 0m5.601s | 0m8.954s | 0m13.211s | 0m19.579s | 0m25.389s
Check 4 | 0m0.611s | 0m5.570s | 0m9.030s | 0m13.544s | 0m19.393s | 0m25.081s
Check 5 | 0m0.612s | 0m5.627s | 0m9.322s | 0m14.218s | 0m19.576s | 0m25.443s
0이 드문 경우 -DCHECK=2
버전 성능이 저하됩니다. 0이 더 많이 나타나기 시작하면 -DCHECK=2
케이스의 성능이 훨씬 좋아지기 시작합니다. 다른 옵션 중에서 실제로 큰 차이가 없습니다.
를 들어 -O3
,하지만, 그것은 다른 이야기입니다 :
Iterations → | 0 | 1 | 2 | 3 | 4 | 5
-------------+-------------------------------------------------------------------
Zeroes | 0 | 1 | 133173 | 1593376 | 135245875 | 373728555
Check 1 | 0m0.646s | - | - | - | - | -
Check 2 | 0m0.654s | 0m5.670s | 0m9.905s | 0m14.238s | 0m17.520s | 0m22.101s
Check 3 | 0m0.647s | 0m5.611s | 0m9.085s | 0m13.626s | 0m18.679s | 0m25.513s
Check 4 | 0m0.649s | 0m5.381s | 0m9.117s | 0m13.692s | 0m18.878s | 0m25.354s
Check 5 | 0m0.649s | 0m6.178s | 0m9.032s | 0m13.783s | 0m18.593s | 0m25.377s
거기에서 수표 2는 다른 수표와 비교할 때 단점이 없으며 0이 더 일반적이됨에 따라 이점을 유지합니다.
하지만 컴파일러와 대표 샘플 데이터에서 어떤 일이 발생하는지 실제로 측정해야합니다.
답변
플랫폼을 모르면 가장 효율적인 방법을 정확히 알 수있는 방법이 없지만 일반 시스템에서는 이것이 최적에 가까울 수 있습니다 (인텔 어셈블러 구문 사용).
(제수가 안으로 ecx
있고 배당금이 안으로 있다고 가정 eax
)
mov ebx, ecx
neg ebx
sbb ebx, ebx
add ecx, ebx
div eax, ecx
4 개의 분기되지 않은 단일 사이클 명령어와 나누기. 몫은 안에 eax
있고 나머지는 edx
끝에 있을 것입니다 . (이런 종류의 이유는 사람의 일을하기 위해 컴파일러를 보내지 않는 이유를 보여줍니다).
답변
이 링크 에 따르면 SIGFPE 신호를 차단할 수 있습니다 sigaction()
(내가 직접 시도하지는 않았지만 작동해야한다고 생각합니다).
0으로 나누기 오류가 극히 드문 경우 가능한 가장 빠른 방법입니다. 유효한 나누기가 아닌 0으로 나누기 만 지불하면 정상적인 실행 경로가 전혀 변경되지 않습니다.
그러나 무시되는 모든 예외에는 OS가 관여하므로 비용이 많이 듭니다. 나는 당신이 무시하는 0으로 나누기 당 적어도 천 개의 좋은 나누기를 가져야한다고 생각합니다. 예외가 그보다 더 자주 발생하면 분할 전에 모든 값을 확인하는 것보다 예외를 무시하여 더 많은 비용을 지불 할 수 있습니다.