Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 781
LukAss741 Avatar asked Jun 05 '16 19:06

LukAss741


1 Answers

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.

like image 103
LukAss741 Avatar answered Oct 01 '22 05:10

LukAss741