13

.NET で float のパフォーマンスをテストしているときに、奇妙なケースに出くわしました。特定の値では、乗算が通常よりもかなり遅くなるようです。テストケースは次のとおりです。

using System;
using System.Diagnostics;

namespace NumericPerfTestCSharp {
    class Program {
        static void Main() {
            Benchmark(() => float32Multiply(0.1f), "\nfloat32Multiply(0.1f)");
            Benchmark(() => float32Multiply(0.9f), "\nfloat32Multiply(0.9f)");
            Benchmark(() => float32Multiply(0.99f), "\nfloat32Multiply(0.99f)");
            Benchmark(() => float32Multiply(0.999f), "\nfloat32Multiply(0.999f)");
            Benchmark(() => float32Multiply(1f), "\nfloat32Multiply(1f)");
        }

        static void float32Multiply(float param) {
            float n = 1000f;
            for (int i = 0; i < 1000000; ++i) {
                n = n * param;
            }
            // Write result to prevent the compiler from optimizing the entire method away
            Console.Write(n);
        }

        static void Benchmark(Action func, string message) {
            // warm-up call
            func();

            var sw = Stopwatch.StartNew();
            for (int i = 0; i < 5; ++i) {
                func();
            }
            Console.WriteLine(message + " : {0} ms", sw.ElapsedMilliseconds);
        }
    }
}

結果:

float32Multiply(0.1f) : 7 ms
float32Multiply(0.9f) : 946 ms
float32Multiply(0.99f) : 8 ms
float32Multiply(0.999f) : 7 ms
float32Multiply(1f) : 7 ms

param = 0.9f で結果が大きく異なるのはなぜですか?

テスト パラメーター: .NET 4.5、リリース ビルド、コードの最適化がオン、x86、デバッガーが接続されていません。

4

2 に答える 2

6

他の人が述べているように、さまざまなプロセッサは、非正規化浮動小数点値が含まれる場合、通常速度の計算をサポートしていません。これは、設計上の欠陥(動作がアプリケーションを損なうか、その他の面倒な場合)または機能(この作業にゲートを使用しないことで可能になった、より安価なプロセッサまたはシリコンの代替使用を好む場合)のいずれかです。

.5に遷移がある理由を理解することは光ります。

pを掛けているとしましょう。最終的に、値が非常に小さくなるため、結果は非正規化数になります(32ビットIEEEバイナリ浮動小数点では2〜126未満)。すると掛け算が遅くなります。乗算を続けると、値は減少し続け、2 -149に達します。これは、表現できる最小の正の数です。ここで、 pを掛けると、正確な結果はもちろん2 -149 pになります。これは、0から2 -149の間にあり、最も近い2つの表現可能な値です。マシンは結果を丸めて、これら2つの値のいずれかを返す必要があります。

どれ?pが1/2未満の場合、2 -149pは2-149より0に近いため、マシンは0を返します。これで、正常以下の値を処理しなくなり、乗算が再び高速になります。pが1/2より大きい場合、2 -149pは0よりも2-149に近いため、マシンは2 -149を返し、通常以下の値で作業を続け、乗算は遅いままです。pが正確に1/2の場合、丸め規則は、仮数(小数部)の下位ビットにゼロがある値、つまりゼロ(2 -149の下位ビットに1がある)を使用するように指示します。

.99fが速く表示されると報告しました。これは遅い動作で終了するはずです。おそらく、投稿したコードは、.99fで高速パフォーマンスを測定したコードと正確には一致しませんか?おそらく、開始値または反復回数が変更されましたか?

この問題を回避する方法があります。1つは、ハードウェアに、使用または取得された非正規化数をゼロに変更するように指定するモード設定があることです。これは、「非正規化数ゼロ」または「ゼロにフラッシュ」モードと呼ばれます。私は.NETを使用しておらず、.NETでこれらのモードを設定する方法についてアドバイスすることはできません。

別のアプローチは、次のような小さな値を毎回追加することです。

n = (n+e) * param;

ここで、eは少なくとも2-126 /paramです。2 -126 /は、非正規値を生成しないほど十分に大きいことが保証されない限り、切り上げてparam計算する必要があることに注意してください。これはまた、否定的ではないと推定します。これの効果は、計算された値が常に正常範囲内にあるのに十分な大きさであり、正常以下になることはないことを確認することです。n(n+e) * paramn

eもちろん、この方法で追加すると、結果が変わります。ただし、たとえば、何らかのエコー効果(または他のフィルター)を使用してオーディオを処理している場合、の値eは小さすぎて、オーディオを聞いている人間が観察できる効果を引き起こすことはできません。オーディオを生成するときにハードウェアの動作に変更を加えるには小さすぎる可能性があります。

于 2012-12-20T14:47:58.073 に答える
2

これは、デノーマル値 (~ 1e-38 より小さい fp 値) とそれらの処理に関連するコストに関係があると思われます。

デノーマル値をテストしてそれらを削除すると、健全性が回復します。

    static void float32Multiply(float param) {
        float n = 1000f;
        int zeroCount=0;
        for (int i = 0; i < 1000000; ++i) {
            n = n * param;
            if(n<1e-38)n=0;
        }
        // Write result to prevent the compiler from optimizing the entire method away
        Console.Write(n);
    }
于 2012-12-20T03:43:23.137 に答える