Более быстрая альтернатива decimal.Parse

Я заметил, что decimal.Parse(number, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture) примерно на 100% медленнее, чем пользовательский метод десятичного разбора, основанный на коде Джеффри Сакса из Более быстрая альтернатива 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));
}

Я предполагаю, что это потому, что native decimal.Parse предназначен для борьбы с типом номера и информацией о культуре.

Однако вышеупомянутый метод не использует 3-й параметр hi byte в new decimal, поэтому он не будет работать с большими числами.

Есть ли более быстрая альтернатива decimal.Parse для преобразования строки, состоящей только из чисел и десятичной точки в десятичную, что будет работать с большими числами?

EDIT: контрольный показатель:

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());

выход:

00:00:04.2313728
00:00:01.4464048

Пользовательский ParseDecimal в этом случае значительно быстрее, чем decimal.Parse.

Ответ 1

Спасибо за все ваши комментарии, которые дали мне немного больше понимания. Наконец я сделал это следующим образом. Если вход слишком длинный, он отделяет входную строку и обрабатывает первую часть с использованием длинной, а остальная - с int, которая все же быстрее, чем decimal.Parse.

Это мой последний производственный код:

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;
}

контрольный код:

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());

результаты на моем i7 920:

: 123.456789

00:00:02.7292447
00:00:00.6043730

: 999999999999999123.456789

00:00:05.3094786
00:00:01.9702198

input: 1.0

00:00:01.4212123
00:00:00.2378833

: 0

00:00:01.1083770
00:00:00.1899732

: -3.3333333333333333333333333333333

00:00:06.2043707
00:00:02.0373628

Если ввод состоит только из 0-9,. и необязательно - в начале, тогда эта пользовательская функция значительно быстрее для разбора строки до десятичной.

Ответ 2

Метод Sax выполняется по двум причинам. Первое, вы уже знаете. Во-вторых, это потому, что он может использовать очень эффективный 8-байтовый длинный тип данных для n. Понимание этого метода использования длинного, также может объяснить, почему (к сожалению) в настоящее время невозможно использовать подобный метод для очень больших чисел.

Первые два параметра: lo и mid в десятичном конструкторе используют по 4 байта каждый. Вместе это тот же объем памяти, что и длинный. Это означает, что вам не остается свободного места, если вы нажмете максимальное значение надолго.

Чтобы использовать подобный метод, вам понадобится 12-байтовый тип данных вместо длинного. Это предоставит вам дополнительные четыре байта, необходимые для использования параметра hi.

Метод Sax очень умный, но пока кто-то не напишет 12-байтовый тип данных, вам просто придется полагаться на decimal.Parse.

Ответ 3

Поскольку этот метод может использоваться для использования больших чисел, я написал метод, который также может заполнять большие числа. Следуя шаблону TryParse, также легко получить дешевые ранние выходы при неверном вводе.

public static class Parser
{
    /// <summary>Parses a decimal.</summary>
    /// <param name="str">
    /// The input string.
    /// </param>
    /// <param name="dec">
    /// The parsed decimal.
    /// </param>
    /// <returns>
    /// True if parsable, otherwise false.
    /// </returns>
    public static bool ToDecimal(string str, out decimal dec)
    {
        dec = default;
        if (string.IsNullOrEmpty(str))
        {
            return false;
        }

        var start = 0;
        var end = str.Length;
        var buffer = 0L;
        var negative = false;
        byte scale = 255;

        int lo = 0;
        int mid = 0;
        var block = 0;

        if (str[0] == '-')
        {
            start = 1;
            negative = true;
        }

        for (var i = start; i < end; i++)
        {
            var ch = str[i];

            // Not a digit.
            if (ch < '0' || ch > '9')
            {
                // if a dot and not found yet.
                if(ch == '.' && scale == 255)
                {
                    scale = 0;
                    continue;
                }
                return false;
            }
            unchecked
            {
                buffer *= 10;
                buffer += ch - '0';

                // increase scale if found.
                if (scale != 255)
                {
                    scale++;
                }
            }

            // Maximum decimals allowed is 28.
            if(scale > 28)
            {
                return false;
            }
            // No longer fits an int.
            if ((buffer & 0xFFFF00000000) != 0)
            {
                if (block == 0)
                {
                    lo = unchecked((int)buffer);
                }
                else if (block == 1)
                {
                    mid = unchecked((int)buffer);
                }
                // Does not longer fits block 2, so overflow.
                else if (block == 2)
                {
                    return false;
                }
                buffer >>= 32;
                block++;
            }
        }

        var hi = unchecked((int)buffer);
        dec = new decimal(lo, mid, hi, negative, scale == 255 ? default : scale);
        return true;
    }
}

И эталон:

Duration: 1.930.358 Ticks (193,04 ms), Decimal.TryParse().
Runs: 2,000,000 Avg: 0,965 Ticks/run
Duration: 319.794 Ticks (31,98 ms), Parser.ToDecimal().
Runs: 2,000,000 Avg: 0,160 Ticks/run

Так что в 6,0 раз быстрее.