65

我正在用 C# 编写一个财务应用程序,其中性能(即速度)至关重要。因为它是一个金融应用程序,所以我必须大量使用 Decimal 数据类型。

在分析器的帮助下,我已经尽可能地优化了代码。在使用 Decimal 之前,一切都是用 Double 数据类型完成的,速度快了好几倍。但是,由于其二进制性质,Double 不是一种选择,在多次操作过程中会导致很多精度错误。

是否有任何可以与 C# 接口的十进制库,它可以让我在 .NET 中的原生 Decimal 数据类型上提高性能?

根据我已经得到的答案,我注意到我不够清楚,所以这里有一些额外的细节:

  • 该应用程序必须尽可能快(即,与使用 Double 而不是 Decimal 时一样快将是一个梦想)。Double 比 Decimal 快大约 15 倍,因为操作是基于硬件的。
  • 硬件已经是一流的(我在双氙气四核上运行)并且应用程序使用线程,因此机器上的 CPU 利用率始终为 100%。此外,该应用程序在 64 位模式下运行,这使其比 32 位具有可测量的性能优势。
  • 我已经优化到了理智点(超过一个半月的优化;信不信由你,现在它需要大约 1/5000 来完成我最初用作参考的相同计算);这种优化涉及到一切:字符串处理、I/O、数据库访问和索引、内存、循环、改变一些东西的制作方式,甚至在任何地方都使用“switch”而不是“if”。分析器现在清楚地表明剩余的性能罪魁祸首是 Decimal 数据类型运算符。没有其他任何东西加起来相当长的时间。
  • 你必须相信我:我已经尽我所能在 C#.NET 领域优化应用程序,我对它目前的性能感到非常惊讶。我现在正在寻找一个好主意,以便将 Decimal 性能提高到接近 Double 的水平。我知道这只是一个梦想,但只是想检查一下我想到了一切可能的事情。:)

谢谢!

4

10 回答 10

44

您可以使用长数据类型。当然,您将无法在其中存储分数,但是如果您将应用程序编码为存储便士而不是英镑,那么您会没事的。长数据类型的准确性是 100%,除非您使用大量数字(使用 64 位长类型),否则您会没事的。

如果您不能强制存储便士,那么将一个整数包装在一个类中并使用它。

于 2008-12-14T20:04:21.113 回答
25

你说它需要快,但你有具体的速度要求吗?如果没有,你可能会优化到理智点:)

正如坐在我旁边的朋友刚刚建议的那样,你可以升级你的硬件吗?这可能比重写代码便宜。

最明显的选择是使用整数而不是小数 - 其中一个“单位”类似于“千分之一美分”(或任何你想要的 - 你明白了)。这是否可行将取决于您对十进制值执行的操作。处理这个问题时你需要非常小心——很容易出错(至少如果你像我一样)。

分析器是否显示了您可以单独优化的应用程序中的特定热点?例如,如果您需要在一小段代码中进行大量计算,您可以将十进制转换为整数格式,进行计算然后再转换回来。这可以使大部分代码的API保持小数形式,这很可能使其更易于维护。但是,如果您没有明显的热点,那可能是不可行的。

+1 用于分析并告诉我们速度是一个明确的要求,顺便说一句:)

于 2008-12-14T19:55:15.690 回答
14

这个问题得到了很好的讨论,但是由于我已经研究了这个问题一段时间,所以我想分享一些我的结果。

问题定义:众所周知,小数比双精度数要慢得多,但金融应用程序不能容忍对双精度数执行计算时出现的任何伪影。

研究

我的目标是测量存储浮点数的不同方法,并得出结论应该将哪一种用于我们的应用程序。

如果我们可以使用它Int64来存储具有固定精度的浮点数是可以接受的。10^6 的乘数给了我们两个:足够的数字来存储分数,并且仍然有很大的范围来存储大量。当然,您必须小心使用这种方法(乘法和除法运算可能会变得很棘手),但我们已经准备好并且也想测量这种方法。除了可能的计算错误和溢出之外,您必须记住的一件事是,通常您不能将这些长数字公开给公共 API。因此,所有内部计算都可以使用 long 执行,但在将数字发送给用户之前,它们应该转换为更友好的东西。

我已经实现了一个简单的原型类,它将一个 long 值包装到一个类似小数的结构(称为 it 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);
    }
}

实验

我测量了以下操作:加法、减法、乘法、除法、相等比较和相对(更大/更少)比较。我正在测量以下类型的操作:doublelong和. 每个操作执行 1.000.000 次。所有数字都预先分配在数组中,因此在构造函数中调用自定义代码不应影响结果。decimalMoneydecimalMoney

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. 加法、减法、乘法、比较运算decimallongor慢约 15 倍double;除法要慢约30倍。
  2. 由于缺乏 CLR 的支持,类包装器的性能Decimal优于Decimal但仍明显低于 的性能。doublelong
  3. 以绝对数字执行计算Decimal非常快:每秒 40.000.000 次操作。

建议

  1. 除非您有一个非常繁重的计算案例,否则请使用小数。在相对数字上,它们比 long 和 double 慢,但绝对数字看起来不错。
  2. Decimal由于缺乏 CLR 的支持,用您自己的结构重新实现没有多大意义。你可能会比它更快,Decimal但它永远不会像double.
  3. 如果Decimal您的应用程序的性能不够,您可能需要考虑将计算切换为long固定精度。在将结果返回给客户端之前,应将其转换为Decimal.
于 2015-12-17T10:37:27.367 回答
8

问题基本上是硬件支持双精度/浮点数,而十进制等不支持。即,您必须在速度 + 有限精度和更高的精度 + 更差的性能之间做出选择。

于 2008-12-14T19:16:36.110 回答
3

我不认为 SSE2 指令可以轻松使用 .NET Decimal 值。.NET Decimal 数据类型是128bit 十进制浮点类型http://en.wikipedia.org/wiki/Decimal128_floating-point_format,SSE2指令使用128bit 整数类型

于 2010-06-24T03:14:41.083 回答
3

在我之前的回答4 年后,我想根据我们多年来在使用浮点数进行高性能计算方面的经验添加另一个答案。

Decimal高性能计算中的数据类型存在两个主要问题:

  1. CLR 将此类型视为常规结构(没有对其他内置类型的特殊支持)
  2. 它是 128 位的

虽然你不能对第一个问题做太多,但第二个看起来更重要。使用 64 位数字进行操作时,内存操作和处理器非常高效。128 位操作要重得多。因此,.NET 的实现在Decimal设计上比Double对读/写操作的操作要慢得多。

如果您的应用程序需要浮点计算的准确性和此类操作的性能,那么既不适合Double也不Decimal适合该任务。我们在我的公司(金融科技领域)采用的解决方案是在英特尔® 十进制浮点数学库之上使用包装器。它实现了IEEE 754-2008 Decimal Floating-Point Arithmetic specification提供的 64 位浮点小数。

评论。Decimals应该只用于存储浮点数和对它们的简单算术运算。所有繁重的数学运算,例如计算技术分析指标,都应该在Double数值上执行。

UPD 2020:我们开源了小数DFP库。它是双语的(C#java)。请java记住,您不能在java. 但这超出了本次讨论的范围。随意使用。

于 2019-08-14T16:31:39.877 回答
2

老问题,但仍然非常有效。

这里有一些数字来支持使用 Long 的想法。

执行 100'000'000 次加法所需的时间

Long     231 mS
Double   286 mS
Decimal 2010 mS

简而言之,十进制比 Long 或 Double 慢约 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
于 2013-05-16T10:34:06.223 回答
1

由于我刚刚开始堆栈溢出,因此我无法发表评论或投反对票。我对 alexsmart 的评论(发布于 2008 年 12 月 23 日 12:31)是表达式 Round(n/precision,precision),其中 n 是 int 并且精度是 long 不会像他想的那样做:

1) n/precision 将返回一个整数除法,即它已经被四舍五入,但您将无法使用任何小数。舍入行为也不同于 Math.Round(...)。

2) 由于 Math.Round(double, int) 和 Math.Round(decimal, int) 之间的歧义,代码“ return Math.Round(n/precision,precision).ToString() ”无法编译。您必须转换为十进制(不是 double,因为它是一个金融应用程序),因此也可以首先使用十进制。

3) n/precision,其中精度为 4 不会截断为四位小数,而是除以 4。例如,Math.Round( (decimal) (1234567/4), 4)返回 308641。 (1234567/4 = 308641.75),而你可能想要的是得到 1235000(从尾随的 567 四舍五入到 4 位的精度)。请注意, Math.Round 允许舍入到固定点,而不是固定精度。

更新:我现在可以添加评论,但是没有足够的空间将这个放到评论区。

于 2008-12-29T22:07:50.463 回答
1

MMX/SSE/SSE2 怎么样?

我认为这会有所帮助...所以...十进制是 128 位数据类型,SSE2 也是 128 位...它可以在 1 个 CPU 滴答声中添加、sub、div、mul 十进制...

您可以使用 VC++ 为 SSE2 编写 DLL,然后在您的应用程序中使用该 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);
}
于 2009-08-03T09:28:07.100 回答
0

使用 double 存储“便士”。除了解析输入和打印输出之外,您的速度与您测量的相同。你克服了 64 位整数的限制。你有一个没有截断的除法。注意:由您决定如何在除法后使用双重结果。在我看来,这似乎是满足您要求的最简单方法。

于 2015-12-13T01:03:19.010 回答