Подтвердить что ты не робот

Математика с фиксированной точкой в ​​С#?

Мне было интересно, знает ли кто-нибудь о хороших ресурсах для математики с фиксированной точкой в ​​С#? Я видел такие вещи (http://2ddev.72dpiarmy.com/viewtopic.php?id=156), и это (Каков наилучший способ математика с фиксированной запятой?) и ряд обсуждений о том, является ли десятичная точка действительно фиксированной точкой или фактически плавающей точкой (обновление: респонденты подтвердили, что это определенно с плавающей запятой), но я не видел сплошной С# библиотека для таких вещей, как вычисление косинуса и синуса.

Мои потребности просты - мне нужны основные операторы, плюс косинус, синус, arctan2, PI... Я думаю об этом. Может быть, sqrt. Я программирую 2D-игру RTS, в которой я в основном работаю, но движение единицы при использовании математики с плавающей запятой (удваивает) имеет очень малые погрешности с течением времени (10-30 минут) на нескольких машинах, что приводит к десинкам. Это в настоящее время только между 32-битной ОС и 64-разрядной ОС, все 32-битные машины, похоже, остаются в синхронизации без проблем, и это заставляет меня думать, что это проблема с плавающей точкой.

Я знал об этом как возможную проблему с самого начала и поэтому ограничил мое использование нецелочисленной математики положения как можно больше, но для плавного диагонального движения с разной скоростью я вычисляю угол между точками в радианы, затем получая составляющие движения x и y с sin и cos. Это главный вопрос. Я также делаю некоторые вычисления для пересечений сегментов линии, пересечений прямых линий, пересечений с прямой связью и т.д., Которые также, вероятно, должны перемещаться с плавающей точкой на фиксированную точку, чтобы избежать проблем с несколькими машинами.

Если есть что-то открытое в Java или VB или другом сопоставимом языке, я мог бы, вероятно, преобразовать код для моих целей. Главным приоритетом для меня является точность, хотя я бы хотел как можно меньше потери скорости по сравнению с текущей производительностью. Вся эта математическая вещь с фиксированной точкой очень полезна для меня, и я удивлен тем, насколько мало практической информации о ней есть в google - большинство вещей, похоже, являются либо теорией, либо плотными заголовочными файлами С++.

Все, что вы могли бы сделать, чтобы указать мне в правильном направлении, очень ценится; если я смогу получить эту работу, я планирую открыть исходные математические функции, которые я собрал, чтобы был ресурс для других программистов на С#.

UPDATE: я мог бы определенно создать таблицу поиска косинус/синус для моих целей, но я не думаю, что это сработало бы для arctan2, так как мне нужно было бы создать таблицу размером около 64,000x64,000 записей (yikes). Если вы знаете какие-либо программные объяснения эффективных способов вычисления таких вещей, как arctan2, это было бы потрясающе. Мой математический фон в порядке, но передовые формулы и традиционная математическая нотация очень сложны для перевода в код.

4b9b3361

Ответ 1

Хорошо, вот то, что я придумал для структуры с фиксированной точкой, основанной на ссылке в моем исходном вопросе, но также включая некоторые исправления того, как она обрабатывала деление и умножение, и добавила логику для модулей, сравнений, сдвиги и т.д.:

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. Это достаточно точно для моих целей. Конечно, помимо исправлений, у меня уже был этот базовый формат, прежде чем я задал свой вопрос. То, что я искал, было способом вычисления Sqrt, Atan2, Sin и Cos в С# с использованием такой структуры. В С# я не знаю никаких других вещей, которые будут обрабатывать это, но в Java мне удалось найти библиотеку MathFP Onno Hommes. Это бесплатная лицензия на программное обеспечение, поэтому я переработал некоторые из своих функций в моих целях на С# (с исправлением 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

В библиотеке MathFP доктора Хомме есть ряд других функций, но они были выше того, что мне было нужно, поэтому я не нашел времени, чтобы перевести их на С# (этот процесс был осложнен тем фактом, что он использовал длинный, и я использую структуру FInt, что делает правила преобразования немного сложными, чтобы сразу увидеть).

Точность этих функций, поскольку они закодированы здесь, более чем достаточно для моих целей, но если вам нужно больше, вы можете увеличить SHIFT AMOUNT на FInt. Просто имейте в виду, что если вы это сделаете, то константы функций доктора Хоммеса затем должны быть разделены на 4096, а затем умножены на то, что требует ваша новая сумма SHIFT AMOUNT. Вероятно, вы столкнетесь с некоторыми ошибками, если вы это сделаете, и не будете осторожны, поэтому обязательно выполняйте проверки против встроенных математических функций, чтобы убедиться, что ваши результаты не откладываются, если некорректно настроить константу.

До сих пор эта логика FInt выглядела так же быстро, если не чуть быстрее, чем эквивалентная встроенная функция .net. Это, очевидно, будет изменяться машиной, так как сопроцессор fp определит это, поэтому я не запускаю определенные тесты. Но теперь они интегрированы в мою игру, и я видел небольшое снижение загрузки процессора по сравнению с предыдущим (это в четырехъядерном ядре Q6600 - в среднем на 1%).

Еще раз спасибо всем, кто прокомментировал вашу помощь. Никто не указал мне прямо на то, что я искал, но вы дали мне некоторые подсказки, которые помогли мне найти его на Google. Я надеюсь, что этот код окажется полезным для кого-то другого, так как, похоже, ничего не сравнимо с С# публично.

Ответ 2

Используйте 64-битные целые числа, например, шкалу 1/1000. Вы можете добавлять и вычитать обычно. Когда вам нужно умножить, то умножьте целые числа, а затем разделите их на 1000. Когда вам нужно sqrt, sin, cos и т.д., Тогда конвертируйте в long double, разделите на 1000, sqrt, умножьте на 1000, конвертируйте в integer. Различия между машинами не должны иметь значения.

Вы можете использовать другой масштаб для более быстрых делений, например 1024 в качестве x/1024 == x >> 10.

Ответ 3

Я знаю, что этот поток немного устарел, но для записи здесь ссылка на проект, который реализует математику с фиксированной точкой в ​​С#: http://www.isquaredsoftware.com/XrossOneGDIPlus.php

Ответ 4

Я реализовал в С# тип Q31.32 с фиксированной точкой. Он выполняет всю базовую арифметику, sqrt, sin, cos, tan и хорошо освещен модульными тестами. Вы можете найти здесь, интересным типом является Fix64.:

Обратите внимание, что библиотека также включает типы Fix32, Fix16 и Fix8, но они были в основном для экспериментов и не были такими полными и без ошибок.

Ответ 5

Как и масштабированные целые числа, существует несколько произвольных числовых числовых библиотек, которые обычно включают тип "BigRational", а фиксированная точка - это фиксированная мощность десяти знаменателей.

Ответ 6

Я создал аналогичную структуру фиксированной точки. Вы получаете удар производительности, используя new(), потому что он помещает данные в кучу, даже если вы используете структуру. См. Google (С# Heap (ing) Vs Stack (ing) в .NET: часть I), истинная сила использования struct - это неспособность не использовать новые и передавать по значению стек. Мой пример ниже делает следующее в стеке. 1. [результат int] в стеке 2. [a int] в стеке 3. [b int] в стеке 4. Оператор [*] в стеке 5. Результат результата не возвратил никаких затрат на кучу.

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