web-dev-qa-db-fra.com

Mathématiques à virgule fixe en c #?

Je me demandais si quelqu'un ici connaissait de bonnes ressources pour les mathématiques à virgule fixe en c #? J'ai vu des choses comme ça ( http://2ddev.72dpiarmy.com/viewtopic.php?id=156 ) et cela ( Quelle est la meilleure façon de faire des mathématiques à virgule fixe ? ), et un certain nombre de discussions pour savoir si la décimale est vraiment virgule fixe ou réellement flottante (mise à jour: les répondants ont confirmé qu'il s'agit bien de virgule flottante), mais je n'ai pas vu de bibliothèque C # solide pour des choses comme le calcul du cosinus et sinus.

Mes besoins sont simples - j'ai besoin des opérateurs de base, plus cosinus, sinus, arctan2, PI ... Je pense que c'est à peu près tout. Peut-être sqrt. Je programme un jeu RTS 2D, sur lequel je travaille en grande partie, mais le mouvement de l'unité lors de l'utilisation des mathématiques à virgule flottante (doubles) présente de très petites imprécisions dans le temps (10-30 minutes) sur plusieurs machines, ce qui conduit à des désynchronisations. Il s'agit actuellement uniquement entre un système d'exploitation 32 bits et un système d'exploitation 64 bits, toutes les machines 32 bits semblent rester synchronisées sans problème, ce qui me fait penser qu'il s'agit d'un problème de virgule flottante.

J'étais conscient de cela comme un problème possible dès le départ, et j'ai donc limité autant que possible mon utilisation des mathématiques de position non entières, mais pour un mouvement diagonal lisse à des vitesses variables, je calcule l'angle entre les points en radians, puis obtenir les composantes x et y du mouvement avec sin et cos. Voilà le principal problème. Je fais également des calculs pour les intersections de segments de ligne, les intersections de ligne-cercle, les intersections de cercle-rect, etc., qui doivent également probablement passer de virgule flottante à virgule fixe pour éviter les problèmes entre machines.

S'il y a quelque chose d'open source dans Java ou VB ou un autre langage comparable, je pourrais probablement convertir le code pour mes utilisations. La priorité principale pour moi est la précision, bien que j'aimerais aussi peu de perte de vitesse que les performances actuelles. Tout ce truc mathématique à virgule fixe est très nouveau pour moi, et je suis surpris par le peu d'informations pratiques à ce sujet sur Google - la plupart des choses semblent être soit des fichiers d'en-tête théoriques ou denses C++.

Tout ce que vous pourriez faire pour m'orienter dans la bonne direction est très apprécié; si je peux faire fonctionner cela, je prévois d'ouvrir les fonctions mathématiques que j'ai rassemblées afin qu'il y ait une ressource pour d'autres programmeurs C #.

MISE À JOUR: Je pourrais certainement faire fonctionner une table de recherche cosinus/sinus pour mes besoins, mais je ne pense pas que cela fonctionnerait pour arctan2, car j'aurais besoin de générer une table avec environ 64 000 x 64 000 entrées (yikes). Si vous connaissez des explications programmatiques sur des moyens efficaces de calculer des choses comme arctan2, ce serait génial. Mes connaissances en mathématiques sont bonnes, mais les formules avancées et la notation mathématique traditionnelle sont très difficiles à traduire en code.

49
x4000

Ok, voici ce que j'ai trouvé pour une structure à virgule fixe, basée sur le lien dans ma question d'origine, mais également en incluant quelques correctifs sur la façon dont il gérait la division et la multiplication, et une logique supplémentaire pour les modules, les comparaisons, les décalages, etc. :

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
}

Sur la base des commentaires de ShuggyCoUk, je vois que c'est au format Q12. C'est assez précis pour mes besoins. Bien sûr, à part les corrections de bugs, j'avais déjà ce format de base avant de poser ma question. Ce que je cherchais, c'était des moyens de calculer Sqrt, Atan2, Sin et Cos en C # en utilisant une structure comme celle-ci. Il n'y a pas d'autre chose que je sache en C # qui va gérer cela, mais dans Java j'ai réussi à trouver la bibliothèque MathFP par Onno Hommes. C'est un licence de logiciel source libérale, j'ai donc converti certaines de ses fonctions à mes fins en C # (avec un correctif pour atan2, je pense).

    #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

Il y a un certain nombre d'autres fonctions dans la bibliothèque MathFP du Dr Hommes, mais elles étaient au-delà de ce dont j'avais besoin, et donc je n'ai pas pris le temps de les traduire en C # (ce processus a été rendu très difficile par le fait qu'il utilisait un long, et j'utilise la structure FInt, ce qui rend les règles de conversion un peu difficiles à voir immédiatement).

La précision de ces fonctions telles qu'elles sont codées ici est plus que suffisante pour mes besoins, mais si vous en avez besoin de plus, vous pouvez augmenter le SHIFT AMOUNT sur FInt. Sachez simplement que si vous le faites, les constantes des fonctions du Dr Hommes devront alors être divisées par 4096, puis multipliées par tout ce que votre nouveau SHIFT AMOUNT requiert. Vous risquez de rencontrer des bogues si vous le faites et ne faites pas attention, alors assurez-vous d'exécuter des vérifications par rapport aux fonctions mathématiques intégrées pour vous assurer que vos résultats ne sont pas repoussés en ajustant incorrectement une constante.

Jusqu'à présent, cette logique FInt semble aussi rapide, sinon peut-être un peu plus rapide, que les fonctions .net intégrées équivalentes. Cela varierait évidemment selon la machine, puisque le coprocesseur fp le déterminerait, donc je n'ai pas exécuté de tests de référence spécifiques. Mais ils sont maintenant intégrés à mon jeu, et j'ai vu une légère diminution de l'utilisation du processeur par rapport à avant (c'est sur un quad core Q6600 - environ une baisse de 1% en moyenne).

Merci encore à tous ceux qui ont commenté votre aide. Personne ne m'a indiqué directement ce que je cherchais, mais vous m'avez donné des indices qui m'ont aidé à le trouver moi-même sur Google. J'espère que ce code s'avère utile pour quelqu'un d'autre, car il ne semble rien de comparable en C # publié publiquement.

57
x4000

Utilisez des entiers 64 bits par exemple à l'échelle 1/1000. Vous pouvez ajouter et soustraire normalement. Lorsque vous devez multiplier, multipliez les entiers, puis divisez par 1000. Lorsque vous avez besoin de sqrt, sin, cos etc., puis convertissez en long double, divisez par 1000, sqrt, multipliez par 1000, convertissez en entier. Les différences entre les machines ne devraient alors pas avoir d'importance.

Vous pouvez utiliser une autre échelle pour des divisions plus rapides, par exemple 1024 comme x/1024 == x >> 10.

5
Tometzky

Je sais que ce fil est un peu ancien, mais pour mémoire, voici un lien vers un projet qui implémente les mathématiques à virgule fixe en C #: http://www.isquaredsoftware.com/XrossOneGDIPlus.php

4
metator

J'ai implémenté un type Q31.32 à virgule fixe en C #. Il effectue toutes les opérations arithmétiques de base, sqrt, sin, cos, tan et est bien couvert par les tests unitaires. Vous pouvez le trouver ici , le type intéressant est Fix64. :

Notez que la bibliothèque comprend également les types Fix32, Fix16 et Fix8, mais ceux-ci étaient principalement destinés à l'expérimentation et ne sont pas aussi complets et exempts de bogues.

4
Asik

J'ai créé une structure de point fixe similaire. Vous obtenez un hit de performance en utilisant new () car il place des données sur le tas même si vous utilisez une structure. Voir Google (C # Heap (ing) Vs Stack (ing) dans .NET: Part I) la vraie puissance de l'utilisation de struct est la capacité de ne pas utiliser new et de passer par valeur à la pile. Mon exemple ci-dessous fait ce qui suit sur la pile. 1. [result int] sur la pile 2. [a int] sur la pile 3. [b int] sur la pile 4. [*] opérateur sur la pile 5. value result n'a renvoyé aucun coût alloué en tas.

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

En plus des entiers mis à l'échelle, il existe quelques bibliothèques numériques de précision arbitraire qui incluent généralement un type "BigRational", et le point fixe n'est qu'une puissance fixe de dix dénominateurs.

3
Richard