Более быстрая альтернатива 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 раз быстрее.