是否可以解析带符号的零? 我尝试了几种方法,但没有人给出正确的结果:
float test1 = Convert.ToSingle("-0.0");
float test2 = float.Parse("-0.0");
float test3;
float.TryParse("-0.0", out test3);
如果我使用直接初始化的值就可以了:
float test4 = -0.0f;
所以问题似乎出在c#的解析过程上。我希望有人能告诉我是否有一些选择或解决方法。
只有转换成二进制才能看到差异:
var bin= BitConverter.GetBytes(test4);
我认为没有办法强制
float.Parse
(或Convert.ToSingle
)尊重负零。它就像这样工作(在这种情况下忽略符号)。所以你必须自己检查一下,例如:
string target = "-0.0";
float result = float.Parse(target, CultureInfo.InvariantCulture);
if (result == 0f && target.TrimStart().StartsWith("-"))
result = -0f;
如果我们查看 coreclr 的源代码,我们会看到(跳过所有不相关的部分):
private static bool NumberBufferToDouble(ref NumberBuffer number, ref double value)
{
double d = NumberToDouble(ref number);
uint e = DoubleHelper.Exponent(d);
ulong m = DoubleHelper.Mantissa(d);
if (e == 0x7FF)
{
return false;
}
if (e == 0 && m == 0)
{
d = 0; // < relevant part
}
value = d;
return true;
}
如您所见,如果尾数和指数均为零 - 值将显式分配给
0
。所以你无法改变这一点。
完整的 .NET 实现具有
NumberBufferToDouble
和 InternalCall
(以纯 C\C++ 实现),但我认为它做了类似的事情。
总结
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 的幂,所以它与 i 只是一个大开关一起使用声明这使得速度稍微快一些。
背景
因为我收到的各种评论,以及我为此付出的努力。我想我应该用我能得到的最准确的基准重写这篇文章。以及它们背后的所有逻辑。
因此,当第一个问题出现时,我已经编写了自己的基准测试框架,并且通常就像为这些东西编写快速解析器并使用不安全的代码一样,十分之九的我可以比同等框架更快地获得这些东西。
一开始这很简单,只需编写一个简单的逻辑来解析带有小数点位置的数字,我做得很好,但是最初的结果并不像他们应有的那么准确,因为我的测试数据只是使用' f' 格式说明符,并将较大精度的数字转换为只有 0 的短格式。
最终我无法编写可靠的解析来处理指数符号,即
1.2324234233E+23
。我能让数学发挥作用的唯一方法是使用 BIGINTEGER
和大量的技巧来强制将正确的精度转换为浮点值。这变得超级慢。我什至查看了 float IEEE 规范,并尝试进行数学计算,以位为单位构造它,这并不难,但是该公式中有循环,并且很难正确计算。最后我不得不放弃指数表示法。
这就是我最终的结果。
我的测试框架在输入数据上运行 10000 个浮点作为字符串的列表,该列表在测试之间共享并为每个测试运行生成,测试运行只是经历每个测试(记住每个测试的数据相同)并且将结果相加然后求平均值。这已经是最好的了。我可以将运行次数增加到 1000 或更多,但它们并没有真正改变。在这种情况下,因为我们正在测试一种基本上采用一个变量(浮点数的字符串表示形式)的方法,所以没有必要对其进行缩放,因为它不是基于设置的,但是我可以调整输入以适应不同长度的浮点数,即, 10、20 到 98 位数字的字符串。不管怎样,记住一个浮点数最多只能到 38。
为了检查我使用以下内容的结果,我之前编写了一个测试单元,涵盖了所有可以想象的浮点数,并且它们都有效,除了我使用 Powers 来计算数字的小数部分的变体。
注意,我的框架仅测试 1 个结果集,并且它不是框架的一部分
private bool Action(List<float> floats, List<float> list)
{
if (floats.Count != list.Count)
return false; // sanity check
for (int i = 0; i < list.Count; i++)
{
// nan is a special case as there is more than one possible bit value
// for it
if ( floats[i] != list[i] && !float.IsNaN(floats[i]) && !float.IsNaN(list[i]))
return false;
}
return true;
}
在这种情况下,我再次测试 3 种类型的输入,如下所示
设置
// numberDecimalDigits specifies how long the output will be
private static NumberFormatInfo GetNumberFormatInfo(int numberDecimalDigits)
{
return new NumberFormatInfo
{
NumberDecimalSeparator = ".",
NumberDecimalDigits = numberDecimalDigits
};
}
// generate a random float by create an int, and converting it to float in pointers
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
// limits the out put to 10 characters
// also because of that it has to check for trunced vales and
// regenerates them
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 (*ptr == '-') start++;
for (l = start; *l >= '0' && *l <= '9' && l < length; l++)
result = result * 10 + *l - 48;
if (*l == '.')
{
char* r;
for (r = length - 1; r > l && *r >= '0' && *r <= '9'; r--)
dec = (dec + (*r - 48)) / 10;
if (l != r)
throw new FormatException($"Invalid float : {value}");
}
else if (l != length)
throw new FormatException($"Invalid float : {value}");
result += dec;
return *ptr == '-' ? (float)result * -1 : (float)result;
}
}
未勾选
这对于较大的字符串会失败,但对于较小的字符串来说是可以的
[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 (*ptr == '-') start++;
for (c = start; c < length && *c != '.'; c++)
result = result * 10 + *c - 48;
if (*c == '.')
{
point = (int)(length - 1 - c);
for (c++; c < length; c++)
dec = dec * 10 + *c - 48;
}
// MyPow is just a massive switch statement
if (dec > 0)
result += dec / MyPow(point);
return *ptr == '-' ? (float)result * -1 : (float)result;
}
}
未选中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 (*ptr == '-') start++;
for (c = start; c < length && *c != '.'; c++)
result = result * 10 + *c - 48;
// this division seems unsafe for a double,
// however i have tested it with every float and it works
if (*c == '.')
for (var d = length - 1; d > c; d--)
dec = (dec + (*d - 48)) / 10;
result += dec;
return *ptr == '-' ? (float)result * -1 : (float)result;
}
}
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
稍慢。
你可以试试这个:
string target = "-0.0";
decimal result= (decimal.Parse(target,
System.Globalization.NumberStyles.AllowParentheses |
System.Globalization.NumberStyles.AllowLeadingWhite |
System.Globalization.NumberStyles.AllowTrailingWhite |
System.Globalization.NumberStyles.AllowThousands |
System.Globalization.NumberStyles.AllowDecimalPoint |
System.Globalization.NumberStyles.AllowLeadingSign));