C # Десятичная производительность типа данных - PullRequest
54 голосов
/ 14 декабря 2008

Я пишу финансовое приложение на 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 для оптимизации приложения, и я действительно поражен его текущей производительностью. Сейчас я ищу хорошую идею, чтобы улучшить десятичную производительность до уровня, близкого к Double. Я знаю, что это всего лишь сон, но просто хотел проверить, я думал обо всем возможном. :)

Спасибо!

Ответы [ 9 ]

40 голосов
/ 14 декабря 2008

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

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

22 голосов
/ 14 декабря 2008

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

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

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

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

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

8 голосов
/ 14 декабря 2008

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

6 голосов
/ 17 декабря 2015

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

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

Research

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

Если для нас было приемлемо использовать 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.
3 голосов
/ 24 июня 2010

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

2 голосов
/ 16 мая 2013

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

Вот некоторые цифры, подтверждающие идею использования 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
2 голосов
/ 03 августа 2009

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

думаю, это поможет ... так... десятичный тип данных 128-битный, а SSE2 тоже 128-битный ... он может добавлять десятичные дроби sub, div, mul в 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);
}
1 голос
/ 30 декабря 2008

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

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

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

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

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

0 голосов
/ 13 декабря 2015

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

...