web-dev-qa-db-ja.com

結果が何であっても、ゼロによる除算をサポートする最速の整数除算とは何ですか?

概要:

最速の計算方法を探しています

_(int) x / (int) y
_

_y==0_の例外を取得することなく。代わりに、私は任意の結果が欲しいだけです。


背景:

画像処理アルゴリズムをコーディングするとき、私はしばしば(累積された)アルファ値で除算する必要があります。最も単純なバリアントは、整数演算を使用した単純なCコードです。私の問題は、通常、_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を他の整数値に変更する最速のビットハックです。


明確化

分岐が高すぎることは100%確信できません。ただし、異なるコンパイラが使用されているため、最適化をほとんど行わずにベンチマークを実行することをお勧めします(これには疑問があります)。

確かに、コンパイラは少しいじることに関しては優れていますが、「気にしない」結果をCで表現することはできないため、コンパイラは最適化の全範囲を使用できません。

コードは完全にC互換である必要があり、主なプラットフォームはgccとclangおよびMacOSを搭載したLinux 64ビットです。

109
philipp

Pentiumおよび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で移植可能な多精度整数ライブラリを作成するのは非常に困難です。私の推測では、ほとんどのまともなコンパイラは上記のイディオムを理解するでしょう。

上記のコメントの一部でも述べられているように、分岐を回避する別の方法は、実行の述語です。したがって、私はphilippの最初のコードとコードを取得し、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は、述部の実行により完全に実装されます。

107
Bryan Olivier

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_は単にゼロをカウントするだけなので、出現頻度が明らかです。

次に、さまざまな方法でコンパイルおよびタイミングを調整します。

_$ 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
_

ゼロがまれな場合、_-DCHECK=2_バージョンのパフォーマンスが低下します。ゼロがより多く表示されるようになると、_-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には他のチェックと比較して欠点はなく、ゼロがより一般的になるため、利点は維持されます。

ただし、コンパイラと代表的なサンプルデータで何が起こるかを実際に測定する必要があります。

20
user743382

プラットフォームを知らないと、正確な最も効率的な方法を知る方法はありませんが、一般的なシステムでは、これは最適に近い場合があります(Intelアセンブラ構文を使用):

(除数はecxにあり、被除数はeaxにあると仮定します)

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

4つの分岐していない、単一サイクル命令と除算。商はeaxに、残りはedxに終わります。 (この種のことは、コンパイラーを送って人間の仕事をしたくない理由を示しています)。

13
Tyler Durden

この link によれば、sigaction()でSIGFPEシグナルをブロックすることができます(自分で試したことはありませんが、動作するはずです)。

これは、ゼロ除算エラーが非常にまれな場合に可能な最速のアプローチです。有効な除算ではなくゼロ除算に対してのみ支払い、通常の実行パスはまったく変更されません。

ただし、OSは無視されるすべての例外に関与するため、コストがかかります。私は、あなたが無視するゼロによる部門ごとに少なくとも千の良い部門を持つべきだと思います。例外がそれよりも頻繁に発生する場合は、除算の前にすべての値をチェックするよりも、例外を無視する方が多くのお金を払うでしょう。