简体   繁体   中英

Faster alternative to decimal.Parse

I noticed decimal.Parse(number, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture) is about 100% slower than custom decimal parse method based on Jeffrey Sax's code from Faster alternative to Convert.ToDouble

public static decimal ParseDecimal(string input) {
    bool negative = false;
    long n = 0;

    int len = input.Length;
    int decimalPosition = len;

    if (len != 0) {
        int start = 0;
        if (input[0] == '-') {
            negative = true;
            start = 1;
        }

        for (int k = start; k < len; k++) {
            char c = input[k];

            if (c == '.') {
                decimalPosition = k +1;
            } else {
                n = (n *10) +(int)(c -'0');
            }
        }
    }

    return new decimal(((int)n), ((int)(n >> 32)), 0, negative, (byte)(len -decimalPosition));
}

I assume that is because native decimal.Parse is designed to struggle with number style and culture info.

However, above mentioned method doesn't use 3rd parameter hi byte in new decimal so it won't work with larger numbers.

Is there a faster alternative to decimal.Parse to convert string that consists only of numbers and decimal dot to decimal which would work with large numbers?

EDIT: Benchmark:

var style = System.Globalization.NumberStyles.AllowDecimalPoint;
var culture = System.Globalization.CultureInfo.InvariantCulture;
System.Diagnostics.Stopwatch s = new System.Diagnostics.Stopwatch();
s.Reset();
s.Start();
for (int i=0; i<10000000; i++)
{
    decimal.Parse("20000.0011223344556", style, culture);
}
s.Stop();
Console.WriteLine(s.Elapsed.ToString());

s.Reset();
s.Start();
for (int i=0; i<10000000; i++)
{
    ParseDecimal("20000.0011223344556");
}
s.Stop();
Console.WriteLine(s.Elapsed.ToString());

output:

00:00:04.2313728
00:00:01.4464048

Custom ParseDecimal is in this case significantly faster than decimal.Parse.

Thanks for all your comments which gave me a little more insight. Finally I did it as follows. If input is too long then it separates input string and parses first part using long and the rest with int which is still faster than decimal.Parse.

This is my final production code:

public static int[] powof10 = new int[10]
{
    1,
    10,
    100,
    1000,
    10000,
    100000,
    1000000,
    10000000,
    100000000,
    1000000000
};
public static decimal ParseDecimal(string input)
{
    int len = input.Length;
    if (len != 0)
    {
        bool negative = false;
        long n = 0;
        int start = 0;
        if (input[0] == '-')
        {
            negative = true;
            start = 1;
        }
        if (len <= 19)
        {
            int decpos = len;
            for (int k = start; k < len; k++)
            {
                char c = input[k];
                if (c == '.')
                {
                    decpos = k +1;
                }else{
                    n = (n *10) +(int)(c -'0');
                }
            }
            return new decimal((int)n, (int)(n >> 32), 0, negative, (byte)(len -decpos));
        }else{
            if (len > 28)
            {
                len = 28;
            }
            int decpos = len;
            for (int k = start; k < 19; k++)
            {
                char c = input[k];
                if (c == '.')
                {
                    decpos = k +1;
                }else{
                    n = (n *10) +(int)(c -'0');
                }
            }
            int n2 = 0;
            bool secondhalfdec = false; 
            for (int k = 19; k < len; k++)
            {
                char c = input[k];
                if (c == '.')
                {
                    decpos = k +1;
                    secondhalfdec = true;
                }else{
                    n2 = (n2 *10) +(int)(c -'0');
                }
            }
            byte decimalPosition = (byte)(len -decpos);
            return new decimal((int)n, (int)(n >> 32), 0, negative, decimalPosition) *powof10[len -(!secondhalfdec ? 19 : 20)] +new decimal(n2, 0, 0, negative, decimalPosition);
        }
    }
    return 0;
}

benchmark code:

const string input = "[inputs are below]";
var style = System.Globalization.NumberStyles.AllowDecimalPoint | System.Globalization.NumberStyles.AllowLeadingSign;
var culture = System.Globalization.CultureInfo.InvariantCulture;
System.Diagnostics.Stopwatch s = new System.Diagnostics.Stopwatch();
s.Reset();
s.Start();
for (int i=0; i<10000000; i++)
{
    decimal.Parse(input, style, culture);
}
s.Stop();
Console.WriteLine(s.Elapsed.ToString());

s.Reset();
s.Start();
for (int i=0; i<10000000; i++)
{
    ParseDecimal(input);
}
s.Stop();
Console.WriteLine(s.Elapsed.ToString());

results on my i7 920:

input: 123.456789

00:00:02.7292447
00:00:00.6043730

input: 999999999999999123.456789

00:00:05.3094786
00:00:01.9702198

input: 1.0

00:00:01.4212123
00:00:00.2378833

input: 0

00:00:01.1083770
00:00:00.1899732

input: -3.3333333333333333333333333333333

00:00:06.2043707
00:00:02.0373628

If input consists only of 0-9, . and optionally - at the begining then this custom function is significantly faster for parsing string to decimal.

Sax's method is fast for two reasons. The first, you already know. The second, is because it is able to take advantage of the very efficient 8-byte long data type for n . Understanding this method's use of the long, can also explain why (unfortunately) it is not currently possible to use a similar method for very large numbers.

The first two parameters: lo and mid in the decimal constructor use 4 bytes each. Together this is the same amount of memory as the long. This means there is no space left to keep going once you hit the max value for a long.

To utilize a similar method you would need a 12 byte data type in place of the long. This would provide you with the extra four bytes needed to utilize the hi parameter.

Sax's method is very clever, but until someone writes a 12 byte data type, you are just going to have to rely on decimal.Parse.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM