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


109

요약:

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

(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 비트입니다.


22
if-branch가 너무 비싸다는 것을 어떻게 결정 했습니까?
djechlin 2013 년

7
지점 있다는 것을 어떻게 결정 했습니까?
leemes

13
프로파일 링을 위해 +1, 현대 분기 예측을 사용하면 필요하지 않을 수 있습니다. 또한 자체 이미지 처리 알고리즘을 코딩하는 이유 는 무엇입니까?
TC1 2013 년

8
"가장 빠른 비트 트위들 링 해킹은 ..."어쩌면 y += !y? 그것을 계산하는 데 필요한 분기가 없습니다. 당신은 비교할 수 x / (y + !y)에 대해 x / max(y, 1)아마도하고 y ? (x/y) : 0. 적어도 최적화가 켜져 있으면 둘 중 하나에 분기가 없을 것 같습니다.
leemes 17:07에

6
현대의 분기 예측을 수행 할 필요가 없다고 생각하는 사람은 픽셀 당 실행되는 분기 제거 코드를 충분히 프로파일 링하지 않았습니다. 알파 0섹션이 거대하고 연속적인 경우 현대 분기 예측이 허용됩니다 . 마이크로 최적화를 조작 할 있는 곳 이 있으며 픽셀 당 작업이 바로 그 곳입니다.
Yakk-Adam Nevraumont 2013 년

답변:


107

일부 주석에서 영감을 받아 펜티엄과 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는 조건부 실행을 통해 완전히 구현 되기 때문에 모든 버전에는 분할 루틴에 대한 분기가 필요합니다 .


결과 어셈블러 코드를 보여 주시겠습니까? 아니면 분기가 없다는 것을 어떻게 결정 했습니까?
Haatschii

1
대박. 만들 수 있습니다 constexpr와 같은 불필요한 타입 캐스트를 피 : template<typename T, typename U> constexpr auto fdiv( T t, U u ) -> decltype(t/(u+!u)) { return t/(u+!u); } 그리고 당신이 원하는 경우 255,(lhs)/(rhs+!rhs) & -!rhs
Yakk - 아담 Nevraumont

1
@leemes 그러나 나는 의미 |하지 않았다 &. 죄송 ( (lhs)/(rhs+!rhs) ) | -!rhs합니다. 값을 0xFFFFFFFif rhsis 0lhs/rhsif 로 설정해야 합니다 rhs!=0.
Yakk-Adam Nevraumont 2013 년

1
이것은 매우 영리했습니다.
Theodoros Chatzigiannakis

1
좋은 대답입니다! 나는 일반적으로 이런 종류의 것들에 대해 어셈블리에 의존하지만, 그것은 항상 유지하기가 끔찍합니다 (덜 이식성있는 것은 말할 것도 없습니다;)).
레오

20

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이 더 일반적이됨에 따라 이점을 유지합니다.

하지만 컴파일러와 대표 샘플 데이터에서 어떤 일이 발생하는지 실제로 측정해야합니다.


4
항목의 50 %를 d=0거의 항상하는 대신 무작위로 만들면 d!=0더 많은 분기 예측 실패를 볼 수 있습니다. 한 가지가 거의 항상 뒤 따르는 경우 또는 한 가지 또는 다른 가지의 다음이 정말로 덩어리 진 경우 분기 예측이 좋습니다.
Yakk-Adam Nevraumont

@Yakk d반복은 내부 루프이므로 d == 0케이스가 균등하게 분산됩니다. 그리고 사례의 50 %를 d == 0현실적으로 만들고 있습니까?

2
0.002%사건을 d==0현실적으로 만들고 있습니까? d==0케이스에 65000 번 반복 할 때마다 배포됩니다 . 하지만 50%힘이 자주 발생하지, 10%또는 1%쉽게 발생, 또는 수 90%또는 99%. 표시되는 테스트는 "기본적으로 분기로 내려 가지 않으면 분기 예측이 분기를 제거하는 데 무의미한가?"라는 테스트 만 수행합니다. 대답은 "예,하지만 흥미롭지 않습니다"입니다.
Yakk-Adam Nevraumont 2013 년

1
아니오, 소음으로 인해 차이가 효과적으로 보이지 않기 때문입니다.
Joe

3
0의 분포는 질문자의 상황에서 발견 된 분포와 관련이 없습니다. 0 알파와 기타가 혼합 된 이미지에는 구멍이 있거나 불규칙한 모양이 있지만 (일반적으로) 이것은 노이즈가 아닙니다. 데이터에 대해 아무것도 모른다고 가정하고 노이즈로 간주하는 것은 실수입니다. 이것은 알파가 0 일 수있는 실제 이미지가있는 실제 응용 프로그램입니다. 그리고 픽셀 행은 모두 a = 0 또는 모두 a> 0 일 가능성이 높기 때문에 분기 예측을 활용하는 것이 가장 빠를 수 있습니다. 특히 a = 0이 많이 발생하고 (느린) 분할 (15+ 사이클 !)는 피합니다.
DDS

13

플랫폼을 모르면 가장 효율적인 방법을 정확히 알 수있는 방법이 없지만 일반 시스템에서는 이것이 최적에 가까울 수 있습니다 (인텔 어셈블러 구문 사용).

(제수가 안으로 ecx있고 배당금이 안으로 있다고 가정 eax)

mov ebx, ecx
neg ebx
sbb ebx, ebx
add ecx, ebx
div eax, ecx

4 개의 분기되지 않은 단일 사이클 명령어와 나누기. 몫은 안에 eax있고 나머지는 edx끝에 있을 것입니다 . (이런 종류의 이유는 사람의 일을하기 위해 컴파일러를 보내지 않는 이유를 보여줍니다).


부서는 어디에 있습니까?
Yakk-Adam Nevraumont 2013 년

1
이것은 나눗셈을하지 않습니다. 제수를 오염시켜 0으로 나눈 것은 불가능합니다
Tyler Durden

@Jens Timmerman 죄송합니다. div 문을 추가하기 전에 썼습니다. 텍스트를 업데이트했습니다.
Tyler Durden 2013 년

1

링크 에 따르면 SIGFPE 신호를 차단할 수 있습니다 sigaction()(내가 직접 시도하지는 않았지만 작동해야한다고 생각합니다).

0으로 나누기 오류가 극히 드문 경우 가능한 가장 빠른 방법입니다. 유효한 나누기가 아닌 0으로 나누기 만 지불하면 정상적인 실행 경로가 전혀 변경되지 않습니다.

그러나 무시되는 모든 예외에는 OS가 관여하므로 비용이 많이 듭니다. 나는 당신이 무시하는 0으로 나누기 당 적어도 천 개의 좋은 나누기를 가져야한다고 생각합니다. 예외가 그보다 더 자주 발생하면 분할 전에 모든 값을 확인하는 것보다 예외를 무시하여 더 많은 비용을 지불 할 수 있습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.