Производительность C# Decimal типа данных

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

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

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

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

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

Спасибо!

Вы находитесь на грани преждевременной оптимизации, просто правильно запрограммируйте приложение, а затем настраивайте его постфактум

TravisO 14.12.2008 22:30

Вы выполнили все низкоуровневые оптимизации C#. Могут быть оставлены некоторые алгоритмические улучшения (например, выполнение меньшего количества операций с десятичными знаками).

Brian 21.05.2009 17:34

Работа с базой данных в чувствительном ко времени финансовом приложении звучит плохо ...

Michael Brown 12.04.2013 18:05
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
63
3
23 842
10

Ответы 10

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

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

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

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

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

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

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

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

Я согласен. Используйте 64-битную машину и длинные. Кроме того, мне пришло в голову - я думаю, что у .NET были некоторые инструменты для генерации машинно-ориентированного кода. Я считаю, что это называлось ngen. Возможно, там есть производительность ...

Vilx- 15.12.2008 11:55

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

Chris 15.12.2008 12:20

Обертывание его в класс повлечет за собой накладные расходы на кучу, и даже структура замедлит работу из-за перегрузки оператора или требуемых вызовов функций, поэтому необработанное хранилище long / int лучше всего для производительности.

Chris 15.12.2008 12:22

Это действительно интересная идея. Обычно финансовые приложения должны больше заботиться о небольшом количестве, чем наоборот. Мне очень нравится это решение. +1

Trap 15.12.2008 12:26

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

ILoveFortran 05.01.2009 01:17

Фактически это то, что делает десятичный тип, за исключением того, что коэффициент масштабирования является переменным, и тип использует 96-битное представление для цифр (и всего 128 бит); поэтому он медленнее, чем предлагаемая версия с использованием long (64 бит). (Эти поля для комментариев слишком короткие.)

ILoveFortran 05.01.2009 01:21

@Vilx: Ngen не ускоряет работу программы. Все, что Ngen сделает, - это ускорит запуск программы.

Brian 21.05.2009 17:32

Использование long может быть очень сложным для существующей логики. Простое выражение «копейки магазина» может работать для простого учета, но не обязательно для расчета сложных процентов и других нецелочисленных формул. Кто знает, какую последовательность вычислений он делает?

Nosredna 15.06.2009 01:43

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

gbjbaanb 15.06.2009 15:46

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

Aidiakapi 05.03.2012 19:13

внутри программы, как распознать "длинное" длинное или "длинное" десятичное? Должен ли я добавить что-то вроде typedef myDecimal long, а затем везде использовать myDecimal вместо long для улучшения читаемости?

Oleg Vazhnev 09.01.2014 21:19

Это называется «Арифметика с фиксированной точкой». en.wikipedia.org/wiki/Fixed-point_arithmetic Если вы используете масштабные коэффициенты с основанием 2, а не с основанием 10, как вы это делали, то можно получить дополнительный выигрыш в вычислениях, а затем разделить и умножить на битовое дерьмо. Существенно ли это повышение производительности, конечно, зависит от вашего общего алгоритма и профилирования производительности.

JimbobTheSailor 22.09.2020 01:17

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

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

2) Код «return Math.Round (n / точность, точность) .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 позволяет округлять до фиксированной точки, а не до фиксированной точности.

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

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

думаю поможет ... так... decimal - это 128-битный тип данных, а SSE2 тоже 128-битный ... и он может добавлять, sub, div, mul decimal за 1 тик ЦП ...

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

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

VC++

#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;
}

C#

[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);
}

Я что-то упустил или этот код - полная чушь? Он игнорирует структуру десятичной дроби и просто выполняет операцию сложения для всего, включая бит знака и показатель степени.

Stefan Fabian 14.04.2018 13:18

Интересная идея. Однако выделение int[] в конечном итоге убьет производительность, когда сработает сборщик мусора.

l33t 17.12.2018 11:40

@Stefan: это действительно полный мусор. Он не настраивает масштаб перед добавлением, он не настраивает масштаб впоследствии (результат может достигать 192 бит + масштаб), он не обрабатывает по-разному верхние 32 бита, не t отрегулировать переполнение шкалы и т. д. Это чистейшая фигня.

Rudy Velthuis 27.12.2018 10:39

@ l33t: совсем не интересно. Что делает Decimal медленным, так это чистое сложение нет. Тогда это должно быть только немного медленнее, чем долго. 1. При этом не учитывается масштабирование, 2. При этом не распознается внутренний формат десятичной дроби, 3. При этом sse2 не учитывает перенос, так что это чистая ерунда. Это не способ добавления десятичных знаков.

Rudy Velthuis 27.12.2018 10:44

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

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

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

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

Long     231 mS
Double   286 mS
Decimal 2010 mS

Короче говоря, десятичная дробь примерно в 10 раз медленнее, чем длинная или двойная.

Код:

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

2 секунды делать добавления 100 миллионов. Каким образом это «поддерживает идею использования long»? Он может быть медленнее, но вы не можете просто сравнивать скорости, 50 миллионов добавлений в секунду просто не стоит оптимизировать.

weston 27.09.2013 17:02

231 Милли секунд, а не секунды. Это не медленнее, а быстрее, чего и добивается OP.

smirkingman 29.09.2013 18:17

Речь идет о десятичных миллисекундах. За 2 секунды он делает 100 миллионов. Это аргумент в пользу того, что OP должен искать узкие места в другом месте. Это не аргумент в пользу того, что они должны переписывать, чтобы использовать long, независимо от того, насколько быстрее long.

weston 29.09.2013 19:09

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

smirkingman 29.09.2013 19:49

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

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

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

Исследовать

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

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

Я реализовал простой класс-прототип, который обертывает длинное значение в десятичную структуру (назвал ее 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

Выводы

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

Совет

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

Вместо использования Multiplier и дополнительных умножений и делений я бы подумал об использовании «сдвигателя» и выполнения сдвиги влево и вправо (<< и >>). Я ожидаю, что двоичные операции будут быстрее десятичных.

Theodor Zoulias 19.08.2020 11:11

Фактически, то, что описано выше, не является производственным кодом качества (без проверки переполнения, потери значимости и т. д.). Он использовался только для получения показателей производительности такого подхода.

user1921819 19.08.2020 11:29

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

Theodor Zoulias 19.08.2020 12:02

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

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

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

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

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

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

UPD 2020: Мы открыли исходный код библиотеки десятичных знаков DFP. Он двуязычный (C# и java). У java есть некоторые особенности, помните, что у вас не может быть пользовательских типов (структур) без выделения памяти в java. Но это выходит за рамки данного обсуждения. Не стесняйтесь использовать.

Теперь это похоже на действительно хорошее решение. Было бы любопытно увидеть сравнение производительности!

Etienne Charland 23.09.2020 18:38

Другие вопросы по теме