web-dev-qa-db-ja.com

C#の固定小数点演算?

ここの誰かがc#の固定小数点数学の優れたリソースを知っているかどうか疑問に思っていましたか?私はこのようなものを見てきました( http://2ddev.72dpiarmy.com/viewtopic.php?id=156 )とこれ( 固定小数点演算を行うための最良の方法は何ですか? ? )、そして小数点が本当に固定小数点か実際に浮動小数点かについての多くの議論(更新:レスポンダはそれが間違いなく浮動小数点であることを確認しました)が、コサインの計算などのための確実なC#ライブラリを見たことはありませんとサイン。

私のニーズは単純です-基本的な演算子に加えて、余弦、正弦、arctan2、PIが必要です...私はそれについてだと思います。多分sqrt。私は2D RTSゲームをプログラミングしていますが、これは主に機能していますが、浮動小数点演算(double)を使用するときのユニットの動きは、複数のマシンにわたって時間(10〜30分)で非常にわずかな誤差があり、非同期につながります。これは現在32ビットOSと64ビットOSの間だけであり、すべての32ビットマシンは問題なく同期しているように見えます。これが浮動小数点の問題だと私に思わせます。

私は最初からこの問題の可能性があることを認識していたため、非整数の位置計算の使用を可能な限り制限しましたが、さまざまな速度でスムーズな斜めの動きをするために、ポイント間の角度をラジアンで計算しています。 sinとcosを使用して、動きのxおよびyコンポーネントを取得する。それが主な問題です。また、マシン間の問題を回避するために、おそらく浮動小数点から固定小数点に移動する必要がある、線分交差、線と円の交差、円と長方形の交差などの計算も行っています。

JavaまたはVBまたは他の同等の言語である場合、オープンソースがある場合、おそらく自分の用途に合わせてコードを変換することができます。私にとっての主な優先事項は正確さです。現在のパフォーマンスよりも速度の損失をできるだけ少なくしたいのですが、この固定小数点数学は私にとって非常に新しいものであり、Googleに実用的な情報がほとんどないことに驚いています-ほとんどのものが理論または高密度のC++ヘッダーファイル。

私を正しい方向に向けるためにできることは何でもありがたいです。これが機能するようになった場合は、他のC#プログラマー向けのリソースが利用できるように、まとめた数学関数をオープンソース化する予定です。

更新:私は間違いなくコサイン/サインルックアップテーブルを目的に合わせて機能させることができますが、約64,000x64,000エントリ(yike)のテーブルを生成する必要があるため、arctan2には機能しないと思います。 arctan2のようなものを計算する効率的な方法のプログラムによる説明を知っているなら、それはすばらしいでしょう。私の数学の背景は大丈夫ですが、高度な数式と従来の数学表記はコードに変換するのが非常に困難です。

49
x4000

元の質問のリンクに基づいて、固定小数点構造体について私が思いついたものをここに示しますが、除算と乗算の処理方法に対するいくつかの修正、およびモジュール、比較、シフトなどの追加ロジックも含まれています:

public struct FInt
{
    public long RawValue;
    public const int SHIFT_AMOUNT = 12; //12 is 4096

    public const long One = 1 << SHIFT_AMOUNT;
    public const int OneI = 1 << SHIFT_AMOUNT;
    public static FInt OneF = FInt.Create( 1, true );

    #region Constructors
    public static FInt Create( long StartingRawValue, bool UseMultiple )
    {
        FInt fInt;
        fInt.RawValue = StartingRawValue;
        if ( UseMultiple )
            fInt.RawValue = fInt.RawValue << SHIFT_AMOUNT;
        return fInt;
    }
    public static FInt Create( double DoubleValue )
    {
        FInt fInt;
        DoubleValue *= (double)One;
        fInt.RawValue = (int)Math.Round( DoubleValue );
        return fInt;
    }
    #endregion

    public int IntValue
    {
        get { return (int)( this.RawValue >> SHIFT_AMOUNT ); }
    }

    public int ToInt()
    {
        return (int)( this.RawValue >> SHIFT_AMOUNT );
    }

    public double ToDouble()
    {
        return (double)this.RawValue / (double)One;
    }

    public FInt Inverse
    {
        get { return FInt.Create( -this.RawValue, false ); }
    }

    #region FromParts
    /// <summary>
    /// Create a fixed-int number from parts.  For example, to create 1.5 pass in 1 and 500.
    /// </summary>
    /// <param name="PreDecimal">The number above the decimal.  For 1.5, this would be 1.</param>
    /// <param name="PostDecimal">The number below the decimal, to three digits.  
    /// For 1.5, this would be 500. For 1.005, this would be 5.</param>
    /// <returns>A fixed-int representation of the number parts</returns>
    public static FInt FromParts( int PreDecimal, int PostDecimal )
    {
        FInt f = FInt.Create( PreDecimal, true );
        if ( PostDecimal != 0 )
            f.RawValue += ( FInt.Create( PostDecimal ) / 1000 ).RawValue;

        return f;
    }
    #endregion

    #region *
    public static FInt operator *( FInt one, FInt other )
    {
        FInt fInt;
        fInt.RawValue = ( one.RawValue * other.RawValue ) >> SHIFT_AMOUNT;
        return fInt;
    }

    public static FInt operator *( FInt one, int multi )
    {
        return one * (FInt)multi;
    }

    public static FInt operator *( int multi, FInt one )
    {
        return one * (FInt)multi;
    }
    #endregion

    #region /
    public static FInt operator /( FInt one, FInt other )
    {
        FInt fInt;
        fInt.RawValue = ( one.RawValue << SHIFT_AMOUNT ) / ( other.RawValue );
        return fInt;
    }

    public static FInt operator /( FInt one, int divisor )
    {
        return one / (FInt)divisor;
    }

    public static FInt operator /( int divisor, FInt one )
    {
        return (FInt)divisor / one;
    }
    #endregion

    #region %
    public static FInt operator %( FInt one, FInt other )
    {
        FInt fInt;
        fInt.RawValue = ( one.RawValue ) % ( other.RawValue );
        return fInt;
    }

    public static FInt operator %( FInt one, int divisor )
    {
        return one % (FInt)divisor;
    }

    public static FInt operator %( int divisor, FInt one )
    {
        return (FInt)divisor % one;
    }
    #endregion

    #region +
    public static FInt operator +( FInt one, FInt other )
    {
        FInt fInt;
        fInt.RawValue = one.RawValue + other.RawValue;
        return fInt;
    }

    public static FInt operator +( FInt one, int other )
    {
        return one + (FInt)other;
    }

    public static FInt operator +( int other, FInt one )
    {
        return one + (FInt)other;
    }
    #endregion

    #region -
    public static FInt operator -( FInt one, FInt other )
    {
        FInt fInt;
        fInt.RawValue = one.RawValue - other.RawValue;
        return fInt;
    }

    public static FInt operator -( FInt one, int other )
    {
        return one - (FInt)other;
    }

    public static FInt operator -( int other, FInt one )
    {
        return (FInt)other - one;
    }
    #endregion

    #region ==
    public static bool operator ==( FInt one, FInt other )
    {
        return one.RawValue == other.RawValue;
    }

    public static bool operator ==( FInt one, int other )
    {
        return one == (FInt)other;
    }

    public static bool operator ==( int other, FInt one )
    {
        return (FInt)other == one;
    }
    #endregion

    #region !=
    public static bool operator !=( FInt one, FInt other )
    {
        return one.RawValue != other.RawValue;
    }

    public static bool operator !=( FInt one, int other )
    {
        return one != (FInt)other;
    }

    public static bool operator !=( int other, FInt one )
    {
        return (FInt)other != one;
    }
    #endregion

    #region >=
    public static bool operator >=( FInt one, FInt other )
    {
        return one.RawValue >= other.RawValue;
    }

    public static bool operator >=( FInt one, int other )
    {
        return one >= (FInt)other;
    }

    public static bool operator >=( int other, FInt one )
    {
        return (FInt)other >= one;
    }
    #endregion

    #region <=
    public static bool operator <=( FInt one, FInt other )
    {
        return one.RawValue <= other.RawValue;
    }

    public static bool operator <=( FInt one, int other )
    {
        return one <= (FInt)other;
    }

    public static bool operator <=( int other, FInt one )
    {
        return (FInt)other <= one;
    }
    #endregion

    #region >
    public static bool operator >( FInt one, FInt other )
    {
        return one.RawValue > other.RawValue;
    }

    public static bool operator >( FInt one, int other )
    {
        return one > (FInt)other;
    }

    public static bool operator >( int other, FInt one )
    {
        return (FInt)other > one;
    }
    #endregion

    #region <
    public static bool operator <( FInt one, FInt other )
    {
        return one.RawValue < other.RawValue;
    }

    public static bool operator <( FInt one, int other )
    {
        return one < (FInt)other;
    }

    public static bool operator <( int other, FInt one )
    {
        return (FInt)other < one;
    }
    #endregion

    public static explicit operator int( FInt src )
    {
        return (int)( src.RawValue >> SHIFT_AMOUNT );
    }

    public static explicit operator FInt( int src )
    {
        return FInt.Create( src, true );
    }

    public static explicit operator FInt( long src )
    {
        return FInt.Create( src, true );
    }

    public static explicit operator FInt( ulong src )
    {
        return FInt.Create( (long)src, true );
    }

    public static FInt operator <<( FInt one, int Amount )
    {
        return FInt.Create( one.RawValue << Amount, false );
    }

    public static FInt operator >>( FInt one, int Amount )
    {
        return FInt.Create( one.RawValue >> Amount, false );
    }

    public override bool Equals( object obj )
    {
        if ( obj is FInt )
            return ( (FInt)obj ).RawValue == this.RawValue;
        else
            return false;
    }

    public override int GetHashCode()
    {
        return RawValue.GetHashCode();
    }

    public override string ToString()
    {
        return this.RawValue.ToString();
    }
}

public struct FPoint
{
    public FInt X;
    public FInt Y;

    public static FPoint Create( FInt X, FInt Y )
    {
        FPoint fp;
        fp.X = X;
        fp.Y = Y;
        return fp;
    }

    public static FPoint FromPoint( Point p )
    {
        FPoint f;
        f.X = (FInt)p.X;
        f.Y = (FInt)p.Y;
        return f;
    }

    public static Point ToPoint( FPoint f )
    {
        return new Point( f.X.IntValue, f.Y.IntValue );
    }

    #region Vector Operations
    public static FPoint VectorAdd( FPoint F1, FPoint F2 )
    {
        FPoint result;
        result.X = F1.X + F2.X;
        result.Y = F1.Y + F2.Y;
        return result;
    }

    public static FPoint VectorSubtract( FPoint F1, FPoint F2 )
    {
        FPoint result;
        result.X = F1.X - F2.X;
        result.Y = F1.Y - F2.Y;
        return result;
    }

    public static FPoint VectorDivide( FPoint F1, int Divisor )
    {
        FPoint result;
        result.X = F1.X / Divisor;
        result.Y = F1.Y / Divisor;
        return result;
    }
    #endregion
}

ShuggyCoUkからのコメントに基づくと、これはQ12形式であることがわかります。それは私の目的にはかなり正確です。もちろん、バグ修正を除いて、質問する前にすでにこの基本的なフォーマットがありました。私が探していたのは、このような構造を使用してCqでSqrt、Atan2、Sin、Cosを計算する方法でした。これを処理するC#で私が知っている他のことはありませんが、Javaには、Onno Hommesによって MathFP ライブラリを見つけることができました。それはリベラルなソースソフトウェアライセンスなので、私は彼の機能の一部をC#で私の目的に変換しました(atan2への修正を含むと思います)。

    #region PI, DoublePI
    public static FInt PI = FInt.Create( 12868, false ); //PI x 2^12
    public static FInt TwoPIF = PI * 2; //radian equivalent of 260 degrees
    public static FInt PIOver180F = PI / (FInt)180; //PI / 180
    #endregion

    #region Sqrt
    public static FInt Sqrt( FInt f, int NumberOfIterations )
    {
        if ( f.RawValue < 0 ) //NaN in Math.Sqrt
            throw new ArithmeticException( "Input Error" );
        if ( f.RawValue == 0 )
            return (FInt)0;
        FInt k = f + FInt.OneF >> 1;
        for ( int i = 0; i < NumberOfIterations; i++ )
            k = ( k + ( f / k ) ) >> 1;

        if ( k.RawValue < 0 )
            throw new ArithmeticException( "Overflow" );
        else
            return k;
    }

    public static FInt Sqrt( FInt f )
    {
        byte numberOfIterations = 8;
        if ( f.RawValue > 0x64000 )
            numberOfIterations = 12;
        if ( f.RawValue > 0x3e8000 )
            numberOfIterations = 16;
        return Sqrt( f, numberOfIterations );
    }
    #endregion

    #region Sin
    public static FInt Sin( FInt i )
    {
        FInt j = (FInt)0;
        for ( ; i < 0; i += FInt.Create( 25736, false ) ) ;
        if ( i > FInt.Create( 25736, false ) )
            i %= FInt.Create( 25736, false );
        FInt k = ( i * FInt.Create( 10, false ) ) / FInt.Create( 714, false );
        if ( i != 0 && i != FInt.Create( 6434, false ) && i != FInt.Create( 12868, false ) && 
            i != FInt.Create( 19302, false ) && i != FInt.Create( 25736, false ) )
            j = ( i * FInt.Create( 100, false ) ) / FInt.Create( 714, false ) - k * FInt.Create( 10, false );
        if ( k <= FInt.Create( 90, false ) )
            return sin_lookup( k, j );
        if ( k <= FInt.Create( 180, false ) )
            return sin_lookup( FInt.Create( 180, false ) - k, j );
        if ( k <= FInt.Create( 270, false ) )
            return sin_lookup( k - FInt.Create( 180, false ), j ).Inverse;
        else
            return sin_lookup( FInt.Create( 360, false ) - k, j ).Inverse;
    }

    private static FInt sin_lookup( FInt i, FInt j )
    {
        if ( j > 0 && j < FInt.Create( 10, false ) && i < FInt.Create( 90, false ) )
            return FInt.Create( SIN_TABLE[i.RawValue], false ) + 
                ( ( FInt.Create( SIN_TABLE[i.RawValue + 1], false ) - FInt.Create( SIN_TABLE[i.RawValue], false ) ) / 
                FInt.Create( 10, false ) ) * j;
        else
            return FInt.Create( SIN_TABLE[i.RawValue], false );
    }

    private static int[] SIN_TABLE = {
        0, 71, 142, 214, 285, 357, 428, 499, 570, 641, 
        711, 781, 851, 921, 990, 1060, 1128, 1197, 1265, 1333, 
        1400, 1468, 1534, 1600, 1665, 1730, 1795, 1859, 1922, 1985, 
        2048, 2109, 2170, 2230, 2290, 2349, 2407, 2464, 2521, 2577, 
        2632, 2686, 2740, 2793, 2845, 2896, 2946, 2995, 3043, 3091, 
        3137, 3183, 3227, 3271, 3313, 3355, 3395, 3434, 3473, 3510, 
        3547, 3582, 3616, 3649, 3681, 3712, 3741, 3770, 3797, 3823, 
        3849, 3872, 3895, 3917, 3937, 3956, 3974, 3991, 4006, 4020, 
        4033, 4045, 4056, 4065, 4073, 4080, 4086, 4090, 4093, 4095, 
        4096
    };
    #endregion

    private static FInt mul( FInt F1, FInt F2 )
    {
        return F1 * F2;
    }

    #region Cos, Tan, Asin
    public static FInt Cos( FInt i )
    {
        return Sin( i + FInt.Create( 6435, false ) );
    }

    public static FInt Tan( FInt i )
    {
        return Sin( i ) / Cos( i );
    }

    public static FInt Asin( FInt F )
    {
        bool isNegative = F < 0;
        F = Abs( F );

        if ( F > FInt.OneF )
            throw new ArithmeticException( "Bad Asin Input:" + F.ToDouble() );

        FInt f1 = mul( mul( mul( mul( FInt.Create( 145103 >> FInt.SHIFT_AMOUNT, false ), F ) -
            FInt.Create( 599880 >> FInt.SHIFT_AMOUNT, false ), F ) +
            FInt.Create( 1420468 >> FInt.SHIFT_AMOUNT, false ), F ) -
            FInt.Create( 3592413 >> FInt.SHIFT_AMOUNT, false ), F ) +
            FInt.Create( 26353447 >> FInt.SHIFT_AMOUNT, false );
        FInt f2 = PI / FInt.Create( 2, true ) - ( Sqrt( FInt.OneF - F ) * f1 );

        return isNegative ? f2.Inverse : f2;
    }
    #endregion

    #region ATan, ATan2
    public static FInt Atan( FInt F )
    {
        return Asin( F / Sqrt( FInt.OneF + ( F * F ) ) );
    }

    public static FInt Atan2( FInt F1, FInt F2 )
    {
        if ( F2.RawValue == 0 && F1.RawValue == 0 )
            return (FInt)0;

        FInt result = (FInt)0;
        if ( F2 > 0 )
            result = Atan( F1 / F2 );
        else if ( F2 < 0 )
        {
            if ( F1 >= 0 )
                result = ( PI - Atan( Abs( F1 / F2 ) ) );
            else
                result = ( PI - Atan( Abs( F1 / F2 ) ) ).Inverse;
        }
        else
            result = ( F1 >= 0 ? PI : PI.Inverse ) / FInt.Create( 2, true );

        return result;
    }
    #endregion

    #region Abs
    public static FInt Abs( FInt F )
    {
        if ( F < 0 )
            return F.Inverse;
        else
            return F;
    }
    #endregion

Dr. HommesのMathFPライブラリには他にも多くの関数がありますが、それらは私が必要とするものを超えていたので、時間をかけてC#に変換していませんでした(そのプロセスは、彼が使用していたために非常に困難になりました)長く、私はFInt構造体を使用しています。これにより、変換ルールをすぐに確認するのが少し難しくなります)。

ここにコード化されているこれらの関数の精度は、私の目的には十分ですが、さらに必要な場合は、FIntのSHIFT AMOUNTを増やすことができます。その場合、オムズ博士の関数の定数を4096で除算し、新しいSHIFT AMOUNTで必要なものを掛ける必要があることに注意してください。これを行うといくつかのバグに遭遇する可能性が高く、注意しないと、組み込みのMath関数に対してチェックを実行して、定数を誤って調整することによって結果が延期されていないことを確認してください。

これまでのところ、このFIntロジックは、同等の組み込み.net関数よりも、おそらく少し高速ではないにしても高速であるように見えます。 fpコプロセッサーがそれを決定するため、それはマシンによって明らかに異なります。そのため、特定のベンチマークを実行していません。しかし、それらは現在私のゲームに統合されており、以前と比べてプロセッサーの使用率がわずかに減少しました(これはQ6600クアッドコア上にあり、平均で使用率が約1%低下しています)。

あなたの助けにコメントしてくれた皆に再び感謝します。誰も私が探していたものを直接私に指摘しませんでしたが、あなたは私にそれをグーグルで見つけるのに役立ついくつかの手がかりをくれました。公に投稿されたC#には同等のコードがないため、このコードが他の誰かに役立つことがわかったと思います。

57
x4000

たとえば1/1000スケールで64ビット整数を使用します。通常どおりに加算および減算できます。整数を乗算してから1000で除算する必要がある場合。sqrt、sin、cosなどを必要とする場合は、long doubleに変換し、1000で除算して、sqrtで除算し、1000で乗算して、整数に変換します。その場合、マシン間の違いは問題になりません。

より高速な除算には別のスケールを使用できます。たとえば、1024をx/1024 == x >> 10

5
Tometzky

私はこのスレッドが少し古いことを知っていますが、記録のために、C#で固定小数点演算を実装するプロジェクトへのリンクを示します。 http://www.isquaredsoftware.com/XrossOneGDIPlus.php

4
metator

C#で固定小数点Q31.32型を実装しました。すべての基本的な演算、sqrt、sin、cos、tanを実行し、単体テストで十分にカバーされています。あなたはそれを見つけることができます ここ 、興味深いタイプはFix64です。 :

ライブラリにはFix32、Fix16、Fix8タイプも含まれていますが、これらは主に実験用であり、完全でバグのないものではありません。

4
Asik

同様の固定小数点構造体を作成しました。 new()を使用すると、構造体を使用している場合でもデータがヒープに書き込まれるため、パフォーマンスが低下します。 Google(C#Heap(ing)対Stack(ing)in .NET:Part I)を参照してください。構造体を使用する本当の力は、newを使用せず、値でスタックに渡す能力です。以下の私の例では、スタックで次のことを行います。 1.スタック上の[result int] 2.スタック3上の[a int] 3.スタック4上の[b int] 4.スタック5上の[*]演算子5.値の結果は、ヒープ割り当てコストを返しませんでした。

    public static Num operator *(Num a, Num b)
    {
        Num result;
        result.NumValue = a.NumValue * b.NumValue;
        return result;
    }
3
user303202

スケーリングされた整数の他に、通常「BigRational」タイプを含むいくつかの任意精度数値ライブラリがあり、固定小数点は10分母の固定べき乗にすぎません。

3
Richard