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


109

概要:

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

(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互換である必要があります。メインプラットフォームはLinux 64ビットで、gccとclang、MacOSを備えています。


22
if分岐のコストが高すぎるとどのように判断しましたか?
djechlin 2013年

7
どのようにして枝がある判断しましたか?
leemes 2013年

13
プロファイリングの+1。今日のブランチ予測では、これは必要ない場合があります。また、なぜ独自の画像処理アルゴリズムをコーディングするのですか?
TC1 2013年

8
「最速のビットいじりハックは...」多分y += !y?それを計算するためにブランチは必要ありません。あなたは、比較可能性x / (y + !y)に対してx / max(y, 1)も多分とy ? (x/y) : 0。少なくとも最適化がオンになっていると、どちらにも分岐はないと思います。
leemes

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 2013年

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-Adam Nevraumont '27年

1
@leemesですが、そうではあり|ませんでした&。Ooops- ( (lhs)/(rhs+!rhs) ) | -!rhs値を0xFFFFFFFif rhsis 0lhs/rhsifに設定する必要がありますrhs!=0
Yakk-Adam Nevraumont 2013年

1
これはとても賢明でした。
Theodoros Chatzigiannakis 2013年

1
正解です。私は通常、この種のことを行うためにアセンブリを使用しますが、それを維持することは常に恐ろしいことです(移植性が低いことは言うまでもありません;))。
2013年

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、ゼロを数えるだけなので、出現頻度が明らかであることにも注意してください。

次に、さまざまな方法でコンパイルしてタイミングを計ります。

$ 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は他のチェックと比較して欠点がなく、ゼロがより一般的になるので、利点を維持します。

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


4
エントリのd=0ほぼ50%をランダムにするのではなく、エントリの50%をランダムにすると、d!=0ブランチ予測の失敗がさらに発生します。分岐予測は、1つの分岐がほとんど常に続いている場合は素晴らしいです、または1つのブランチまたは他の以下は、本当に塊状であれば...
Yakk -アダムNevraumont

@Yakk d反復は内部ループであるため、d == 0ケースは均等に分散されます。また、ケースの50%をd == 0現実的なものにしていますか?

2
0.002%ケースの作成はd==0現実的ですか?それらは全体に分散されており、65000反復ごとにd==0ケースにヒットします。一方で50%かもしれないが、多くの場合、発生しない、10%または1%簡単に起こる、あるいはでき90%たり99%。表示されるテストは、実際には「基本的に分岐を行わない場合、分岐を予測しても分岐を削除しても意味がないのか?」というテストだけであり、答えは「はい、しかしそれは興味深いものではありません」です。
Yakk-Adam Nevraumont 2013年

1
いいえ、違いはノイズのために事実上見えなくなります。
ジョー

3
ゼロの分布は、質問者の状況で見られる分布とは関係ありません。0アルファとその他の混合画像には、穴や不規則な形状がありますが、(通常)これはノイズではありません。データについて何も知らないと想定すること(そしてそれをノイズと見なすこと)は間違いです。これは、アルファが0である可能性のある実際の画像を使用した実際のアプリケーションです。また、ピクセルの行はすべてa = 0またはすべてa> 0である可能性が高いため、特にa = 0が多く発生し、(遅い)除算が行われる場合(15+サイクル)、分岐予測を利用すると非常に高速になる可能性があります。 !)は回避されます。
DDS 2013年

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
これは除算を行わず、除数を汚染するだけなのでゼロによる除算は不可能です
タイラーダーデン2013年

@Jens Timmerman申し訳ありませんが、divステートメントを追加する前に書きました。本文を更新しました。
タイラーダーデン2013年

1

このリンクによれば、SIGFPE信号をブロックするだけですsigaction()(私は自分で試したことはありませんが、動作するはずです)。

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

ただし、OSは無視されるすべての例外に関与するため、コストがかかります。私は、あなたが無視するゼロによる分割ごとに少なくとも千の良い分割があるべきだと思います。例外がそれよりも頻繁である場合、除算の前にすべての値をチェックするよりも、例外を無視することでより多くの費用がかかる可能性があります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.