Вопрос хорошо обсужден, но так как я некоторое время копал эту проблему, я хотел бы поделиться некоторыми моими результатами.
Определение проблемы: Известно, что десятичные дроби намного медленнее, чем двойные, но финансовые приложения не могут допускать каких-либо артефактов, которые возникают, когда вычисления выполняются на удваиваниях.
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
Выводы
- Операции сложения, вычитания, умножения, сравнения на
decimal
в ~ 15 раз медленнее операций на long
или double
; деление в ~ 30 раз медленнее.
- Производительность
Decimal
-подобной оболочки лучше, чем производительность Decimal
, но все же значительно хуже, чем производительность double
и long
из-за отсутствия поддержки со стороны CLR.
- Выполнение вычислений для
Decimal
в абсолютных числах довольно быстро: 40 000 000 операций в секунду.
Обратить
- Если у вас нет очень тяжелых вариантов вычисления, используйте десятичные дроби. В относительных числах они медленнее длинных и двойных, но абсолютные числа выглядят хорошо.
- Нет особого смысла в повторной реализации
Decimal
с вашей собственной структурой из-за отсутствия поддержки со стороны CLR. Вы можете сделать это быстрее, чем Decimal
, но это никогда не будет так быстро, как double
.
- Если производительности
Decimal
недостаточно для вашего приложения, чем вы могли бы подумать о переключении ваших расчетов на long
с фиксированной точностью. Перед возвратом результата клиенту его следует преобразовать в Decimal
.