如何解析带符号的零?

19

能否解析带符号的零?我尝试了几种方法,但没有一个能给出正确的结果:

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);
3个回答

17

我认为无法强制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 ++中实现),但我认为它执行了类似的操作。


1
@mortal 但是你数百万个字符串中有多少是零呢?因为如果不是零,那么开销只是与 0f 进行一次比较,我认为与将字符串解析为浮点数所需的时间相比,这是完全可以忽略不计的。 - Evk
2
@mortal 是的,但与将字符串解析为浮点数的代码相比,这个检查几乎不需要时间。无论如何,我认为没有直接的解决方案。 - Evk
6
@mortal说:解析浮点数的代码是框架中一些最慢的字符串转换代码,更不用说额外检查负零了(这很难)。如果速度真的是一个很大的问题,你应该首先考虑是否可以将值减少到允许更快解析的受限形式,并且如果你需要你的浮点值精确地回路传输,甚至需要区分负零,那么你可能本来就不应该使用十进制表示法。 - Jeroen Mostert
我本以为这个答案会非常快。它可能只需要很小一部分的解析时间。 - Matthew Watson
7
我认为你陷入了一个误区,认为快速的代码和短小(简洁)的代码是相同的。它们并不相同。在计算机科学中,相反的情况经常发生:通常,简短的简单算法往往效率低下,而高效算法要长得多。在这里,调用Parse一次可能需要比使用==进行一次比较花费更多的时间,可能会相差1000倍。总之,除非你事先对代码进行了性能分析,否则不要应用微观优化。 - chi
显示剩余3条评论

9
更新结果 摘要
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; // 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 稍微慢一些。


1
我注意到的一件事(在你的基准测试中得到了验证)是,专门的操作通常比必须考虑各种情况的BCL方法更快。无论“有符号零”问题如何,OP可能会更喜欢针对可预测数据集的自定义(更快速)解析。当然,如果数据不可预测,那就是另外一回事了。 - Tim M.
1
@TimMedora 没错,这些 parse 方法有很多功能,所以这就是尝试的背后思想,以减少自由度。 - TheGeneral
1
你的解决方案比库更快的一个原因是它不会担心结果的准确性。在某些特定情况下(包括这种情况),这可能并不重要,但在一般情况下,对准确性有很强的期望。重复地将浮点数除以10会在每一步中丢失精度。 - rici
什么是最坏情况?仅仅有更多的除法并不一定更糟;这取决于各种舍入误差的大小和方向。想要得出一个足够的测试集并不容易。 - rici
1
你可能想看一下David Gay的论文:https://www.ampl.com/REFS/rounding.pdf。William Clinger也有非付费版的论文,很容易找到。如果你使用双精度来产生浮点结果,错误是非常罕见的。我记得Cligner声称使用长双精度来产生双精度结果失败的概率不到1%,而双精度/浮点数的错误应该更加罕见。 - rici
显示剩余12条评论

3
你可以尝试这个方法:
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));

谢谢您的回复,但我需要将其解析为浮点数(我尝试了您的建议,但对于浮点数无效)。 - mortal
这确实解析了负零(即与0.0m不同的decimal),可以通过Decimal.GetBits进行验证。当然,floatdecimal具有不同的范围和表示方式,因此是否是解决方案取决于具体情况。 - Jeroen Mostert
1
我不是在玩文字游戏,但根据你的主题,我假设你的最终目标是仅解析有符号的0。我认为它会解析成功。 - Gaurang Dave

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接