更新结果
摘要
Mode : Release
Test Framework : .NET Framework 4.7.1
Benchmarks runs : 100 times (averaged/scale)
Tests limited to 10 digits
Name | Time | Range | StdDev | Cycles | Pass
Mine Unchecked | 9.645 ms | 0.259 ms | 0.30 | 32,815,064 | Yes
Mine Unchecked2 | 10.863 ms | 1.337 ms | 0.35 | 36,959,457 | Yes
Mine Safe | 11.908 ms | 0.993 ms | 0.53 | 40,541,885 | Yes
float.Parse | 26.973 ms | 0.525 ms | 1.40 | 91,755,742 | Yes
Evk | 31.513 ms | 1.515 ms | 7.96 | 103,288,681 | Base
Test Limited to 38 digits
Name | Time | Range | StdDev | Cycles | Pass
Mine Unchecked | 17.694 ms | 0.276 ms | 0.50 | 60,178,511 | No
Mine Unchecked2 | 23.980 ms | 0.417 ms | 0.34 | 81,641,998 | Yes
Mine Safe | 25.078 ms | 0.124 ms | 0.63 | 85,306,389 | Yes
float.Parse | 36.985 ms | 0.052 ms | 1.60 | 125,929,286 | Yes
Evk | 39.159 ms | 0.406 ms | 3.26 | 133,043,100 | Base
Test Limited to 98 digits (way over the range of a float)
Name | Time | Range | StdDev | Cycles | Pass
Mine Unchecked2 | 46.780 ms | 0.580 ms | 0.57 | 159,272,055 | Yes
Mine Safe | 48.048 ms | 0.566 ms | 0.63 | 163,601,133 | Yes
Mine Unchecked | 48.528 ms | 1.056 ms | 0.58 | 165,238,857 | No
float.Parse | 55.935 ms | 1.461 ms | 0.95 | 190,456,039 | Yes
Evk | 56.636 ms | 0.429 ms | 1.75 | 192,531,045 | Base
可验证的是,
Mine Unchecked
在处理较小数字时效果很好,但在使用幂计算小数时,对于较大的数字组合它无法正常工作,因为它只是一种基于10的幂,它仅仅是一个大的开关语句,这使得它略微更快。
背景
由于各种评论和我所做的工作,我认为我应该用最准确的基准来重写这篇文章,并解释背后的所有逻辑。
所以当这个问题第一次出现时,我已经编写了自己的基准框架,并且通常就像为这些事情编写快速解析器和使用不安全代码一样,我可以比框架等效物更快地完成这些工作。
起初这很容易,只需编写一个简单的逻辑来解析具有小数点位数的数字,我做得很好,然而初始结果可能不够准确,因为我的测试数据仅使用“f”格式说明符,并将更高的精度数字转换为只有0的短格式。
最终我无法编写可靠的解析器来处理指数符号,即 1.2324234233E+23
。我能够让数学计算生效的唯一方法是使用 BIGINTEGER
和大量的黑客技巧来将正确的精度强制转换为浮点值。这变得非常缓慢。我甚至去查了 IEEE 规范并尝试以位构造它,这并不难,但公式中有循环,并且很难正确地实现。最终我不得不放弃指数符号。
所以这就是我最终得到的结果
我的测试框架在输入数据上运行一个由10000个字符串表示的浮点数列表,该列表在测试之间共享并为每个测试运行生成。一个测试运行只是遍历每个测试(记住它是每个测试相同的数据),然后添加结果并求平均值。这是最好的情况。我可以将运行次数增加到1000倍或更多,但它们实际上并没有改变。在这种情况下,因为我们正在测试一个基本上只需要一个变量(浮点数的字符串表示)的方法,所以没有必要进行扩展,但是我可以调整输入以适应不同长度的浮点数,例如,10、20甚至98位的字符串。请记住,浮点数最高只能达到38位。
为了检查结果,我使用了以下方法:我之前编写了一个测试单元,涵盖了所有可能的浮点数,它们都可以工作,除了一种情况,即我使用幂来计算数字的小数部分。
请注意,我的框架只测试1个结果集,并且不是框架的一部分
private bool Action(List<float> floats, List<float> list)
{
if (floats.Count != list.Count)
return false;
for (int i = 0; i < list.Count; i++)
{
if ( floats[i] != list[i] && !float.IsNaN(floats[i]) && !float.IsNaN(list[i]))
return false;
}
return true;
}
在这种情况下,我再次测试以下3种输入类型,如下所示。
设置:
private static NumberFormatInfo GetNumberFormatInfo(int numberDecimalDigits)
{
return new NumberFormatInfo
{
NumberDecimalSeparator = ".",
NumberDecimalDigits = numberDecimalDigits
};
}
private static unsafe string GetRadomFloatString(IFormatProvider formatInfo)
{
var val = Rand.Next(0, int.MaxValue);
if (Rand.Next(0, 2) == 1)
val *= -1;
var f = *(float*)&val;
return f.ToString("f", formatInfo);
}
测试数据 1
public static List<string> GenerateInput10(int scale)
{
var result = new List<string>(scale);
while (result.Count < scale)
{
var val = GetRadomFloatString(GetNumberFormatInfo(10));
if (val != "0.0000000000")
result.Add(val);
}
result.Insert(0, (-0f).ToString("f", CultureInfo.InvariantCulture));
result.Insert(0, "-0");
result.Insert(0, "0.00");
result.Insert(0, float.NegativeInfinity.ToString("f", CultureInfo.InvariantCulture));
result.Insert(0, float.PositiveInfinity.ToString("f", CultureInfo.InvariantCulture));
return result;
}
测试数据2
// basically that max value for a float
public static List<string> GenerateInput38(int scale)
{
var result = Enumerable.Range(1, scale)
.Select(x => GetRadomFloatString(GetNumberFormatInfo(38)))
.ToList();
result.Insert(0, (-0f).ToString("f", CultureInfo.InvariantCulture));
result.Insert(0, "-0");
result.Insert(0, float.NegativeInfinity.ToString("f", CultureInfo.InvariantCulture));
result.Insert(0, float.PositiveInfinity.ToString("f", CultureInfo.InvariantCulture));
return result;
}
测试数据3
// Lets take this to the limit
public static List<string> GenerateInput98(int scale)
{
var result = Enumerable.Range(1, scale)
.Select(x => GetRadomFloatString(GetNumberFormatInfo(98)))
.ToList();
result.Insert(0, (-0f).ToString("f", CultureInfo.InvariantCulture));
result.Insert(0, "-0");
result.Insert(0, float.NegativeInfinity.ToString("f", CultureInfo.InvariantCulture));
result.Insert(0, float.PositiveInfinity.ToString("f", CultureInfo.InvariantCulture));
return result;
}
这是我使用的测试:
这是
Evk 。
private float ParseMyFloat(string value)
{
var result = float.Parse(value, CultureInfo.InvariantCulture);
if (result == 0f && value.TrimStart()
.StartsWith("-"))
{
result = -0f;
}
return result;
}
“我的安全”,作者认为它是安全的,因为它尝试检查无效字符串。
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private unsafe float ParseMyFloat(string value)
{
double result = 0, dec = 0;
if (value[0] == 'N' && value == "NaN") return float.NaN;
if (value[0] == 'I' && value == "Infinity")return float.PositiveInfinity;
if (value[0] == '-' && value[1] == 'I' && value == "-Infinity")return float.NegativeInfinity;
fixed (char* ptr = value)
{
char* l, e;
char* start = ptr, length = ptr + value.Length;
if
未经检查的
这对于较大的字符串会失败,但对于较小的字符串则可以。
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private unsafe float ParseMyFloat(string value)
{
if (value[0] == 'N' && value == "NaN") return float.NaN;
if (value[0] == 'I' && value == "Infinity") return float.PositiveInfinity;
if (value[0] == '-' && value[1] == 'I' && value == "-Infinity") return float.NegativeInfinity;
fixed (char* ptr = value)
{
var point = 0;
double result = 0, dec = 0;
char* c, start = ptr, length = ptr + value.Length;
if
未核对的2
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private unsafe float ParseMyFloat(string value)
{
if (value[0] == 'N' && value == "NaN") return float.NaN;
if (value[0] == 'I' && value == "Infinity") return float.PositiveInfinity;
if (value[0] == '-' && value[1] == 'I' && value == "-Infinity") return float.NegativeInfinity;
fixed (char* ptr = value)
{
double result = 0, dec = 0;
char* c, start = ptr, length = ptr + value.Length;
if
Float.parse
float.Parse(t, CultureInfo.InvariantCulture)
原始答案
假设您不需要一个TryParse
方法,我使用指针和自定义解析来实现了我认为您想要的功能。
基准测试使用了一个包含1,000,000个随机浮点数的列表,并且每个版本运行100次,所有版本使用相同的数据。
Test Framework : .NET Framework 4.7.1
Scale : 1000000
Name | Time | Delta | Deviation | Cycles
----------------------------------------------------------------------
Mine Unchecked2 | 45.585 ms | 1.283 ms | 1.70 | 155,051,452
Mine Unchecked | 46.388 ms | 1.812 ms | 1.17 | 157,751,710
Mine Safe | 46.694 ms | 2.651 ms | 1.07 | 158,697,413
float.Parse | 173.229 ms | 4.795 ms | 5.41 | 589,297,449
Evk | 287.931 ms | 7.447 ms | 11.96 | 979,598,364
为简洁起见,删减了内容
注意,这两个版本无法处理扩展格式、NaN
、+Infinity
或 -Infinity
。不过,实现起来并不困难。
我检查过这个代码,但必须承认我没有编写任何单元测试,因此请谨慎使用。
免责声明,我认为 Evk 的 StartsWith
版本可能可以更加优化,但最多也只能比 float.Parse
稍微慢一些。
0f
进行一次比较,我认为与将字符串解析为浮点数所需的时间相比,这是完全可以忽略不计的。 - EvkParse
一次可能需要比使用==
进行一次比较花费更多的时间,可能会相差1000倍。总之,除非你事先对代码进行了性能分析,否则不要应用微观优化。 - chi