C#十进制数据类型性能

C# Decimal datatype performance

我正在用C语言编写一个财务应用程序,其中性能(即速度)至关重要。因为它是一个金融应用程序,我必须集中使用十进制数据类型。

我已经在分析器的帮助下尽可能地优化了代码。在使用decimal之前,所有的操作都是用double数据类型完成的,速度快了几倍。但是,double不是一个选项,因为它的二进制性质,在多次操作过程中会导致很多精度错误。

是否有任何十进制库可以与C接口,使我的性能比.NET中的本机十进制数据类型有所提高?

根据我已经得到的答案,我注意到我不够清楚,下面是一些额外的细节:

  • 应用程序必须尽可能快(也就是说,当使用double而不是decimal时,尽可能快地使用double将是一个梦想)。double比decimal快15倍,因为它是基于硬件的。
  • 硬件已经是一流的(我在双氙气四核上运行),应用程序使用线程,所以CPU在机器上的利用率总是100%。此外,该应用程序在64位模式下运行,这使得它比32位具有可测量的性能优势。
  • 我已经优化了超过健全点(超过一个半月的优化;信不信由你,现在大约需要1/5000的时间来做我最初作为参考的计算);这个优化涉及到所有事情:字符串处理、I/O、数据库访问和索引、内存、循环、改变某些事情的方式制造,甚至在任何地方都使用"切换"来"如果",都会产生不同。探查器现在清楚地显示剩余的性能罪魁祸首是decimal数据类型运算符。没有别的东西能增加相当长的时间。
  • 你必须相信我:我已经尽可能地在C.NET领域优化了应用程序,我对它的当前性能感到非常惊讶。我现在正在寻找一个好主意,以提高十进制的性能,接近双倍。我知道这只是一个梦,但我只是想检查一下,我想所有可能的事情。:)

谢谢!


您可以使用long数据类型。当然,你不能在那里存储分数,但是如果你把你的应用程序编码成存储硬币而不是英镑,你会没事的。对于长数据类型,精度是100%,除非您使用的是大量的数字(使用64位长的类型),否则您会没事的。

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


你说速度要快,但是你有具体的速度要求吗?如果没有,你可能会很好地超越理智的极限。

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

最明显的选择是使用整数而不是小数,其中一个"单位"类似于"千分之一美分"(或者你想要的任何东西),你就有了这个想法。这是否可行将取决于您对十进制值执行的操作。你在处理这件事时需要非常小心-很容易出错(至少如果你和我一样)。

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

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


问题基本上是硬件支持双精度/浮点型,而十进制等则不支持。也就是说,您必须在速度+有限精度和更高精度+较差性能之间进行选择。


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

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

研究

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

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

我实现了一个简单的原型类,它将一个长值包装成类似十进制的结构(称为Money),并将其添加到度量中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
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);
    }
}

实验

我测量了以下操作:加法、减法、乘法、除法、相等比较和相对(大/小)比较。我在测量以下类型的手术:doublelongdecimalMoney。每次手术进行1000.000次。所有的数字都预先分配在数组中,因此在decimalMoney的构造函数中调用自定义代码不应影响结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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上的加法、减法、乘法、比较运算比longdouble上的运算慢约15倍;除法慢约30倍。
  • 由于缺乏clr的支持,decimal类包装的性能优于decimal类包装的性能,但仍明显低于doublelong类包装的性能。
  • decimal进行绝对数计算非常快:每秒40000.000次操作。
  • 忠告

  • 除非你的计算量很大,否则使用小数。在相对数上,它们比长整数和双整数慢,但绝对数看起来不错。
  • 由于放弃了clr的支持,用您自己的结构重新实现decimal没有多大意义。你可能比decimal快,但速度永远不会比double快。
  • 如果decimal的性能不足以满足您的应用程序,那么您可以考虑将计算转换为具有固定精度的long。在将结果返回给客户之前,应将其转换为decimal

  • 我不认为SSE2指令可以轻松地处理.NET十进制值。.NET DECIMAL数据类型为128位十进制浮点类型http://en.wikipedia.org/wiki/decimal128_浮点_格式,SSE2指令适用于128位整数类型。


    老问题,不过还是很有道理的。

    以下是一些支持使用long的数字。

    执行1000000次添加所需的时间

    1
    2
    3
    Long     231 mS
    Double   286 mS
    Decimal 2010 mS

    简言之,十进制比长的或两倍的慢约10倍。

    代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    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


    MMX/SSE/SSE2怎么样?

    我想这会有帮助…所以…decimal是128位数据类型,sse2也是128位…它可以在1个CPU时钟周期内添加,sub,div,mul十进制数…

    可以使用VC++为SSE2编写DLL,然后在应用程序中使用该DLL

    例如//你可以这样做

    VC++

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #include
    #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.*

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    [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);
    }


    我还不能给出评论或投票否决,因为我刚开始堆栈溢出。我对Alexsmart(发表于2008年12月23日12:31)的评论是,表达式round(n/precision,precision),其中n是int,precision是long,不会做他认为的事情:

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

    2)由于math.round(double,int)和math.round(decimal,int)之间存在歧义,代码"return math.round(n/precision,precision).toString()"无法编译。您将不得不强制转换为十进制(不是双精度的,因为它是一个金融应用程序),因此也可以首先使用十进制。

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

    更新:我现在可以添加评论,但没有足够的空间将此评论放入评论区域。


    用双倍硬币储存"便士"。除了解析输入和打印输出之外,您还具有与您测量的速度相同的速度。您克服了64位整数的限制。您有一个不截断的除法。注意:如何使用除法后的双精度结果取决于您。在我看来,这是满足您需求的最简单方法。