none
double型の有効桁数について RRS feed

  • 質問

  • こんにちは

    VS2007+CS環境です。

    昨日「標準の数値書式指定文字列の固定小数点 ("F) 書式指定子」について質問させていただき、回答をいただいたのですが、その後、腑に落ちない点が出てきました。

    double型の有効桁数は15~16桁というのは理解できるのですが、ヘルプでは

      ・Math.PI = 3.14159265358979323846 → 21桁

      ・Math.E = 2.7182818284590452354 → 20桁

    と定義されています。

    PIとEには何か秘密があるのでしょうか?

    初歩的な問題ですが、ご存知の方がいらっしゃれば御教示ください。

    2011年8月3日 0:02

回答

  • 外池と申します。私が思うに、ヘルプの記載があまり良くない(精度的に意味がない桁まで示している)と思います。もしかすると、Microsoft殿のソースコードの中でも、この値を明示してハードコーディングされているかもしれませんが・・・、この後示すように誤りではありませんが、意味があるものでもありません。

    意味があるのは、やはり、15桁~16桁程度です。

    Math.PIと、Marh.Eの値を、書式指定子"R"で出力してみてください。これは、内部の二進数表現と、人間が読める十進数表現の変換が一意に決まるように、必要十分な桁数で出力するものです。そうしますと、Math.PIは「3.1415926535897931」、Math.Eは「2.7182818284590451」と出力されます。(厳密には、現行の.Net Framework(C#など)におけるDouble型の実装において。)

    ちなみに、ドキュメントにある「3.14159265358979323846」と「2.7182818284590452354」という十進数表現の数値をDouble型の変数に代入してやれば、確かに、Math.PIと、Math.Eに一致します。(厳密には、==演算子がTrueを返す、つまり、内部の二進数表現が同じ。)

    もう少し、詳しく見てみます。試しに、ドキュメントの数値の一番末尾の数値を±1変更して、Double型の変数に代入してやります。それでも、代入結果はMath.PIや、Math.Eに一致します。一桁上に丸めて、「3.1415926535897932385」や「2.718281828459045235」にしても一致します。この桁数で一番末尾の数値を±1変更してみると、なお一致。こんな風にやっていると、どこまで「適当」なんだ?(内部の二進数表現の精度に照らして、どうでもいい数字を何桁末尾に表示しているのか?)ということが判ります。

    ただ、桁数を多く表示することを擁護もしておきます。C#の場合、Double型の精度が、IEEE754に従って仮数部が52bit+1bit(仮想)の53bitと厳密に決まっています。しかし、他の言語や他のプラットフォームでは、Double型の精度が多少異なっていてることもありえて、それでも円周率や自然対数の底を同じ十進数表現の定数で表しておきたい、という要求はあると思います。なので、20桁程度(十進数)の表現で定数を示しているのだと思います。


    (ホームページを再開しました)

    • 回答としてマーク mokosan 2011年8月3日 1:42
    2011年8月3日 1:20

すべての返信

  • Math.E の 2.7182818284590452354 に続く値が0だからだと思いますよ。

    別に各々21桁と20桁を明確に定義しているという認識は違っていると思います。

    2011年8月3日 0:43
  • 気持ちの問題です。Mathクラスのソースコードにはヘルプに書かれている値、つまり精度以上の数値が書かれていました。

    public static class Math {
      ...
      public const double PI = 3.14159265358979323846;
      ...
    }

    とは言え

    Console.WriteLine("{0:F20}", Math.PI);
    →3.14159265358979000000

    となります。

    C#とは全く無関係な余談ですが、Intel CPUはdouble(64bit)値もfloat(32bit)値もCPU内部では80bit値で計算しています。そのため、double定数を使う代わりに FLDPI(π値をレジスターにロード)という命令があり、80bit計算できるようになっています。
    # という余談もSSEの登場でFLDPI命令を使わないようになってさらに無関係になってますが…。

    2011年8月3日 1:07
  • ドキュメントがおかしくて、実際には 3.14159265358979 までで打ち切られていると思います。

    また、精度に関しては、正確には2進数で52ケタなので、10進数だと15ケタになる場合と16ケタになる場合があります。


    IWANAGA Nobuyuki
    2011年8月3日 1:08
  • 外池と申します。私が思うに、ヘルプの記載があまり良くない(精度的に意味がない桁まで示している)と思います。もしかすると、Microsoft殿のソースコードの中でも、この値を明示してハードコーディングされているかもしれませんが・・・、この後示すように誤りではありませんが、意味があるものでもありません。

    意味があるのは、やはり、15桁~16桁程度です。

    Math.PIと、Marh.Eの値を、書式指定子"R"で出力してみてください。これは、内部の二進数表現と、人間が読める十進数表現の変換が一意に決まるように、必要十分な桁数で出力するものです。そうしますと、Math.PIは「3.1415926535897931」、Math.Eは「2.7182818284590451」と出力されます。(厳密には、現行の.Net Framework(C#など)におけるDouble型の実装において。)

    ちなみに、ドキュメントにある「3.14159265358979323846」と「2.7182818284590452354」という十進数表現の数値をDouble型の変数に代入してやれば、確かに、Math.PIと、Math.Eに一致します。(厳密には、==演算子がTrueを返す、つまり、内部の二進数表現が同じ。)

    もう少し、詳しく見てみます。試しに、ドキュメントの数値の一番末尾の数値を±1変更して、Double型の変数に代入してやります。それでも、代入結果はMath.PIや、Math.Eに一致します。一桁上に丸めて、「3.1415926535897932385」や「2.718281828459045235」にしても一致します。この桁数で一番末尾の数値を±1変更してみると、なお一致。こんな風にやっていると、どこまで「適当」なんだ?(内部の二進数表現の精度に照らして、どうでもいい数字を何桁末尾に表示しているのか?)ということが判ります。

    ただ、桁数を多く表示することを擁護もしておきます。C#の場合、Double型の精度が、IEEE754に従って仮数部が52bit+1bit(仮想)の53bitと厳密に決まっています。しかし、他の言語や他のプラットフォームでは、Double型の精度が多少異なっていてることもありえて、それでも円周率や自然対数の底を同じ十進数表現の定数で表しておきたい、という要求はあると思います。なので、20桁程度(十進数)の表現で定数を示しているのだと思います。


    (ホームページを再開しました)

    • 回答としてマーク mokosan 2011年8月3日 1:42
    2011年8月3日 1:20
  • 質問する前に実際に Math.PI や Math.E を出力してみましたか?
    2011年8月3日 1:24
  • aviator_様

    佐祐理様

    iwanaga様

    返信ありがとうございます。

    試してみました。

     double d1 = Math.Pi * 1.0; → 3.1415926535897931

     double d2 = Math.Pi * 1.000; → 314.15926535897933

    ウォッチウィンドウには上記のように表示されれています。

    下2桁が「31」と「33」となっているのは丸め誤差としても、合点がいきません。

    更に

     string s1 = string.Format("{0:F20}", d1); → "3.14159265358979000000"

     string s2 = string.Format("{0:F20}", d2); → "314.15926535897900000000"

    ますます合点がいかなくなりました。

    2011年8月3日 1:25
  • 外池様

    丁寧な解説、ありがとうございます。

    ヘルプを読み直してみると、内部的には17桁を保持しているとの記述がありました。

    理屈では理解できるのですが...

    ちなみに先ほど返信した記述内に

    >double d2 = Math.Pi * 1.000; → 314.15926535897933

    とありましたが

    >double d2 = Math.Pi * 100.0; → 314.15926535897933

    の誤りでした。

    ともあれ、double型に高精度を期待することはやめて、decimal型に期待をかけることとします。

    ありがとうございました。

    2011年8月3日 1:42
  • 外池です。

    結構大事な議論なので、間違いがないように記載してください。

    double d1 = Math.Pi * 1.0; → 3.1415926535897931
    double d2 = Math.Pi * 1.000; → 314.15926535897933

    の2行目は、間違ってますよね? 「1.000」を乗じているのではなく、「1,00」、あるいは、「100」ですよね? この前提で議論します。

    -------------以下、本論です。------------

    2進数の内部表現同士の乗除算において最も末尾のbitをどのように処理するかの定義と、2進数の内部表現を十進数に変換する時に最も末尾の桁をどのように処理するかの定義は、「独立」です。mokosanは、この二つをごちゃ混ぜにして観察しているので、合点がいかないのだと思います。

    前者の2進数同士の演算については、私の知る範囲では、IEEEの規格を見たり、プロセッサのメーカーが公開している仕様の資料を見るなりして、面倒ではありますが厳密に追っていけると思います。

    後者の、十進数の変換については、ソフトウェアの実装によってどうなるかマチマチです。String型のFormatメソッドでも、書式指定子によって、最後の桁の丸め方はマチマチだと思いますので、こちらを厳密に追うことは非常に難しいと思います。

    mokosanが求める「合点」が、前者に関するものなら、2進数のままの内部表現を覗いて考察するようにしてください。もし、後者の十進数表現に関するものなら、私なら「最後の桁のことなんて気にしない」とアドバイスします。

    前者と後者を組み合わせて議論したいなら、諦めた方がいいです。唯一の解決は、内部でも10進数表現として、基数変換における丸めの問題を排除して、その上で、演算における丸めの問題に集中すべきです。


    (ホームページを再開しました)

    2011年8月3日 1:47
  • 外池です。老婆心ながら・・・、decimal型で加減乗除だけなら良いですが・・・、

    平方根なんかどうしますか? 円周率やら自然対数の底なんかを話題にされているので、気になるところです。三角関数や指数関数、対数関数などの高精度計算は、どう実装されますか?

    decimal型をMathクラスのメソッドに放り込んでも内部演算はDouble型からね? 本当に高精度を実現しようとすると、全部自前でプログラムしないとダメですよ?

    すごく深い問題にはまりこんでいるようにお見受けします。

    ちなみに・・・、この問題を広範に解決している例があります。
    http://keisan.casio.jp/

    計算機メーカーが大々的に取り組むレベルの問題だと私は感じています。

     


    (ホームページを再開しました)
    2011年8月3日 2:03
  • "C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC#\Specifications\1041\CSharp Language Specification.doc" C#言語仕様 4.1.6 浮動小数点型から引用します。

    浮動小数点演算は、結果の型より高い精度で実行できます。たとえば、一部のハードウェア アーキテクチャは、double 型より範囲が広く精度が高い "extended" や "long double" のような浮動小数点型をサポートし、すべての浮動小数点演算をこの高精度型を使って暗黙に実行します。このようなハードウェア アーキテクチャが、低精度の浮動小数点数を扱うときにも高負荷の演算を行う設計になっている場合、パフォーマンスと精度の両方を低下させるような実装を強制することは望ましくないため、C# ではすべての浮動小数点演算に高精度の型を使用できます。より高い精度の結果が必要な場合を除き、これによって大きな違いが出ることはあまりありません。ただし、x * y / z のような形式の式で、乗算において double の範囲を超える結果になり、その後の除算で一時的な結果が double の範囲内に戻るような場合には、より広い範囲で式が評価されることにより、無限大ではなく有限の結果が生成される可能性があります。

    先に挙げた80bitについて言及されていました。SSEを搭載していない古いコンピューターの場合、内部演算を80bitで処理することもあります。

    それからデバッガーはstring.Format()を実行しているわけではなく独自の表示を行っているため、string.Format()とは精度が異なることがあります。

    2011年8月3日 2:12
  • 外池です。

    佐祐理さんの引用されている文章、そのように説明されているのは事実ですが、原文の英語とは相当にかけ離れた和訳になっているように感じられて・・・、本件で言及することが適切かどうか、よくわからないです。

    引用されている文章は、要約としては、

    「(C#の仕様として、floatとdoubleを浮動小数点数として定義しているが) パフォーマンスと精度の双方を犠牲にする実装を要求するよりは、もっと精度の高い浮動小数点型を定義して使用することも認める。それは、プロセッサが低い精度の演算を悪いパフォーマンスで行う可能性がある場合に限る。」

    というものと理解しています。これは、プロセッサが内部的に精度の高い演算を行っている場合であって、その精度と同じ浮動小数点型を新たにC#の言語に取り入れる可能性を述べているのではないでしょうか?

    で、それもかなり制限をかけてハードルを高くしていると思います。例えばですが、C#の64bit-Doubleとプロセッサの80bit-Doubleの変換のコストがえらく高いとか、プロセッサに64bit-Doubleでそのまま計算する機能があるがこれがトンデモなくタコなエミュレーションだとか。

    要するに、プロセッサが80bit-Doubleで計算する仕様になっていて、C#の側も80bit-Doubleを定義してやらないと、えらくパフォーマンスが悪くなる状況のみを説明しているのではないかと。


    (ホームページを再開しました)

    2011年8月3日 2:33
  • パフォーマンス云々に関係なく、「64bitを超える精度で計算される場合がある」という意味で引用しましたが。この範囲であれば原文と内容はかけ離れていないと思いますがいかがでしょうか?

    実際問題、x87で64bit計算するにはFSTPでメモリに書き出す必要があり遅くなるのは確実かと。(一部命令は64bit精度に落とすこともできるようですが。)

    2011年8月3日 3:49
  • 外池です。「64bitを超える精度で計算される場合がある」、「そういう計算をC#は許す」ということですね。佐祐理さんの1行の要約で私も理解できました。

    すいません、私の意訳の中で、C#の言語の中にdoubleを超える、もっと高精度な浮動小数点型を導入することに言及しましたが、それは間違いですね。プロセッサの中で80bitで計算していても、それは「任せる」ということで。

    ただ、まぁ、どっちでもいいです。(笑)

    mokosanが「合点」されるか、されないか・・・、実のところ、本質的に、32bitだろうが、64bitだろうが、80bitだろうが、関係ないわけで・・・。decimal型を採用しても、小数を有限精度で表現することによる計算結果の丸め操作の問題は、どこまでも消えないわけで。本当のところ何桁の精度を要求しているのか、mokosan自身の要求仕様の情報がなければ、具体的な解決は見られないと思っています。


    (ホームページを再開しました)
    2011年8月3日 4:18
  • 高精度に関しては、実際C#では以下のようなことが起きるようですので、参考までにご紹介しておきます。

    float と 80bit FPU
    http://d.hatena.ne.jp/saiya_moebius/20090111/1231669765

     


    ★良い回答には回答済みマークを付けよう! わんくま同盟 MVP - Visual C# http://d.hatena.ne.jp/trapemiya/
    2011年8月3日 5:02
    モデレータ
  • 外池です。VS2008のC#で少し書き直してやってみたら・・・、Debug(最適化ナシ)だと同じ結果。

      class Program {
        static public float f1 = 2.82323f;
        static public float f2 = 2.3f;
        static public float member;
    
        static public float Calc() {
          return f1 * f2;
        }
    
        static void Main(string[] args) {
          member = Calc();
          float local = Calc();
    
          Console.WriteLine(member == Calc());
          Console.WriteLine(local == Calc());
          Console.WriteLine((f1 * f2) == Calc());
          Console.WriteLine((f1 * f2) == (f1 * f2));
        }
      }
    


    でも、最適化アリのReleaseでやってみたら、False, False, True, Trueでした。


    (ホームページを再開しました)
    2011年8月3日 5:22
  • 一番最初のコメントからSSEについて言及していますが、もろにその影響が。

    x64ターゲットにするとDebug / ReleaseともにすべてTrueになります。これはx64命令セットにはSSEが必ず存在し、そのSSEにはfloat(32bit精度)での演算命令があります(乗算ならMULSS)。.NET FrameworkではそのSSE命令を使うため、32bit精度で計算されることになります。

    x86ターゲットにすると外池さんと同じ結果になりました。

    2011年8月3日 5:38
  • 外池さん、 佐祐理さん、検証していただき、ありがとうございます。勉強になりました。
    私が紹介したページに書いてありますが、doubleやfloatやらで比較するようなコードは危ないってことですね。

     


    ★良い回答には回答済みマークを付けよう! わんくま同盟 MVP - Visual C# http://d.hatena.ne.jp/trapemiya/
    2011年8月3日 5:45
    モデレータ
  • 外池です。

    私の仕事の範囲では、浮動小数点数で、「==」や「!=」の比較は、実用上は、絶対と言って良いほど、やりません。(NaNの検知に使うことはありますが。) 今回のように、bitごとに変化があるかどうかを検知する場合にだけ、使えると思います。

    今・・・、doubleだけの計算で、メモリ上の64bit Doubleと、プロセッサ上の80bit Doubleと、計算結果に差異がでる例を、ゴチョゴチョやっているところです・・・。なんか、上手い方法ないですかね。

    PI/2から少しずらしたところで、SinとAsinを繰り返して、値がずれていくスピードを検知しようかとやているところ・・・。

    -------追記------

    Math.Sin()は、fsin命令に展開されるけど、Asinは、引数をプッシュして(この時点で64-bit)か? どこぞの関数を呼びに行ってるので、ダメか。

    trapemiyaさんの紹介された記事と同じような感じで、doubleの加減乗除でやるしかないかも、です。

    OTZ


    (ホームページを再開しました)
    2011年8月3日 5:55
  • 外池さん、 佐祐理さん、検証していただき、ありがとうございます。勉強になりました。
    私が紹介したページに書いてありますが、doubleやfloatやらで比較するようなコードは危ないってことですね。

    外池さんも書かれてますが、一般に浮動小数点の数値は=などで比較するものではないですね。
    =での比較を使うのはむしろ特殊な事情がある場合になると思います。
    2011年8月3日 7:25
  • 外池です。doulbeの加減乗除をいくつか試してみましたが、どうも、プロセッサの80bit浮動小数点演算のご利益を感じる結果を得られずに居ます。とりあえず、次のようなプログラムを書いてみました。

    double e = 2.2204460492503131E-16;
    bool b = ((e*0.5 + 1.0) == (e*0.25 + 1.0));
    Console.WriteLine(b);
    
    ここで、「2.2204460492503131E-16」という数値は、1.0に加えると、Doubleの仮数部の一番末尾(52bit目)に1が立つ数値です。もし、80bit演算ができているなら、上記の比較演算ではfalseとなって欲しいところですが、trueになってしまいます。

    デバッグのとき、比較演算の部分を逆アセンブルを表示させると。

    0000002c fld   qword ptr [ebp-0Ch] 
    
    0000002f fmul  dword ptr ds:[038E15C0h] 
    
    00000035 fld1    
    
    00000037 faddp  st(1),st 
    
    00000039 fld   qword ptr [ebp-0Ch] 
    
    0000003c fmul  dword ptr ds:[038E15C8h] 
    
    00000042 fld1    
    
    00000044 faddp  st(1),st 
    
    00000046 fcomip  st,st(1) 

    となりました・・・。これは、意図した80bit演算になっていないのでしょうか? (すいません、命令セットに詳しくないもので、調べながらやってます。)


    (ホームページを再開しました)
    2011年8月3日 7:30
  • デバッグ中に浮動小数点レジスターを表示すると CTRL = 027F となっていました。8~9bitは精度制御フィールドで値 10b は double 精度を意味します。

    上のコメントで「一部命令は64bit精度に落とすこともできるようですが」と書きましたが、FADD、FMUL辺りがこれに該当し、64bit精度で計算しています。FSIN辺りはこの精度制御フィールドの影響を受けないため、80bit精度で計算されると思います。

    2011年8月3日 7:44
  • 外池です。環境を書いていませんでしたね。まぁ、当たり前のようにx86でやってます。

    で、fsinの計算結果を、1.0と比較したいと思うのですが、fcomipが80bitで比較してくれないと・・、ご利益を検証できないような。

    -----追記 できました-----

       double pi2 = Math.PI * 0.5;
    
       double pi2m = pi2 * 0.999999999;
    
       Console.WriteLine(Math.Sin(pi2m)==1.0);
    
       double s = Math.Sin(pi2m);
    
       Console.WriteLine(s == 1.0);
    
       Console.WriteLine(s.ToString("R"));
    
    
    
    

    とやってやると、出力は、false、true、1となりました。

    3行目の比較の部分は、

    0000003e fld   qword ptr [ebp-14h] 
    
    00000041 fsin    
    
    00000043 fstp  qword ptr [ebp-24h] 
    
    00000046 fld   qword ptr [ebp-24h] 
    
    00000049 fld1    
    
    0000004b fcomip  st,st(1) 

    です。

    -------追記の2-------

    動作の中身はかなり勉強になりましたが、実用性の観点で費やした時間・・・OTZ 


    (ホームページを再開しました)

    2011年8月3日 7:56
  • 外池です。

    逆アセンブルしたリストが間違っていました。上記のものは、Debugモードでコードの最適化がなされていません。fsinのあと、fstpとfldがありますが、これでは、fsinの結果が64bit Doubleに丸められてしまいます。実際、この展開のコードだと、結果は、True、True、1になっていました。

    最適化して、False、True、1の結果になるとき、逆アセンブルのリストは以下のとおりです。

    0000002e fld     qword ptr ds:[00CD0148h] 
    00000034 fsin       
    00000036 fld1       
    00000038 fcomip   st,st(1) 
    

     


    (ホームページを再開しました)
    2011年8月3日 11:02
  • ★良い回答には回答済みマークを付けよう!
    参考になった投稿として投票してもいいんですよ?
    2011年8月6日 11:11
  • その通りですね。失念しておりました。ご指摘ありがとうございます。m(_ _)m

     


    ★良い回答には回答済みマークを付けよう! わんくま同盟 MVP - Visual C# http://d.hatena.ne.jp/trapemiya/
    2011年8月7日 3:04
    モデレータ