概要:
最速の計算方法を探しています
_(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ビットです。
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
は、述部の実行により完全に実装されます。
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には他のチェックと比較して欠点はなく、ゼロがより一般的になるため、利点は維持されます。
ただし、コンパイラと代表的なサンプルデータで何が起こるかを実際に測定する必要があります。
プラットフォームを知らないと、正確な最も効率的な方法を知る方法はありませんが、一般的なシステムでは、これは最適に近い場合があります(Intelアセンブラ構文を使用):
(除数はecx
にあり、被除数はeax
にあると仮定します)
mov ebx, ecx
neg ebx
sbb ebx, ebx
add ecx, ebx
div eax, ecx
4つの分岐していない、単一サイクル命令と除算。商はeax
に、残りはedx
に終わります。 (この種のことは、コンパイラーを送って人間の仕事をしたくない理由を示しています)。
この link によれば、sigaction()
でSIGFPEシグナルをブロックすることができます(自分で試したことはありませんが、動作するはずです)。
これは、ゼロ除算エラーが非常にまれな場合に可能な最速のアプローチです。有効な除算ではなくゼロ除算に対してのみ支払い、通常の実行パスはまったく変更されません。
ただし、OSは無視されるすべての例外に関与するため、コストがかかります。私は、あなたが無視するゼロによる部門ごとに少なくとも千の良い部門を持つべきだと思います。例外がそれよりも頻繁に発生する場合は、除算の前にすべての値をチェックするよりも、例外を無視する方が多くのお金を払うでしょう。