web-dev-qa-db-ja.com

符号付きゼロを解析する方法は?

符号付きゼロを解析することは可能ですか?私はいくつかのアプローチを試しましたが、誰も適切な結果を与えませんでした:

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);
19
mortal

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実装にはNumberBufferToDoubleInternalCall(純粋なC\C++で実装)としてありますが、同様のことをしていると思います。

17
Evk

更新された結果

概要

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の累乗だけがaiだけで大きいため、わずかに速くなるswitchステートメント。

背景

私が得たさまざまなコメントと、これに入れた仕事のおかげで、わかりました。この投稿を、入手できる最も正確なベンチマークで書き直したいと思いました。そしてそれらの背後にあるすべてのロジック

したがって、この最初の質問が出たとき、idは独自のベンチマークフレームワークを作成しており、これらのもののクイックパーサーを作成して安全でないコードを使用するのと同じように、10回のうち9回、同等のフレームワークよりも速くこれを取得できます。

最初はこれは簡単でした。単純に、小数点以下の桁数を解析するロジックを記述しました。かなりうまくいきましたが、テストデータは 'fを使用していたため、最初の結果は正確ではありませんでした。 'フォーマット指定子。これにより、より大きな精度の数値が0のみの短いフォーマットに変換されます。

結局、指数表記、つまり1.2324234233E+23を処理するための信頼できる解析を作成できませんでした。数学を機能させる唯一の方法は、BIGINTEGERおよび多くのハックを使用して、正しい精度を浮動小数点値に強制することでした。これは非常に遅くなった。 IEEE仕様のフロートに行って、ビット単位で構築するための計算を試みましたが、これはそれほど難しくありませんでしたが、式にはループがあり、正しく理解するのが複雑でした。結局私は指数表記をあきらめなければなりませんでした。

だからこれは私が終わったものです

私のテストフレームワークは、入力データに対して文字列として10000フルートのリストを実行します。これは、テスト全体で共有され、テスト実行ごとに生成されます。テスト実行は、各テストを実行するだけです(各テストで同じデータであることを思い出してください)。結果を上げて平均化します。これは、可能な限り優れています。実行を1000以上に増やすことができますが、実際には変更されません。この場合、基本的に1つの変数(フロートの文字列表現)を取るメソッドをテストしているため、セットに基づいていないため、これをスケーリングするポイントはありませんが、フロートの長さを変えるために入力を微調整できます。 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;
}

テストデータ

// 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よりも少し遅くなります。

9
TheGeneral

あなたはこれを試すことができます:

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));
3
Gaurang Dave