С# Десятичный тип данных

Я пишу финансовое приложение на С#, где критическая производительность (то есть скорость). Поскольку это финансовое приложение, я должен интенсивно использовать десятичный тип данных.

Я оптимизировал код настолько, насколько мог, с помощью профилировщика. Прежде чем использовать Decimal, все было сделано с двойным типом данных, и скорость была в несколько раз быстрее. Однако Double не является вариантом из-за его двоичного характера, вызывая много ошибок точности в течение нескольких операций.

Есть ли какая-нибудь десятичная библиотека, с которой я могу взаимодействовать с С#, которая может дать мне улучшение производительности над родным типом данных Decimal в .NET?

Основываясь на ответах, которые я уже получил, я заметил, что я недостаточно ясен, поэтому вот несколько дополнительных деталей:

  • Приложение должно быть таким же быстрым, как и возможно (то есть, как быстро, когда использование Double вместо Decimal будет мечтой). Двойной был примерно в 15 раз быстрее, чем десятичный, поскольку операции основаны на оборудовании.
  • Аппаратное обеспечение уже является первоклассным (я работаю на двухъядерном процессоре Dual Xenon), и приложение использует потоки, поэтому загрузка процессора всегда на 100%. Кроме того, приложение работает в 64-битном режиме, что дает ему надежное преимущество в производительности более 32 бит.
  • Я оптимизировал прошлое с точки зрения здравомыслия (более полутора месяцев, оптимизируя, верьте или нет, теперь требуется около 1/5000 того, что требуется для выполнения тех же вычислений, которые я использовал в качестве ссылки изначально ); эта оптимизация включала в себя все: строчную обработку, ввод-вывод, доступ к базе данных и индексы, память, циклы, изменение способа создания некоторых вещей и даже использование "переключателя" над "если" везде, где это имело значение. Профилировщик теперь ясно показывает, что оставшийся виновник производительности относится к операторам десятичного типа данных. Ничто другое не добавляет значительного количества времени.
  • Вы должны поверить мне здесь: я дошел до того, что смог продвинуться в области С#.NET для оптимизации приложения, и я действительно поражен его текущей производительностью. Теперь я ищу хорошую идею, чтобы улучшить десятичную производительность до уровня, близкого к Double. Я знаю это только сон, но просто хотел проверить, что я думал обо всем, что возможно.:)

Спасибо!

Ответ 1

вы можете использовать длинный тип данных. Конечно, вы не сможете хранить фракции там, но если вы закажете свое приложение, чтобы хранить пенни вместо фунтов, вы будете в порядке. Точность составляет 100% для длинных типов данных, и если вы не работаете с огромными числами (используйте 64-битный длинный тип), вы будете в порядке.

Если вы не можете поручить хранить гроши, тогда заверните целое число в классе и используйте это.

Ответ 2

Вы говорите, что это должно быть быстро, но есть ли у вас конкретные требования к скорости? Если нет, вы можете оптимизировать прошлое с точки зрения здравого смысла:)

Как только кто-то, сидящий рядом со мной, только что предложил, вы можете модернизировать свое оборудование? Это, вероятно, будет дешевле, чем переписывать код.

Наиболее очевидным вариантом является использование целых чисел вместо десятичных знаков - где одна "единица" - это что-то вроде "тысячной доли" (или того, что вы хотите - вы получаете идею). Независимо от того, будет ли это осуществимо или нет, будет зависеть от операций, которые вы выполняете с десятичными значениями для начала. Вы должны быть очень осторожны при обращении с этим - легко ошибиться (по крайней мере, если вы похожи на меня).

Профилировщик показал конкретные горячие точки в вашем приложении, которые вы могли бы оптимизировать индивидуально? Например, если вам нужно выполнить множество вычислений в одной небольшой области кода, вы можете конвертировать из десятичного в целочисленный формат, выполнять вычисления и затем конвертировать назад. Это может сохранить API в терминах десятичных знаков для большей части кода, что вполне может облегчить его поддержку. Однако, если у вас нет выраженных горячих точек, это может оказаться невозможным.

+1 для профилирования и говорит нам, что скорость - это определенное требование, btw:)

Ответ 3

Проблема в том, что double/float поддерживаются в аппаратном обеспечении, а Decimal и т.п. - нет. То есть вы должны выбирать между скоростью + ограниченной точностью и большей точностью + более низкой производительностью.

Ответ 4

Вопрос хорошо обсуждается, но поскольку я давно искал эту проблему, я хотел бы поделиться некоторыми из моих результатов.

Определение проблемы: Известно, что десятичные разряды намного медленнее, чем удваиваются, но финансовые приложения не могут терпеть любые артефакты, возникающие при выполнении вычислений в парном разряде.

Исследование

Моя цель состояла в том, чтобы измерить различные подходы к хранению чисел с плавающей точкой и сделать вывод, который нужно использовать для нашего приложения.

Если было приемлемо использовать Int64 для хранения чисел с плавающей точкой с фиксированной точностью. Множитель 10 ^ 6 давал нам и то, и другое: достаточно цифр для хранения фракций и большого диапазона для хранения больших сумм. Конечно, вы должны быть осторожны при таком подходе (операции умножения и деления могут стать сложными), но мы были готовы и хотели также измерить этот подход. Одна вещь, которую вы должны иметь в виду, кроме возможных ошибок вычисления и переполнения, заключается в том, что обычно вы не можете выставлять эти длинные номера в открытый API. Таким образом, все внутренние вычисления могут выполняться с помощью longs, но прежде чем отправлять номера пользователю, они должны быть преобразованы в нечто более дружелюбное.

Я реализовал простой класс прототипов, который переносит длинное значение в десятичную структуру (называемую ей Money) и добавляет ее к измерениям.

public struct Money : IComparable
{
    private readonly long _value;

    public const long Multiplier = 1000000;
    private const decimal ReverseMultiplier = 0.000001m;

    public Money(long value)
    {
        _value = value;
    }

    public static explicit operator Money(decimal d)
    {
        return new Money(Decimal.ToInt64(d * Multiplier));
    }

    public static implicit operator decimal (Money m)
    {
        return m._value * ReverseMultiplier;
    }

    public static explicit operator Money(double d)
    {
        return new Money(Convert.ToInt64(d * Multiplier));
    }

    public static explicit operator double (Money m)
    {
        return Convert.ToDouble(m._value * ReverseMultiplier);
    }

    public static bool operator ==(Money m1, Money m2)
    {
        return m1._value == m2._value;
    }

    public static bool operator !=(Money m1, Money m2)
    {
        return m1._value != m2._value;
    }

    public static Money operator +(Money d1, Money d2)
    {
        return new Money(d1._value + d2._value);
    }

    public static Money operator -(Money d1, Money d2)
    {
        return new Money(d1._value - d2._value);
    }

    public static Money operator *(Money d1, Money d2)
    {
        return new Money(d1._value * d2._value / Multiplier);
    }

    public static Money operator /(Money d1, Money d2)
    {
        return new Money(d1._value / d2._value * Multiplier);
    }

    public static bool operator <(Money d1, Money d2)
    {
        return d1._value < d2._value;
    }

    public static bool operator <=(Money d1, Money d2)
    {
        return d1._value <= d2._value;
    }

    public static bool operator >(Money d1, Money d2)
    {
        return d1._value > d2._value;
    }

    public static bool operator >=(Money d1, Money d2)
    {
        return d1._value >= d2._value;
    }

    public override bool Equals(object o)
    {
        if (!(o is Money))
            return false;

        return this == (Money)o;
    }

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

    public int CompareTo(object obj)
    {
        if (obj == null)
            return 1;

        if (!(obj is Money))
            throw new ArgumentException("Cannot compare money.");

        Money other = (Money)obj;
        return _value.CompareTo(other._value);
    }

    public override string ToString()
    {
        return ((decimal) this).ToString(CultureInfo.InvariantCulture);
    }
}

Эксперимент

Я измерил следующие операции: сложение, вычитание, умножение, деление, сравнение равенства и сравнительное (большее/меньшее) сравнение. Я выполнял операции по следующим типам: double, long, decimal и Money. Каждая операция выполнялась 1.000.000 раз. Все номера были предварительно выделены в массивы, поэтому вызов пользовательского кода в конструкторах decimal и Money не должен влиять на результаты.

Added moneys in 5.445 ms
Added decimals in 26.23 ms
Added doubles in 2.3925 ms
Added longs in 1.6494 ms

Subtracted moneys in 5.6425 ms
Subtracted decimals in 31.5431 ms
Subtracted doubles in 1.7022 ms
Subtracted longs in 1.7008 ms

Multiplied moneys in 20.4474 ms
Multiplied decimals in 24.9457 ms
Multiplied doubles in 1.6997 ms
Multiplied longs in 1.699 ms

Divided moneys in 15.2841 ms
Divided decimals in 229.7391 ms
Divided doubles in 7.2264 ms
Divided longs in 8.6903 ms

Equility compared moneys in 5.3652 ms
Equility compared decimals in 29.003 ms
Equility compared doubles in 1.727 ms
Equility compared longs in 1.7547 ms

Relationally compared moneys in 9.0285 ms
Relationally compared decimals in 29.2716 ms
Relationally compared doubles in 1.7186 ms
Relationally compared longs in 1.7321 ms

Выводы

  • Сложение, вычитание, умножение, операции сравнения на decimal примерно в 15 раз медленнее операций на long или double; деление ~ 30 раз медленнее.
  • Производительность decimal -подобной обертки лучше производительности decimal, но все же значительно хуже производительности double и long из-за отсутствия поддержки от CLR.
  • Выполнение вычислений на decimal в абсолютных числах довольно быстрое: 40.000.000 операций в секунду.

Совет

  • Если у вас очень тяжелый расчет, используйте десятичные числа. В относительных числах они медленнее, чем длинные и удвоенные, но абсолютные цифры выглядят хорошо.
  • Невозможно повторить реализацию decimal с вашей собственной структурой из-за поддержки поддержки CLR. Вы можете сделать это быстрее, чем decimal, но оно никогда не будет таким быстрым, как double.
  • Если для вашего приложения недостаточно производительности decimal, вам может потребоваться перевести ваши вычисления на long с фиксированной точностью. Прежде чем возвращать результат клиенту, он должен быть преобразован в decimal.

Ответ 5

Я не думаю, что инструкции SSE2 могли бы легко работать с значениями .NET Decimal. Тип данных .NET Decimal 128-битная десятичная плавающая точка тип http://en.wikipedia.org/wiki/Decimal128_floating-point_format, инструкции SSE2 работают с 128 бит целочисленные типы.

Ответ 6

Как насчет MMX/SSE/SSE2?

Я думаю, это поможет... так... десятичный - 128-битный тип данных, а SSE2 - тоже 128 бит... и он может добавлять, под, div, mul decimal в 1 тике процессора...

вы можете написать DLL для SSE2 с помощью VС++, а затем использовать эту DLL в своем приложении

например // вы можете сделать что-то вроде этого

VС++

#include <emmintrin.h>
#include <tmmintrin.h>

extern "C" DllExport __int32* sse2_add(__int32* arr1, __int32* arr2);

extern "C" DllExport __int32* sse2_add(__int32* arr1, __int32* arr2)
{
    __m128i mi1 = _mm_setr_epi32(arr1[0], arr1[1], arr1[2], arr1[3]);
    __m128i mi2 = _mm_setr_epi32(arr2[0], arr2[1], arr2[2], arr2[3]);

    __m128i mi3 = _mm_add_epi32(mi1, mi2);
    __int32 rarr[4] = { mi3.m128i_i32[0], mi3.m128i_i32[1], mi3.m128i_i32[2], mi3.m128i_i32[3] };
    return rarr;
}

С#

[DllImport("sse2.dll")]
private unsafe static extern int[] sse2_add(int[] arr1, int[] arr2);

public unsafe static decimal addDec(decimal d1, decimal d2)
{
    int[] arr1 = decimal.GetBits(d1);
    int[] arr2 = decimal.GetBits(d2);

    int[] resultArr = sse2_add(arr1, arr2);

    return new decimal(resultArr);
}

Ответ 7

Старый вопрос, все еще очень действительный.

Вот некоторые цифры, чтобы поддержать идею использования Long.

Время, затраченное на выполнение 100 000 000 дополнений

Long     231 mS
Double   286 mS
Decimal 2010 mS

в двух словах, десятичная буква ~ 10 раз медленнее, чем Long или Double.

код:

Sub Main()
    Const TESTS = 100000000
    Dim sw As Stopwatch

    Dim l As Long = 0
    Dim a As Long = 123456
    sw = Stopwatch.StartNew()
    For x As Integer = 1 To TESTS
        l += a
    Next
    Console.WriteLine(String.Format("Long    {0} mS", sw.ElapsedMilliseconds))

    Dim d As Double = 0
    Dim b As Double = 123456
    sw = Stopwatch.StartNew()
    For x As Integer = 1 To TESTS
        d += b
    Next
    Console.WriteLine(String.Format("Double  {0} mS", sw.ElapsedMilliseconds))

    Dim m As Decimal = 0
    Dim c As Decimal = 123456
    sw = Stopwatch.StartNew()
    For x As Integer = 1 To TESTS
        m += c
    Next
    Console.WriteLine(String.Format("Decimal {0} mS", sw.ElapsedMilliseconds))

    Console.WriteLine("Press a key")
    Console.ReadKey()
End Sub

Ответ 8

Я не могу дать комментарий или проголосовать до сих пор, так как я только начал переполнение стека. Мой комментарий к alexsmart (опубликовано 23.12.2008 12:31) заключается в том, что выражение Round (n/precision, precision), где n является int и precices длинным, не будет делать то, что он думает:

1) n/precision вернет целочисленное деление, т.е. оно уже будет округлено, но вы не сможете использовать десятичные числа. Поведение округления также отличается от Math.Round(...).

2) Код "return Math.Round(n/precision, precision).ToString()" не скомпилируется из-за двусмысленности между Math.Round(double, int) и Math.Round(decimal, int). Вам придется отбрасывать до десятичной (не двойной, так как это финансовое приложение), и поэтому в первую очередь может пойти с десятичной запятой.

3) n/precision, где точность 4 не будет усекаться до четырех десятичных знаков, но делит на 4. Например, Math.Round((десятичный) (1234567/4), 4) возвращает 308641. (1234567/4 = 308641,75), а то, что вы, вероятно, хотели сделать, - получить 1235000 (округляется до 4 цифр от задней 567). Обратите внимание, что Math.Round позволяет округлять до фиксированной точки, а не фиксированную точность.

Обновление: теперь я могу добавлять комментарии, но не хватает места, чтобы поместить это в область комментариев.

Ответ 9

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

Ответ 10

Через 4 года после моего предыдущего ответа я хотел бы добавить еще один, основанный на опыте, накопленном нами за годы работы с высокопроизводительными вычислениями с числами с плавающей точкой.

Существует две основные проблемы с типом данных Decimal при высокопроизводительных вычислениях:

  1. CLR рассматривает этот тип как обычную структуру (никакой специальной поддержки, как для других встроенных типов)
  2. В это 128 бит

Хотя вы не можете многое сделать по первому вопросу, второй выглядит еще более важным. Операции с памятью и процессоры чрезвычайно эффективны при работе с 64-битными числами. 128-битные операции намного тяжелее. Таким образом, реализация Decimal в .NET по своей конструкции значительно медленнее, чем в Double, даже для операций чтения/записи.

Если вашему приложению требуется точность вычислений с плавающей запятой и выполнение таких операций, то ни Double, ни Decimal не подходят для этой задачи. Решение, которое мы приняли в моей компании (домен Fintech), заключается в использовании оболочки поверх математической библиотеки десятичных чисел с плавающей запятой Intel®. Он реализует IEEE 754-2008 Decimal Floating-Point Arithmetic specification, обеспечивающий 64-битные десятичные числа с плавающей точкой.

Замечания. Decimals следует использовать только для хранения чисел с плавающей точкой и простых арифметических операций над ними. Вся тяжелая математика, такая как вычисление индикаторов для технического анализа, должна выполняться на значениях Double.