결과가 무엇이든 상관없이 0으로 나누기를 지원하는 가장 빠른 정수 나눗셈은 무엇입니까? 결과가 전혀 중요하지 않은

요약:

계산하는 가장 빠른 방법을 찾고 있습니다

(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으로 나누기 당 적어도 천 개의 좋은 나누기를 가져야한다고 생각합니다. 예외가 그보다 더 자주 발생하면 분할 전에 모든 값을 확인하는 것보다 예외를 무시하여 더 많은 비용을 지불 할 수 있습니다.


답변