web-dev-qa-db-ja.com

定数整数除数を使用した効率的な浮動小数点除算

最近の 質問 、コンパイラが浮動小数点除算を浮動小数点乗算に置き換えることが許可されているかどうか、私はこの質問をするように促されました。

コード変換後の結果が実際の除算演算とビット単位で同一であるという厳しい要件の下では、バイナリIEEE-754算術の場合、これが2の累乗の除数で可能であることを確認するのは簡単です。除算器の逆数が表現可能である限り、除数の逆数を掛けると、除算と同じ結果が得られます。たとえば、0.5による乗算は、2.0による除算を置き換えることができます。

次に、ビットが同一の結果を提供しながら、除算を置き換えるが大幅に高速に実行される短い命令シーケンスを許可すると仮定すると、そのような置換が機能する他の除数について疑問に思います。特に、単純な乗算に加えて、融合された積和演算を許可します。コメントで私は次の関連する論文を指摘しました:

Nicolas Brisebarre、Jean-Michel Muller、およびSaurabh Kumar Raina。除数が事前にわかっている場合、正しく丸められた浮動小数点除算を加速します。IEEETransactionson Computers、Vol。53、No。8、2004年8月、pp。1069- 1072.

論文の著者によって提唱された手法は、除数の逆数yを正規化されたヘッドテールペアz)として事前計算します。h:zl次のように:zh = 1/y、zl = fma(-y、zh、1)/ y。その後、除算q = x/yq = fma(zh、x、zl * x)。この論文は、このアルゴリズムが機能するために除数yが満たさなければならないさまざまな条件を導き出します。すぐにわかるように、このアルゴリズムには、頭と尾の符号が異なると無限大とゼロの問題があります。さらに重要なことに、このアルゴリズムは配信に失敗します。商テールの計算のため、大きさが非常に小さい被除数xの正しい結果zl * x、アンダーフローに悩まされています。

この論文はまた、ピーター・マークスタインがIBMにいたときに開拓した代替のFMAベースの除算アルゴリズムについても言及しています。関連するリファレンスは次のとおりです。

P。W.Markstein。IBMRISCSystem/6000プロセッサーでの初等関数の計算。IBMJournalofResearch&Development、Vol。34、No。1、1990年1月、pp。111-119

Marksteinのアルゴリズムでは、最初に逆数rcを計算し、そこから初期商q = x * rcが形成されます。次に、除算の余りはFMAを使用してr = fma(-y、q、x)であり、改善されたより正確な商は、最終的にq = fma(r、rc、q)として計算されます。

このアルゴリズムには、ゼロまたは無限大であるx(適切な条件付き実行で簡単に回避))の問題もありますが、IEEE-754単精度floatデータを使用した徹底的なテストでは、すべての可能な被除数にわたって正しい商を提供することが示されていますx多くの除数に対してy、これらの多くの小さな整数の中で。このCコードはそれを実装します:

/* precompute reciprocal */
rc = 1.0f / y;

/* compute quotient q=x/y */
q = x * rc;
if ((x != 0) && (!isinf(x))) {
    r = fmaf (-y, q, x);
    q = fmaf (r, rc, q);
}

ほとんどのプロセッサアーキテクチャでは、これは、予測、条件付き移動、または選択タイプの命令のいずれかを使用して、分岐のない一連の命令に変換されるはずです。具体的な例を挙げます。3.0fで除算する場合、CUDA 7.5のnvccコンパイラは、KeplerクラスのGPUに対して次のマシンコードを生成します。

    LDG.E R5, [R2];                        // load x
    FSETP.NEU.AND P0, PT, |R5|, +INF , PT; // pred0 = fabsf(x) != INF
    FMUL32I R2, R5, 0.3333333432674408;    // q = x * (1.0f/3.0f)
    FSETP.NEU.AND P0, PT, R5, RZ, P0;      // pred0 = (x != 0.0f) && (fabsf(x) != INF)
    FMA R5, R2, -3, R5;                    // r = fmaf (q, -3.0f, x);
    MOV R4, R2                             // q
@P0 FFMA R4, R5, c[0x2][0x0], R2;          // if (pred0) q = fmaf (r, (1.0f/3.0f), q)
    ST.E [R6], R4;                         // store q

私の実験では、整数除数を昇順でステップ実行し、それぞれについて上記のコードシーケンスを適切な除算に対して徹底的にテストする、以下に示す小さなCテストプログラムを作成しました。この徹底的なテストに合格した除数のリストを出力します。部分的な出力は次のようになります。

PASS: 1, 2, 3, 4, 5, 7, 8, 9, 11, 13, 15, 16, 17, 19, 21, 23, 25, 27, 29, 31, 32, 33, 35, 37, 39, 41, 43, 45, 47, 49, 51, 53, 55, 57, 59, 61, 63, 64, 65, 67, 69,

最適化として置換アルゴリズムをコンパイラーに組み込むために、上記のコード変換を安全に適用できる除数のホワイトリストは実用的ではありません。これまでのプログラムの出力(1分あたり約1つの結果の割合)は、高速コードが、奇数の整数または2の累乗である除数xyのすべての可能なエンコーディングで正しく機能することを示しています。もちろん、証拠ではなく、事例証拠です。

上記のコードシーケンスへの除算の変換が安全かどうかを事前に判断できる数学的条件のセットは何ですか?回答はすべての浮動小数点を想定できます操作は、デフォルトの丸めモードである「最も近いまたは偶数に丸める」で実行されます。

#include <stdlib.h>
#include <stdio.h>
#include <math.h>

int main (void)
{
    float r, q, x, y, rc;
    volatile union {
        float f;
        unsigned int i;
    } arg, res, ref;
    int err;

    y = 1.0f;
    printf ("PASS: ");
    while (1) {
        /* precompute reciprocal */
        rc = 1.0f / y;

        arg.i = 0x80000000;
        err = 0;
        do {
            /* do the division, fast */
            x = arg.f;
            q = x * rc;
            if ((x != 0) && (!isinf(x))) {
                r = fmaf (-y, q, x);
                q = fmaf (r, rc, q);
            }
            res.f = q;
            /* compute the reference, slowly */
            ref.f = x / y;

            if (res.i != ref.i) {
                err = 1;
                break;
            }
            arg.i--;
        } while (arg.i != 0x80000000);

        if (!err) printf ("%g, ", y);
        y += 1.0f;
    }
    return EXIT_SUCCESS;
}
45
njuffa

3回目の再起動をさせてください。私たちは加速しようとしています

_    q = x / y
_

ここで、yは整数定数であり、qx、およびyはすべて IEEE 754-2008 binary32 float-ポイント値。以下のfmaf(a,b,c)は、binary32値を使用した融合積和_a * b + c_を示します。

素朴なアルゴリズムは、事前に計算された逆数を介して行われます。

_    C = 1.0f / y
_

そのため、実行時に(はるかに高速な)乗算で十分です。

_    q = x * C
_

Brisebarre-Muller-Raina加速は、2つの事前計算された定数を使用します。

_    zh = 1.0f / y
    zl = -fmaf(zh, y, -1.0f) / y
_

そのため、実行時に、1つの乗算と1つの融合積和で十分です。

_    q = fmaf(x, zh, x * zl)
_

Marksteinアルゴリズムは、ナイーブアプローチと2つの融合乗算加算を組み合わせて、ナイーブアプローチが最下位の1単位以内の結果を生成する場合に、事前計算によって正しい結果を生成します。

_    C1 = 1.0f / y
    C2 = -y
_

除算を使用して近似できるように

_    t1 = x * C1
    t2 = fmaf(C1, t1, x)
    q  = fmaf(C2, t2, t1)
_

素朴なアプローチは、2つのyのすべての累乗で機能しますが、それ以外の場合はかなり悪いです。たとえば、除数7、14、15、28、および30の場合、考えられるすべてのxの半分以上に対して誤った結果が生成されます。

Brisebarre-Muller-Rainaアプローチは、2つのyのほとんどすべての非累乗に対して同様に失敗しますが、はるかに少ないxは、誤った結果をもたらします(すべての可能なxyによって異なります)。

Brisebarre-Muller-Rainaの記事は、ナイーブなアプローチの最大誤差が±1.5ULPであることを示しています。

Marksteinアプローチでは、2の累乗yと、奇数の整数yに対して正しい結果が得られます。 (Marksteinアプローチで失敗する奇数の整数除数は見つかりませんでした。)


Marksteinアプローチでは、除数1〜19700( 生データはこちら )を分析しました。

失敗のケースの数(横軸の除数、Marksteinアプローチがその除数に対して失敗するxの値の数)をプロットすると、単純なパターンが発生することがわかります。

Markstein failure cases
(出典: nominal-animal.net

これらのプロットには、水平軸と垂直軸の両方が対数であることに注意してください。このアプローチでは、テストしたすべての奇数除数に対して正しい結果が得られるため、奇数除数のドットはありません。

X軸を除数のビットリバース(逆順の2進数、つまり0b11101101→0b10110111、 data )に変更すると、非常に明確なパターンが得られます。 Markstein failure cases, bit reverse divisor
(出典: nominal-animal.net

ポイントセットの中心を通る直線を描くと、曲線_4194304/x_が得られます。 (プロットは可能なフロートの半分のみを考慮しているため、すべての可能なフロートを考慮する場合は、2倍にすることを忘れないでください。)_8388608/x_および_2097152/x_は、エラーパターン全体を完全に囲みます。

したがって、rev(y)を使用して除数yのビットリバースを計算する場合、8388608/rev(y)は(可能なすべての中で)ケース数の適切な1次近似です。 float)ここで、Marksteinアプローチは、2のべき乗ではない偶数の除数yに対して誤った結果を生成します。 (または、上限は16777216/rev(x)です。)

2016-02-28を追加:任意の整数(binary32)除数を指定して、Marksteinアプローチを使用したエラーケースの数の概算を見つけました。これは擬似コードです。

_function markstein_failure_estimate(divisor):
    if (divisor is zero)
        return no estimate
    if (divisor is not an integer)
        return no estimate

    if (divisor is negative)
        negate divisor

    # Consider, for avoiding underflow cases,
    if (divisor is very large, say 1e+30 or larger)
        return no estimate - do as division

    while (divisor > 16777216)
        divisor = divisor / 2

    if (divisor is a power of two)
        return 0

    if (divisor is odd)
        return 0

    while (divisor is not odd)
        divisor = divisor / 2

    # Use return (1 + 83833608 / divisor) / 2
    # if only nonnegative finite float divisors are counted!
    return 1 + 8388608 / divisor
_

これにより、私がテストしたMarkstein障害のケースで±1以内の正しい誤差推定値が得られます(ただし、8388608より大きい除数はまだ十分にテストしていません)。最終的な分割は、偽のゼロが報告されないようにする必要がありますが、(まだ)保証することはできません。アンダーフローの問題がある非常に大きな除数(たとえば、0x1p100、または1e + 30、およびそれより大きい大きさ)は考慮されません。とにかく、そのような除数を加速から確実に除外します。

予備テストでは、見積もりは驚くほど正確に見えます。除数1から20000の推定値と実際の誤差を比較するプロットは作成しませんでした。これは、ポイントがすべてプロット内で正確に一致しているためです。 (この範囲内では、推定値は正確であるか、大きすぎます。)基本的に、推定値はこの回答の最初のプロットを正確に再現します。


Marksteinアプローチの失敗のパターンは規則的であり、非常に興味深いものです。このアプローチは、2つの除数のすべての累乗、およびすべての奇数の整数の約数に対して機能します。

16777216より大きい除数の場合、16777216未満の値を生成するために2の最小の累乗で除算された除数の場合と同じエラーが一貫して表示されます。たとえば、0x1.3cdfa4p +23および0x1.3cdfa4p + 41、0x1。 d8874p +23および0x1.d8874p + 32、0x1.cf84f8p +23および0x1.cf84f8p + 34、0x1.e4a7fp +23および0x1.e4a7fp + 37。 (各ペア内で、仮数は同じであり、2の累乗のみが異なります。)

私のテストベンチにエラーがないと仮定すると、これは、除数が2の最小の累乗で除算されるようなものである場合、Marksteinアプローチは大きさが16777216より大きい(ただし、たとえば1e + 30より小さい)除数でも機能することを意味します。大きさが16777216未満の商を生成し、商は奇数です。

6
Nominal Animal

この質問は、定数Yの値を特定する方法を求めています。これにより、xのすべての可能な値に対してFMAを使用してx / Yをより安価な計算に安全に変換できます。別のアプローチは、静的分析を使用して、xが取ることができる値の過大近似を決定することです。これにより、変換されたコードが元の除算と異なる値を認識して、一般的に不健全な変換を適用できます。起こらない。

浮動小数点計算の問題にうまく適合した浮動小数点値のセットの表現を使用すると、関数の最初から始まるフォワード分析でさえ、有用な情報を生成できます。例えば:

float f(float z) {
  float x = 1.0f + z;
  float r = x / Y;
  return r;
}

デフォルトの最も近い丸めモード(*)を想定すると、上記の関数では、xはNaN(入力がNaNの場合)、+ 0.0f、または2より大きい数のみになります。-24 大きさは異なりますが、-0.0fまたは2よりゼロに近いものではありません-24。これは、定数Yの多くの値について質問に示されている2つの形式のいずれかに変換することを正当化します。

(*)多くの最適化が不可能であり、プログラムが明示的に#pragma STDC FENV_ACCESS ONを使用しない限り、Cコンパイラーがすでに行っているという仮定


上記のxの情報を予測するフォワード静的分析は、式が次のタプルとして取ることができる浮動小数点値のセットの表現に基づくことができます。

  • 可能なNaN値のセットの表現(NaNの動作は十分に指定されていないため、ブール値のみを使用することを選択します。trueは一部のNaNが存在する可能性があることを意味し、falseはNaNが存在しないことを示します。が存在します。)、
  • + inf、-inf、+ 0.0、-0.0、の存在をそれぞれ示す4つのブールフラグ
  • 負の有限浮動小数点値の包括的間隔、および
  • 正の有限浮動小数点値の包括的間隔。

このアプローチに従うには、Cプログラムで発生する可能性のあるすべての浮動小数点演算を静的アナライザーで理解する必要があります。説明のために、分析されたコードで+を処理するために使用される、値UとVのセット間の加算は、次のように実装できます。

  • NaNがオペランドの1つに存在する場合、またはオペランドが反対の符号の無限大である可能性がある場合、NaNは結果に存在します。
  • Uの値とVの値を加算した結果が0にならない場合は、標準の区間演算を使用します。結果の上限は、Uの最大値とVの最大値を最も近い値に丸めて加算した場合に得られるため、これらの境界は最も近い値で計算する必要があります。
  • 0がUの正の値とVの負の値の加算の結果である可能性がある場合、-MがVに存在するように、MをUの最小の正の値とします。
    • succ(M)がUに存在する場合、この値のペアは、succ(M)-Mを結果の正の値に寄与します。
    • -succ(M)がVに存在する場合、この値のペアは、結果の負の値に負の値M --suc(M)を与えます。
    • pred(M)がUに存在する場合、この値のペアは、結果の負の値に負の値pred(M)-Mを与えます。
    • -pred(M)がVに存在する場合、この値のペアは、値M --pred(M)を結果の正の値に寄与します。
  • 0がUの負の値とVの正の値の加算の結果である可能性がある場合は、同じ作業を行います。

謝辞:上記は、「浮動小数点の加算と減算の制約の改善」、Bruno Marre&ClaudeMichelからのアイデアを借りています。


例:以下の関数fのコンパイル:

float f(float z, float t) {
  float x = 1.0f + z;
  if (x + t == 0.0f) {
    float r = x / 6.0f;
    return r;
  }
  return 0.0f;
}

問題のアプローチでは、関数fの除算を別の形式に変換することを拒否しています。これは、6は除算を無条件に変換できる値の1つではないためです。代わりに、私が提案しているのは、関数の最初から単純な値分析を適用することです。この場合、x+0.0fまたは少なくとも2のいずれかの有限フロートであると判断されます。-24 規模が大きく、この情報を使用してBrisebarre et alの変換を適用するには、x * C2がアンダーフローしないという知識に自信を持ってください。

明確にするために、分割をより単純なものに変換するかどうかを決定するために、以下のようなアルゴリズムを使用することを提案しています。

  1. Yは、Brisebarre et alのアルゴリズムに従って変換できる値の1つですか?
  2. それらの方法からのC1とC2は同じ符号を持っていますか、それとも配当が無限である可能性を排除することは可能ですか?
  3. それらのメソッドのC1とC2は同じ符号を持っていますか、それともxは0の2つの表現のうちの1つだけを取ることができますか? C1とC2の符号が異なり、xがゼロの1つの表現にしかなり得ない場合は、FMAベースの計算の符号をいじって(**)、正しいゼロを生成するようにしてください。 xはゼロです。
  4. 配当の大きさは、x * C2がアンダーフローする可能性を排除するのに十分な大きさであることが保証されますか?

4つの質問に対する答えが「はい」の場合、コンパイルされる関数のコンテキストで、除算を乗算とFMAに変換できます。上記の静的分析は、質問2、3、および4に答えるのに役立ちます。

(**)「記号をいじる」とは、結果を出すために必要な場合に、FMA(C1、x、C2 * x)の代わりに-FMA(-C1、x、(-C2)* x)を使用することを意味します。 xが2つの符号付きゼロのうちの1つにしかなり得ない場合は正しく

7
Pascal Cuoq

私は @ Pascal の答えが大好きですが、最適化では、完全なソリューションよりも、単純でよく理解された変換のサブセットを使用する方がよい場合がよくあります。

現在および一般的なすべての履歴浮動小数点形式には、2進仮数という1つの共通点がありました。

したがって、すべての分数は次の形式の有理数でした。

x/ 2n

これは、次の形式の有理数であるプログラムの定数(およびすべての可能な基数10の分数)とは対照的です。

x/(2n * 5m

したがって、1つの最適化では、m== 0の入力と逆数をテストするだけです。これらの数値は、FPフォーマットとそれらを使用した操作により、フォーマット内で正確な数値が生成されるはずです。

したがって、たとえば、.01から0.99の(10進数の2桁の)範囲内で、次の数値を除算または乗算すると、最適化されます。

.25 .50 .75

そして、他のすべてはそうではありません。 (私は、最初にそれをテストしてください、笑)。

1
DigitalRoss

浮動小数点除算の結果は次のとおりです。

  • サインフラグ
  • 仮数
  • 指数
  • フラグのセット(オーバーフロー、アンダーフロー、不正確など-fenv()を参照)

最初の3つの部分を正しくする(ただし、フラグのセットは正しくない)だけでは不十分です。それ以上の知識がなければ(たとえば、結果のどの部分が実際に重要であるか、配当の可能な値など)、定数による除算を定数による乗算(および/または複雑なFMAの混乱)に置き換えることはほとんどだと思います決して安全ではありません。

加えて;最近のCPUの場合、除算を2つのFMAに置き換えることが常に改善されるとは思いません。たとえば、ボトルネックが命令のフェッチ/デコードである場合、この「最適化」によってパフォーマンスが低下します。別の例として、後続の命令が結果に依存しない場合(CPUは結果を待機している間、他の多くの命令を並行して実行できます)、FMAバージョンは複数の依存関係ストールを導入し、パフォーマンスを低下させる可能性があります。 3番目の例では、すべてのレジスタが使用されている場合、FMAバージョン(追加の「ライブ」変数が必要)によって「スピル」が増加し、パフォーマンスが低下する可能性があります。

(すべてではありませんが)多くの場合、2の定数倍による除算または乗算は、加算のみで実行できます(具体的には、指数にシフトカウントを加算します)。

0
Brendan