Я пишу финансовое приложение на C#, где производительность (т.е. скорость) имеет решающее значение. Поскольку это финансовое приложение, мне приходится интенсивно использовать тип данных Decimal.
Я максимально оптимизировал код с помощью профилировщика. До использования Decimal все делалось с типом данных Double, и скорость была в несколько раз выше. Однако Double не подходит из-за его двоичной природы, вызывая множество ошибок точности при выполнении нескольких операций.
Есть ли какая-либо библиотека десятичных чисел, с которой я могу взаимодействовать с C#, которая могла бы улучшить производительность по сравнению с собственным типом данных Decimal в .NET?
Основываясь на ответах, которые я уже получил, я заметил, что был недостаточно ясен, поэтому вот некоторые дополнительные детали:
Спасибо!
Вы выполнили все низкоуровневые оптимизации C#. Могут быть оставлены некоторые алгоритмические улучшения (например, выполнение меньшего количества операций с десятичными знаками).
Работа с базой данных в чувствительном ко времени финансовом приложении звучит плохо ...





Проблема в основном в том, что double / float поддерживаются аппаратно, а Decimal и т.п. - нет. Т.е. вам придется выбирать между скоростью + ограниченной точностью и большей точностью + более низкой производительностью.
Вы говорите, что он должен быть быстрым, но есть ли у вас конкретные требования к скорости? Если нет, вы вполне можете оптимизировать за пределами здравого смысла :)
Как только что предложил друг, сидящий рядом со мной, можете ли вы вместо этого обновить свое оборудование? Вероятно, это будет дешевле, чем переписывать код.
Самый очевидный вариант - использовать целые числа вместо десятичных - где одна «единица» - это что-то вроде «тысячной доли цента» (или как хотите - вы поняли). Возможно ли это или нет, будет зависеть от операций, которые вы выполняете для начала с десятичными значениями. Вы должны быть осторожны с очень при обращении с этим - легко ошибиться (по крайней мере, если вы похожи на меня).
Показал ли профилировщик определенные горячие точки в вашем приложении, которые вы могли бы оптимизировать индивидуально? Например, если вам нужно выполнить много вычислений в одной небольшой области кода, вы можете преобразовать десятичный формат в целочисленный, выполнить вычисления и затем преобразовать обратно. Это могло бы сохранить API в виде десятичных знаков для основной части кода, что вполне может облегчить его поддержку. Однако, если у вас нет ярко выраженных горячих точек, это может оказаться невозможным.
+1 за профилирование и сообщение о том, что скорость - определенное требование, кстати :)
вы можете использовать длинный тип данных. Конечно, вы не сможете хранить там дроби, но если вы закодируете свое приложение для хранения копеек, а не фунтов, все будет в порядке. Точность составляет 100% для длинных типов данных, и если вы не работаете с большими числами (используйте 64-битный длинный тип), все будет в порядке.
Если вы не можете назначить хранение пенни, оберните целое число в класс и используйте его.
Я согласен. Используйте 64-битную машину и длинные. Кроме того, мне пришло в голову - я думаю, что у .NET были некоторые инструменты для генерации машинно-ориентированного кода. Я считаю, что это называлось ngen. Возможно, там есть производительность ...
Это определенно правильный путь. Используйте long или int и храните копейки или доли пенни в зависимости от того, какая точность вам нужна. Многие операции теперь будут выполняться так же быстро или быстрее, чем при использовании удвоений.
Обертывание его в класс повлечет за собой накладные расходы на кучу, и даже структура замедлит работу из-за перегрузки оператора или требуемых вызовов функций, поэтому необработанное хранилище long / int лучше всего для производительности.
Это действительно интересная идея. Обычно финансовые приложения должны больше заботиться о небольшом количестве, чем наоборот. Мне очень нравится это решение. +1
Вам не нужно хранить дроби - просто помните о фиксированном коэффициенте масштабирования. Например, ваш тип хранит 1/1000 цента, а затем просто разделите полученный результат на этот коэффициент (100000), чтобы получить его в долларах.
Фактически это то, что делает десятичный тип, за исключением того, что коэффициент масштабирования является переменным, и тип использует 96-битное представление для цифр (и всего 128 бит); поэтому он медленнее, чем предлагаемая версия с использованием long (64 бит). (Эти поля для комментариев слишком короткие.)
@Vilx: Ngen не ускоряет работу программы. Все, что Ngen сделает, - это ускорит запуск программы.
Использование long может быть очень сложным для существующей логики. Простое выражение «копейки магазина» может работать для простого учета, но не обязательно для расчета сложных процентов и других нецелочисленных формул. Кто знает, какую последовательность вычислений он делает?
вам просто нужно не забыть сохранить наименьшее значение, которое вы готовы отслеживать. Это происходит даже с типами Decimal, только они пытаются дать вам гораздо большую точность, чем вам может понадобиться. Десятичные типы в конечном итоге все равно не имеют точности.
Имейте в виду, что при использовании целочисленных типов будет происходить целочисленное деление, и значения будут усечены, а не округлены.
внутри программы, как распознать "длинное" длинное или "длинное" десятичное? Должен ли я добавить что-то вроде typedef myDecimal long, а затем везде использовать myDecimal вместо long для улучшения читаемости?
Это называется «Арифметика с фиксированной точкой». en.wikipedia.org/wiki/Fixed-point_arithmetic Если вы используете масштабные коэффициенты с основанием 2, а не с основанием 10, как вы это делали, то можно получить дополнительный выигрыш в вычислениях, а затем разделить и умножить на битовое дерьмо. Существенно ли это повышение производительности, конечно, зависит от вашего общего алгоритма и профилирования производительности.
Я пока не могу дать комментарий или проголосовать, так как только начал переполнение стека. Мой комментарий к 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);
}
Я что-то упустил или этот код - полная чушь? Он игнорирует структуру десятичной дроби и просто выполняет операцию сложения для всего, включая бит знака и показатель степени.
Интересная идея. Однако выделение int[] в конечном итоге убьет производительность, когда сработает сборщик мусора.
@Stefan: это действительно полный мусор. Он не настраивает масштаб перед добавлением, он не настраивает масштаб впоследствии (результат может достигать 192 бит + масштаб), он не обрабатывает по-разному верхние 32 бита, не t отрегулировать переполнение шкалы и т. д. Это чистейшая фигня.
@ l33t: совсем не интересно. Что делает Decimal медленным, так это чистое сложение нет. Тогда это должно быть только немного медленнее, чем долго. 1. При этом не учитывается масштабирование, 2. При этом не распознается внутренний формат десятичной дроби, 3. При этом sse2 не учитывает перенос, так что это чистая ерунда. Это не способ добавления десятичных знаков.
Я не думаю, что инструкции 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 миллионов добавлений в секунду просто не стоит оптимизировать.
231 Милли секунд, а не секунды. Это не медленнее, а быстрее, чего и добивается OP.
Речь идет о десятичных миллисекундах. За 2 секунды он делает 100 миллионов. Это аргумент в пользу того, что OP должен искать узкие места в другом месте. Это не аргумент в пользу того, что они должны переписывать, чтобы использовать long, независимо от того, насколько быстрее long.
Внимательно перечитайте вопрос ОП. Сначала он сделал это с двойным числом, а когда он перешел на десятичную дробь, это было намного медленнее. Он хочет более быстрое решение для арифметики. Это финансовое приложение. В качестве альтернативы можно было бы хранить суммы в центах в лонге.
хранить «копейки» с помощью двойника. Помимо анализа ввода и вывода на печать, вы получаете ту же скорость, которую вы измерили. вы преодолеваете ограничение в 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
Выводы
decimal примерно в 15 раз медленнее, чем операции на long или double; деление в ~ 30 раз медленнее.Decimal-подобной оболочки лучше, чем производительность Decimal, но все же значительно хуже, чем производительность double и long из-за отсутствия поддержки со стороны CLR.Decimal в абсолютных числах происходит довольно быстро: 40 000 000 операций в секунду.Совет
Decimal с вашей собственной структурой из-за отсутствия поддержки со стороны CLR. Вы можете сделать его быстрее, чем Decimal, но он никогда не будет таким быстрым, как double.Decimal недостаточно для вашего приложения, вы можете подумать о переключении ваших вычислений на long с фиксированной точностью. Перед возвратом результата клиенту его необходимо преобразовать в Decimal.Вместо использования Multiplier и дополнительных умножений и делений я бы подумал об использовании «сдвигателя» и выполнения сдвиги влево и вправо (<< и >>). Я ожидаю, что двоичные операции будут быстрее десятичных.
Фактически, то, что описано выше, не является производственным кодом качества (без проверки переполнения, потери значимости и т. д.). Он использовался только для получения показателей производительности такого подхода.
Да, я понимаю, что это прототип для грубых замеров производительности. Мой комментарий в том же духе.
Через 4 года после моего предыдущий ответ я хотел бы добавить еще один, основываясь на нашем многолетнем опыте работы с высокопроизводительными вычислениями с числами с плавающей запятой.
Есть две основные проблемы с типом данных Decimal при высокопроизводительных вычислениях:
Хотя с первой проблемой мало что можно сделать, вторая выглядит еще более важной. Операции с памятью и процессоры чрезвычайно эффективны при работе с 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. Но это выходит за рамки данного обсуждения. Не стесняйтесь использовать.
Теперь это похоже на действительно хорошее решение. Было бы любопытно увидеть сравнение производительности!
Вы находитесь на грани преждевременной оптимизации, просто правильно запрограммируйте приложение, а затем настраивайте его постфактум