トップ回答者
IEEE 754-2008について

質問
-
前置きとして浮動小数点数については、「表現できない数値があり、その場合は近似値で格納されるデータ型」程度の理解度しかりません。
ここから本題です。
.NET Core 3.0 から浮動小数点数の処理がIEEE 754-2008に準拠するよう変更されたと知りました。
また、.NET FrameworkではIEEE 754に準拠していると理解しています。
この2つは、整数型との相互変換や、浮動小数点数での四則演算時の結果が異なると理解してよろしいでしょうか?
それから、IEEE 754に対してIEEE 754-2008は計算の精度が向上すると理解してよろしいでしょうか?
また、場違いでなければ、Visual C++(.NET Frameworkではない)はどのような対応状況になっているのかお教えいただけますと幸いです。
以上、よろしくお願い致します。
回答
-
基本的に、IEEE-754 1985と2008に大きな非互換はないです。
.Net Core 3.0のIEEE-754 2008対応ですが、下記のように、文字列変換がらみと、いくつかの関数の追加となっています。
https://docs.microsoft.com/ja-jp/dotnet/core/whats-new/dotnet-core-3-0#ieee-floating-point-improvements
https://devblogs.microsoft.com/dotnet/floating-point-parsing-and-formatting-improvements-in-net-core-3-0/なので、特殊な例外(新たに追加されたFMAを使う、とか、指数が極端に大きかったり小さかったりしたときのDouble.Parse/ToString)を除いて、四則演算の結果は異なったりしませんし、計算精度が向上したりはしません。
なお、C++については、https://docs.microsoft.com/ja-jp/cpp/porting/visual-cpp-change-history-2003-2015 に<q>NaNs 値や無限大値などの特殊なケースの入力に関する IEEE-754 仕様および C11 Annex F 仕様に対する準拠を改善するために、多くの変更が数値演算ライブラリに対して加えられています</q>とあり、ここで参照されているのはIEEE-754 2008なので、対応しているものと思われます。
jzkey
- 回答としてマーク VB User1 2019年10月15日 11:47
-
整数型との相互変換や、浮動小数点数での四則演算時の結果が異なると理解してよろしいでしょうか?
C++ 界隈の事は分からないので、他の方にお任せするとして。。。
int 値を double 型に拡大変換することに関して仰っているのだとしたら、それは .NET Core 3.0 であっても何ら変わりはありません。Double 型は、Int32 や UInt32 型で表現可能な整数すべてを誤差なく格納することができるためです。double 型から int 型への縮小変換の場合は、端数部分をどう処理するかによって異なってくる可能性もありますが。
大きな違いというのであれば、IEEE 754-2008 では 5 種類の丸めアルゴリズムに合わせ、Math.Round や MathF.Round メソッドで使用される MidpointRounding 列挙型も同様に、最近接丸め の 2 種だけでなく、方向丸め の 3 種 もサポートされるようになった点が挙げられます。
- 0: ToEven … 最近接丸め。中間値なら偶数方向に丸め。(一番低い仮数ビットが 0 になる標準動作)
- 1: AwayFromZero … 最近接丸め。中間値なら 0 から遠ざかる方へ丸める。(正数はより大きく、負数はより小さく)
- 2: ToZero … 方向丸め。0 に近い側へ丸める。(いわゆる切り捨て)
- 3: ToNegativeInfinity … 方向丸め。負の無限大に近い側に丸める。(いわゆる切り下げ)
- 4: ToPositiveInfinity … 方向丸め。正の無限大に近い側に丸める。(いわゆる切り上げ)
また、文字列化する際の書式表現について、パーサーが多少変化しています。
身近なところだと、Math.PI.ToString() の結果が、
"3.14159265358979" から
"3.141592653589793" となるよう見直されています。(※注釈)これは文字列表現が変わったというだけであり、Math.PI の値そのものが修正されたというわけではありません。
Math.PI が返す Double 値の内部バイナリは、今も昔も 0x400921FB54442D18 ( 18-2D-44-54-FB-21-09-40 ) のままです。この変更により、従来は引数無し ToString によって文字列化した場合に、
Double.Parse(Math.PI.ToString()) < Math.PI になっていたものが、.NET Core 3.0 にて
Double.Parse(Math.PI.ToString()) == Math.PI と、正しく復元できるようになりました。破壊的変更ではあるので、アプリケーションによっては、この違いが影響を受けるかもしれません。
とはいえ元々、数値を文字列化して永続化する際には、"R" 書式を用いた方が安全であるとされていたので、引数無しの ToString がそもそも適切ではなかったという見方はあります。
実際、 .NET Framework および .NET Core 2.2 以下の従来バージョンにおいても、Double.Parse(Math.PI.ToString("R")) としていた場合には、正しく Math.PI に復元されるようになっています。ただその R 書式についても変化が生じており、従来は
"3.1415926535897931" を返していたものが
"3.141592653589793" を返すようになっているようです。ついでに言えば、Math.PI.ToString("N20") とした場合も、
"3.14159265358979000000" から
"3.14159265358979311600" に変化していました。下記もご覧ください。
Floating-Point Parsing and Formatting improvements in .NET Core 3.0
.NET Core 3.0 の新機能 - IEEE 浮動小数点の改良
※ 蛇足: Math.PI が返す浮動小数点数の 0x400921FB54442D18 というバイナリは、二進小数 「11.001001000011111101101010100010001000010110100011」を意味する値です。これを十進小数に戻すと「3.141592653589793115997963468544185161590576171875」という値になって、小数点以下16桁目以降の値が実際の円周率と違ってしまいます。といっても、そもそも double の有効桁数は 52+1bit 相当 (十進数で換算すると、正規化数の時で約 15.9545 桁分、非正規化数では 15.654 桁未満)しかありませんので、有効桁数の範囲で言えば、精度的には十分に正しい値となります。- 編集済み 魔界の仮面弁士MVP 2019年10月13日 14:49
- 回答としてマーク VB User1 2019年10月15日 11:50
-
「現在のスレッドの内容として不適切でしたら、ご指摘ください。」
いえ、VB User1さんが「ここに差があるかも」と睨んでおられることと、スレッドの趣旨は一致しているので、不適切とは思いませんが・・・、しかし、「ここに差があるかも」と睨んでおられることは、小生の理解からすると、的外れになっていると思います。CPUの動作を調べるべきじゃないかと。で、CPUの動きを調べるとなると、実行時最適化の領域に踏み込むことになり、非常に難解です。数値演算ユニットFPUのレジスタ長が80bit、CPUの通常レジスタやメモリ格納時には64bit長なんていう事実もあります。
wavファイルの生成結果でチェックすると、確かに差があるわけですが・・・、みな、1bitだけの差ですよね。これ、再確認ですが、何バイトか組になっているデータの、LSB(一番小さい桁のビット)ですよね? 小生なら気にしません。
.Net Frameworkの場合、ドキュメントに浮動小数点数の処理がIEEE754に準拠する旨が書かれていたとしても、浮動小数点数の数値演算(文字列との変換は調べていないので、よくわかりません。)は、CPU(FPUの場合もあり。以下同じ。)に全面的に依存しています。このことはコンパイル結果の中間言語や、さらに実行時の機械語への展開状況をみればわかります。CPUの計算結果を、改めて、IEEE754に準拠しているかどうかチェックする、あるいは、IEEE754に準拠するように修正するような機械語は生成されません。さらに、32bitシステムの場合(x86を指定したコンパイルの場合)はFPUを使うが、64bitシステムの場合(x64を指定してコンパイルの場合)はFPUを使わないという差もあります。浮動小数点数の計算の順序や仕組みに、差が生じるわけです。
.Net Core 3.0において、IEEE754-2008に準拠することをチェックする仕組みが新たに加わったかもしれませんが、上記下線部をチェックされれば良いんじゃないかと思います。(ただ・・・、小生の理解では、そんなことをすると、ものすごく遅くなってしまいます。) なので、想像ですが、やはり、CPUの計算結果をそのまま使っているんじゃないかと。
- 回答としてマーク VB User1 2019年10月15日 11:51
-
最初から一貫して「四則演算」と書かれていたのでコメントを躊躇っていたのですが…Math.Sin / Math.Cosのような三角関数、その他は、数学的に値は一意に定まりますが、その値を導き出すためには一定のコストが掛かります。
そのため、基本的には近似値が使われており、算出アルゴリズムはプロセッサー毎に異なります。.NET Coreを使用した際に結果が変わったとのことですが、そうでなくても実行環境毎に結果が異なる処理であることを理解しておいてください。# ということがわかっていれば、.NET Coreでの違いなど気にすべきでないことはわかるはずです。
- 回答としてマーク VB User1 2019年10月15日 11:48
すべての返信
-
小生の理解では、「浮動小数点数の計算結果は、bit単位で比較すれば、異なり得る」です。それは、IEEE 754だろうが、IEEE 754-2008だろうが、関係ないです。同じIEEE 754の範囲内であっても、です。
というのも、コンパイルの仕方によって、あるいは、コンパイル後でも実行時の最適化によって、同じ入力に対して同じ順序(レジスタとメモリのやりとりを含む)で計算されるとは限らないからです。これが原因で丸め誤差の伝搬に微妙な差が生じ得ます。小生の経験では、デバッグ(最適化なし)コンパイルと、リリース(最適化あり)コンパイルで結果が異なることがありました。
その差をどう管理するか、仕様でよく定めて、プログラマがそれに応える必要があります。
- 編集済み 外池 2019年10月15日 7:29
-
基本的に、IEEE-754 1985と2008に大きな非互換はないです。
.Net Core 3.0のIEEE-754 2008対応ですが、下記のように、文字列変換がらみと、いくつかの関数の追加となっています。
https://docs.microsoft.com/ja-jp/dotnet/core/whats-new/dotnet-core-3-0#ieee-floating-point-improvements
https://devblogs.microsoft.com/dotnet/floating-point-parsing-and-formatting-improvements-in-net-core-3-0/なので、特殊な例外(新たに追加されたFMAを使う、とか、指数が極端に大きかったり小さかったりしたときのDouble.Parse/ToString)を除いて、四則演算の結果は異なったりしませんし、計算精度が向上したりはしません。
なお、C++については、https://docs.microsoft.com/ja-jp/cpp/porting/visual-cpp-change-history-2003-2015 に<q>NaNs 値や無限大値などの特殊なケースの入力に関する IEEE-754 仕様および C11 Annex F 仕様に対する準拠を改善するために、多くの変更が数値演算ライブラリに対して加えられています</q>とあり、ここで参照されているのはIEEE-754 2008なので、対応しているものと思われます。
jzkey
- 回答としてマーク VB User1 2019年10月15日 11:47
-
整数型との相互変換や、浮動小数点数での四則演算時の結果が異なると理解してよろしいでしょうか?
C++ 界隈の事は分からないので、他の方にお任せするとして。。。
int 値を double 型に拡大変換することに関して仰っているのだとしたら、それは .NET Core 3.0 であっても何ら変わりはありません。Double 型は、Int32 や UInt32 型で表現可能な整数すべてを誤差なく格納することができるためです。double 型から int 型への縮小変換の場合は、端数部分をどう処理するかによって異なってくる可能性もありますが。
大きな違いというのであれば、IEEE 754-2008 では 5 種類の丸めアルゴリズムに合わせ、Math.Round や MathF.Round メソッドで使用される MidpointRounding 列挙型も同様に、最近接丸め の 2 種だけでなく、方向丸め の 3 種 もサポートされるようになった点が挙げられます。
- 0: ToEven … 最近接丸め。中間値なら偶数方向に丸め。(一番低い仮数ビットが 0 になる標準動作)
- 1: AwayFromZero … 最近接丸め。中間値なら 0 から遠ざかる方へ丸める。(正数はより大きく、負数はより小さく)
- 2: ToZero … 方向丸め。0 に近い側へ丸める。(いわゆる切り捨て)
- 3: ToNegativeInfinity … 方向丸め。負の無限大に近い側に丸める。(いわゆる切り下げ)
- 4: ToPositiveInfinity … 方向丸め。正の無限大に近い側に丸める。(いわゆる切り上げ)
また、文字列化する際の書式表現について、パーサーが多少変化しています。
身近なところだと、Math.PI.ToString() の結果が、
"3.14159265358979" から
"3.141592653589793" となるよう見直されています。(※注釈)これは文字列表現が変わったというだけであり、Math.PI の値そのものが修正されたというわけではありません。
Math.PI が返す Double 値の内部バイナリは、今も昔も 0x400921FB54442D18 ( 18-2D-44-54-FB-21-09-40 ) のままです。この変更により、従来は引数無し ToString によって文字列化した場合に、
Double.Parse(Math.PI.ToString()) < Math.PI になっていたものが、.NET Core 3.0 にて
Double.Parse(Math.PI.ToString()) == Math.PI と、正しく復元できるようになりました。破壊的変更ではあるので、アプリケーションによっては、この違いが影響を受けるかもしれません。
とはいえ元々、数値を文字列化して永続化する際には、"R" 書式を用いた方が安全であるとされていたので、引数無しの ToString がそもそも適切ではなかったという見方はあります。
実際、 .NET Framework および .NET Core 2.2 以下の従来バージョンにおいても、Double.Parse(Math.PI.ToString("R")) としていた場合には、正しく Math.PI に復元されるようになっています。ただその R 書式についても変化が生じており、従来は
"3.1415926535897931" を返していたものが
"3.141592653589793" を返すようになっているようです。ついでに言えば、Math.PI.ToString("N20") とした場合も、
"3.14159265358979000000" から
"3.14159265358979311600" に変化していました。下記もご覧ください。
Floating-Point Parsing and Formatting improvements in .NET Core 3.0
.NET Core 3.0 の新機能 - IEEE 浮動小数点の改良
※ 蛇足: Math.PI が返す浮動小数点数の 0x400921FB54442D18 というバイナリは、二進小数 「11.001001000011111101101010100010001000010110100011」を意味する値です。これを十進小数に戻すと「3.141592653589793115997963468544185161590576171875」という値になって、小数点以下16桁目以降の値が実際の円周率と違ってしまいます。といっても、そもそも double の有効桁数は 52+1bit 相当 (十進数で換算すると、正規化数の時で約 15.9545 桁分、非正規化数では 15.654 桁未満)しかありませんので、有効桁数の範囲で言えば、精度的には十分に正しい値となります。- 編集済み 魔界の仮面弁士MVP 2019年10月13日 14:49
- 回答としてマーク VB User1 2019年10月15日 11:50
-
みなさま、コメントいただき誠にありがとうございます。
Visual C++はjzkeyさまにお示しいただいたリンク先の情報から、Visual Studio 2015から対応ということになると理解をいたしました。
「整数型との相互変換や、浮動小数点数での四則演算時」の.NET Frameworkと.NET Core 3.0の違いについては
jzkeyさまによると「基本的に、IEEE-754 1985と2008に大きな非互換はないです。」
とのことですが、
魔界の仮面弁士さまによると「double 型から int 型への縮小変換の場合は、端数部分をどう処理するかによって異なってくる可能性もありますが。」
ということでご意見が分かれているように思われます。
この点についてはいかがでしょうか?
また、Int64からDoubleへの変換時は違いが発生するのでしょうか?
-
先の URL では IEEE754-2008 準拠への更新が行われた理由について、『これらの変更の目的は、すべての必要な操作を公開し、それらの動作が IEEE 仕様に準拠していることを保証することです。』と述べられています。
そして先の MidpointRounding や パーサーの変化も、そのために行われたものであると思います。変更箇所も記載されていましたよね。
そういえば、 System.Math クラスのソースコードを見てみると、.NET Framework 版の Round(double) メソッド や Shared Source の実装では処理内容が [InternalCall] になっていましたが、.NET Core 3.0 版 においてはC# での実装に置き換わっていました(一部、InternalCall も併用されていますが)。
ということでご意見が分かれているように思われます。
IEEE754-2008 で浮動小数点数の内部形式が変わったわけではありませんので、計算などに影響する大きな非互換はありません。「操作を公開」するために、Math および MathF クラスにメソッド等が追加されたのと、より正確なラウンドトリップのために、文字列書式およびパース処理に手が加えられたというのが変更の骨子です。
文字列パースの違いとしては、たとえばこのようなものがあります。
double d1 = 1.0 / double.Parse("+0.0"); double d2 = 1.0 / double.Parse("-0.0"); // 負のゼロのパースをサポート
.NET Core 3.0 で上記を実行すると、d2 が Double.NegativeInfinity すなわち負の無限大「-∞」となります。
従来バージョン(.NET Core 2.2 以下や .NET Framework 等)においては、d2 が Double.PositiveInfinity すなわち正の無限大「∞」となっていました。double d3 = Double.Parse("0.6822871999174000000"); double d4 = Double.Parse("0.6822871999174000001"); // 有効数字一杯の値をより正確にパース
double の有効桁数から言えば、上記 2 つは同じ値として解釈されるべきです。
.NET Core 3.0 で上記を実行すると、d3 と d4 のいずれも 0x3FE5D54BF743FD1B なバイナリを返します。
従来バージョンだと d4 は正しいですが、x64 環境では d3 が 1bit ずれて 0x3FE5D54BF743FD1C なバイナリを返していました。0x3FE5D54BF743FD1A は 0.6822871999173998336374324935604818165302276611328125 相当。
0x3FE5D54BF743FD1B は 0.68228719991739994465973495607613585889339447021484375 相当。
0x3FE5D54BF743FD1C は 0.682287199917400055682037418591789901256561279296875 相当。
「double 型から int 型への縮小変換の場合は、端数部分をどう処理するかによって異なってくる可能性もありますが。」
曖昧な物言いで済みません。上記は IEE754-2008 云々というよりも、浮動小数点数全般の話です。
int から double の拡大変換は誤差が生じる心配がないのに対し、その逆変換は、どうしても非可逆的になります。たとえば Math.PI を、Int32 型の変数にそのまま代入することはできませんので、端数処理と型変換が必須です。
しかし、端数部分の処理法にもいろいろあり、それぞれで処理結果も異なってきます。それゆえ一般論として語りにくかったため、縮小変換時に『どう処理するか』次第で異なる結果となる「可能性がある」という言い方をして言葉を濁してしまいました。
極端な話、ToString() してから文字列処理して丸め処理を行い、それを Parse しなおすような実装を取っていれば、今回の変更の影響を受ける可能性は高まるでしょう。
まぁ流石に文字列経由というのは極端ですが、たとえば C# で「int x = (int)doubleValue;」と書いたり、VB で「Dim x As Integer = CInt(doubleValue)」と書くことであれば珍しく無いかと思います。そしてこの場合、両者は異なった結果を返します。これは丸めモードの違い(切り捨てと偶数丸め)であって、IEEE754-2008 か否かという違いでは無いですけれどね。
あるいはこうした言語機能への依存性を減らすため、言語機能ではなく、.NET 側で用意されている Math.Round メソッドを使って、「int x = (int)Math.Round(doubleValue, 0, mode);」と書くという選択肢もあろうかと思います。これなら、言語間で同じ結果が得られることが期待されるわけですが、これについても .NET Core 3.0 で mode : MidpointRounding.ToNegativeInfinity を指定されているようなコードを .NET Framework 側に逆移植するようなケースにおいては、異なるコードに置き換えねばなりません(例えば、mode が方向丸めだった場合は、Floor / Truncate / Ceiling に差し替えるなど)。
また、Int64からDoubleへの変換時は違いが発生するのでしょうか?
.NET Core 3.0 になったことで違いが生じたか、という意味なら変化は無いでしょう。
Int32 から Double への変換処理と比べた場合の差異、という意味なら違いは生じるでしょう。Double の最大値は、Int64.MaxValue よりはるかに広いですが、
Double の有効桁数は約 15.955 桁で、Int64の約 18.965 桁より劣るため、誤差が避けられないためです。
(Int32 の有効桁数は、約 9.322 桁)0.682287199917400055682037418591789901256561279296875
-
魔界の仮面弁士さま
コメントいただきまして誠にありがとうございます。
せっかくコメントいただきましたのに、内容から私が知りたい事を読み取ることができませんでした。
誠に申し訳ございません。
・浮動小数点数型の四則演算は.NET Frameworkと.NET Core 3.0で違いが生じますか?
・整数型と浮動小数点数型の相互変換において、.NET Frameworkと.NET Core 3.0で違いが生じますか?
たとえば以下のような2つのメソッドの場合はどうでしょう。
Public Shared Function Int64toDouble(ByVal Int64Data As Int64) As Double Return Int64Data / 9223372036854775807 End Function Public Shared Function DoubletoInt64(ByVal DoubleData As Double) As Int64 Return System.Convert.ToInt64(DoubleData * 9223372036854775807) End Function
- 編集済み VB User1 2019年10月14日 3:07 誤字の訂正
-
・浮動小数点数型の四則演算は.NET Frameworkと.NET Core 3.0で違いが生じますか?
生じないでしょう。IEEE754-2008 で浮動小数点数の内部形式が変わったわけではないですし、四則計算に影響する非互換は報告されていません。
・整数型と浮動小数点数型の相互変換において、.NET Frameworkと.NET Core 3.0で違いが生じますか?
.NET Core 3.0 になったことで違いが生じるかと言えば、変化は無いでしょう。
.NET Core 3.0 の新機能に関するドキュメントにあるように、変更があったのは文字列解析(パース)および文字列化された際の書式、そして幾つかの API (列挙型メンバーやメソッド等)の追加であるとアナウンスされています。それらの影響を受けるようなコードがあるかどうかで判断してみてください。
たとえば以下のような2つのメソッドの場合はどうでしょう。
これはまず、9223372036854775807 というリテラル表記を用いるのではなく、Long.MaxValue あるいは Int64.MaxValue の定数表記を用いた方が読みやすそうです。(あるいはせめて &H7FFF_FFFF_FFFF_FFFF )
表記はさておき、このコードでは今回の変更の影響を受けません。
もしも問題が生じているが故に質問をしているのであれば、違いの生じている具体的な値を例示していただけると話が早いかと思います。
また、これらの VB コードの場合、いずれの .NET 実装においても
「Return CDbl(Int64Data) / 9.2233720368547758E+18」
「Return System.Convert.ToInt64(DoubleData * 9.2233720368547758E+18)」
に相当する IL に展開されることになるでしょう。// Int64toDouble(Long) ldarg.0 conv.r8 ldc.r8 9.2233720368547758e+18 div ret
// DoubletoInt64(Double) ldarg.0 ldc.r8 9.2233720368547758e+18 mul call int64 [System.Runtime.Extensions]System.Convert::ToInt64(float64) ret
- 編集済み 魔界の仮面弁士MVP 2019年10月14日 4:38 誤字の訂正
-
魔界の仮面弁士さま
> もしも問題が生じているが故に質問をしているのであれば、違いの生じている具体的な値を例示していただけると話が早いかと思います。
ありがとうございます。
別スレッドにしほうが良い内容かもしれませんが、
同じソースコードにより作成した.NET Frameworkと.NET Core 3.0のプログラムがあります。
それぞれのプログラムが出力したファイルに違いが生じています。
・下記結果は左が.NET Frameowrk、右が.NET Core 3.0
・出力ファイルはWAV形式(PCM,24bit,88200Hz)
・出力は24bitの整数値(3バイト、リトルエンディアン)
・ファイルサイズはいずれも101,928,294 バイト
・文字列を介した数値変換は行っておりません
複雑なプログラムの最終結果が下のものとなりますが、ソースコード内のどこで違いが発生しているのかまでは特定できてきません。
Z:\>fc /b a.wav b.wav
ファイル A.wav と B.WAV を比較しています
003AB8BA: 84 85
00564F2C: CB CC
0126127D: D5 D4
0135B342: 78 77
015C3E87: F0 EF
01A20508: 99 9A
01BAC6B2: 71 72
02459CA5: A7 A6
024EA9EC: 1F 20
0304C0A0: BD BE
04379D35: 16 17
043EF182: F8 F7
0484C883: D3 D4
051B6BC7: 09 0A
05BD5F91: B5 B6 -
少しわかってきました。
自作のWin32DLL(Visual C++ 2010プロジェクト、ビルドバージョン不明、ビルドは2014年)内の関数を呼び出したときに違いが起きることがわかりました。
Public Structure Coefs Dim a1 As Double Dim a2 As Double Dim b1 As Double End Structure <DllImport("Hogex64.DLL", EntryPoint:="RunHoge")> Private Shared Sub RunHogex64(ByVal src() As Double, ByVal dest() As Double, ByVal c As Integer, ByVal d As Integer, ByVal e As Double, ByVal f(,) As Double, ByVal g() As Coefs) End Sub
自作のWin32DLLは変更しておらず、同じバイナリです。自作のWin32DLL内では浮動小数点数の四則演算がほとんどです。
DllImportで呼び出した処理が.NET Framework と .NET Core 3.0 で違う事はありえるでしょうか?
現在のスレッドの内容として不適切でしたら、ご指摘ください。
-
「現在のスレッドの内容として不適切でしたら、ご指摘ください。」
いえ、VB User1さんが「ここに差があるかも」と睨んでおられることと、スレッドの趣旨は一致しているので、不適切とは思いませんが・・・、しかし、「ここに差があるかも」と睨んでおられることは、小生の理解からすると、的外れになっていると思います。CPUの動作を調べるべきじゃないかと。で、CPUの動きを調べるとなると、実行時最適化の領域に踏み込むことになり、非常に難解です。数値演算ユニットFPUのレジスタ長が80bit、CPUの通常レジスタやメモリ格納時には64bit長なんていう事実もあります。
wavファイルの生成結果でチェックすると、確かに差があるわけですが・・・、みな、1bitだけの差ですよね。これ、再確認ですが、何バイトか組になっているデータの、LSB(一番小さい桁のビット)ですよね? 小生なら気にしません。
.Net Frameworkの場合、ドキュメントに浮動小数点数の処理がIEEE754に準拠する旨が書かれていたとしても、浮動小数点数の数値演算(文字列との変換は調べていないので、よくわかりません。)は、CPU(FPUの場合もあり。以下同じ。)に全面的に依存しています。このことはコンパイル結果の中間言語や、さらに実行時の機械語への展開状況をみればわかります。CPUの計算結果を、改めて、IEEE754に準拠しているかどうかチェックする、あるいは、IEEE754に準拠するように修正するような機械語は生成されません。さらに、32bitシステムの場合(x86を指定したコンパイルの場合)はFPUを使うが、64bitシステムの場合(x64を指定してコンパイルの場合)はFPUを使わないという差もあります。浮動小数点数の計算の順序や仕組みに、差が生じるわけです。
.Net Core 3.0において、IEEE754-2008に準拠することをチェックする仕組みが新たに加わったかもしれませんが、上記下線部をチェックされれば良いんじゃないかと思います。(ただ・・・、小生の理解では、そんなことをすると、ものすごく遅くなってしまいます。) なので、想像ですが、やはり、CPUの計算結果をそのまま使っているんじゃないかと。
- 回答としてマーク VB User1 2019年10月15日 11:51
-
外池さま
コメントいただきまして、ありがとうございます。
> wavファイルの生成結果でチェックすると、確かに差があるわけですが・・・、みな、1bitだけの差ですよね。これ、再確認ですが、何バイトか組になっているデータの、LSB(一番小さい桁のビット)ですよね? 小生なら気にしません。
私も.NET Frameworkと.NET Core 3.0の仕様の違いであると確認ができれば、同じ出力結果を求めてはいないので気にしないつもりですが、原因は把握したいなと。
そこで、なんとか違いが発生するコードを可能な範囲で小さくまとめる事ができました。
私が記憶していた以上に色々な事をやっていました。単なる四則演算だけではありませんでした。申し訳ございません。
Dim sP() As System.Numerics.Complex Dim zP() As System.Numerics.Complex Dim order As Double Dim wc As Double Dim target As Double order = 64 wc = Math.Tan(22050 * Math.PI / 88200) zP = Nothing sP = Nothing Array.Resize(sP, Convert.ToInt32(order / 2)) Array.Resize(zP, Convert.ToInt32(order / 2)) For k As Integer = 0 To Convert.ToInt32(order / 2 - 1) Dim theta As Double = (2.0 * k + 1.0) * Math.PI / (2.0 * order) sP(k) = New System.Numerics.Complex(-1 * Math.Cos(theta), Math.Sin(theta)) zP(k) = (1.0 + wc * sP(k)) / (1.0 - wc * sP(k)) Next target = CDbl(2.0 * (zP(16).Real)) System.Windows.Forms.MessageBox.Show(target.ToString("R") & vbCrLf & BitConverter.ToString(BitConverter.GetBytes(target)))
最後のメッセージボックスの値は以下のようになりました。
.NET Framework
1.6653345369377351E-16
01-00-00-00-00-00-A8-3C
.NET Core 3.0
2.775557561562892E-16
01-00-00-00-00-00-B4-3C
可能であれば、この違いの理由を知りたいです。
-
最初から一貫して「四則演算」と書かれていたのでコメントを躊躇っていたのですが…Math.Sin / Math.Cosのような三角関数、その他は、数学的に値は一意に定まりますが、その値を導き出すためには一定のコストが掛かります。
そのため、基本的には近似値が使われており、算出アルゴリズムはプロセッサー毎に異なります。.NET Coreを使用した際に結果が変わったとのことですが、そうでなくても実行環境毎に結果が異なる処理であることを理解しておいてください。# ということがわかっていれば、.NET Coreでの違いなど気にすべきでないことはわかるはずです。
- 回答としてマーク VB User1 2019年10月15日 11:48
-
佐祐理さま
コメントいただきまして、ありがとうございます。
> そのため、基本的には近似値が使われており、算出アルゴリズムはプロセッサー毎に異なります。.NET Coreを使用した際に結果が変わったとのことですが、そうでなくても実行環境毎に結果が異なる処理であることを理解しておいてください。
> # ということがわかっていれば、.NET Coreでの違いなど気にすべきでないことはわかるはずです。
CPUの種類ごとに結果が異なる可能性があるということでしょうか。それについては理解をいたしました。ありがとうございます。
しかしながら、今回の場合は同じ実行環境で.NET Frameworkと.NET Core 3.0で違いが出たのでその理由も知りたいという事になります。「気にすべきでない」とおっしゃられてもその理由を述べられていないので、それについては納得出来ません。申し訳ございません。
-
もし、x64でコンパイルされているのであれば、三角関数もFPUは使わず四則演算に展開されるので、広い意味で「四則演算」で良いと思います。が、繰り返し、繰り返しになりますが、.Net Frameworkの中だけでも、コンパイル条件や実行環境によって、差は生じ得ます。なので、.Net Frameworkと.Net Core 3.0に差があるはずだ、という線で調べても、何も見つからないと思います。(.Net Core 3.0に興味をお持ちということは、クロスプラットフォームで実行できるようにしたい・・・ってことでしょうか。それなら、なおさら・・・)
「target」の計算結果では、数十%の差があって驚かれるかもしれませんが、引き算による精度悪化(俗に言う「桁落ち」落ち)の典型例に見えます。sP()とzP()の値の一覧を比較されてみてはいかがでしょう? (wcすら・・・、きっちり1.0000になってない可能性もりますしね。)
で、ここからは、数値計算の精度をどう管理するかの観点で重要な議論ですが、実行環境によって異なり得る結果の話なので、もう、IEEE754や、.Net Frameworkや、.Net Core 3.0とは無関係な議論になります。VB User1さんの今のプログラミング課題として、targetのこの計算結果の差は致命的に困りますか?
困るのであれば、そのような差が生じるようなソースプログラムを書くべきではありません。targetの計算を、thetaの取り得る値の範囲によって(あるいは全域でもいいですが)どれだけの精度を確保するか決めて、それを達成するようにプログラミングしなければなりません。thetaの値によって計算手順を変えることもあり得ます。
Complex型(複素数型)を使われていますが、これは、IEEE 754では定義されていませんし、CPUやFPUでも扱えません。さらに、.Net Frameworkや.Net Coreの中間言語でも扱えないので、中間言語にコンパイルされる際に、Double型を使う演算に展開されているはずです。この展開の実装が、.Net Frameworkと.Net Core 3.0で差があることは、十分に考えられます。計算精度を管理したいのであれば、Complex型は使わずに、独自に複素数を扱う手法をプログラミングしなければなりません。最終的には、種々の実行環境でテストしてみて、最初に決めた計算精度が達成できていることを確認することは、プログラマ側の責任になります。IEEE754や.Net Frameworkや.Net Core 3.0の仕様に依存することはできないです。
その点で、提示頂いたプログラムのどこで差が出ているのか知りたい、ということであれば、それは大事ですね。その場合、各行ごとに実行しつつ、変数の値をToString("R")で書きだして相互比較して、と綿密にやるしかないです。
困らないのであれば、気にしないことです。
- 編集済み 外池 2019年10月14日 12:23
-
外池さま
ありがとうございます。
今回の実験はAnyCPUで行いました。運用環境でもAnyCPUで実行する予定です。
気にする、気にしない問題ですが・・・(笑)
私としてましては、.NET Framwork と .NET Core 3.0 で実行結果が異なるので何故か知りたくなります。
.NET Core 3.0 が IEEE 754-2008に準拠したことで、整数型との変換や浮動小数点数の四則演算に違いが発生したと考えましたが、外池さまや魔界の仮面弁士さまのご回答により、そうではないようだという事がわかります。
では何故なのか思うのは自然な事だと思います。
Math.SinやComplex型についてご指摘をいただきましたが、これらが、.NET Frameworkと.NET Core 3.0で実装に違いがあるから結果が異なるんだとおっしゃっていただければ、なるほどと納得できる状態にあります。
> VB User1さんの今のプログラミング課題として、targetのこの計算結果の差は致命的に困りますか?
まずは、今回の件が.NET Frameworkと.NET Core 3.0の仕様の違いによるものか知りたいということです。
仕様の違いであるならば、その内容により対処を検討する事ができます。対処が難しい場合に初めて「困るか困らないか」という判断が必要となります。
ですので、現時点で「困る・困らない」「気にする・気にしない」を判断する段階ではございません。
今回のサンプルコードの結果は、.NET Frameworkと.NET Core 3.0の仕様の違いによるもので、明確にどのクラス、メソッドかは不明だが、Math.XXXやComplex型あたりが怪しいという理解でよろしいでしょうか?
追記
Forループのkが16のときに
Math.Cos(theta(k))
の値が違うことを確認できました。
.NET Framework
0.68954054473706694
DF-2C-1D-55-B7-10-E6-3F
.NET Core 3.0
0.6895405447370668
DE-2C-1D-55-B7-10-E6-3F
- 編集済み VB User1 2019年10月14日 14:00
-
>Math.XXXやComplex型あたりが怪しいという理解でよろしいでしょうか?
なんとも言えないです。ーーーーーーーーーーーーーーーーーーーーーーーーーーー
今、ざっと提示頂いたソースコードを、数式で書き起こしたら、zP()の実数部であるtargetの数学的な値は「ゼロ」ですね。
target = (1-cos(theta)^2-sin(theta)^2) / hogehoge
これは、数学的にはthetaの値によらず常にゼロです。小生の実行環境で計算してみたら、thetaの値によってマチマチですが(これ、ミソです)、E-17~E-18より小さい値でした。
一方で、zP()の虚数部は・・・、数学的にはsin(theta)の平方根。たぶん。theta = 1/2 *PIのときに数学的に「イチ」のはず。小生の実行環境で、thetaの変化(0~1/2 * PI)に対して、0~1の間でそれっぽい答えが出ています。
では、zPの複素数としての精度はどうなんだ? と言えば、実数部は、虚数部に比べて十分に小さな数字であって、何の問題も無いように見えます。小生の実行環境は、Windows 7の64bit環境で、.Net frameworkの4ナンボか。C#を使ってます。Net Core 3.0で、実数部の値が数十%違う値が出ることがあるかもしれませんが・・・、
そもそも、数学的な答えがゼロであるべきところに、小生の同一の実行環境でさえも、thetaの値によって、E-17~E-18のあたりでいろんな値が出ています。繰り返しになりますが、これがミソです。なので、他の実行環境で同程度に値がバラついたところで、バラつきかたが異なったところで、気にしません。
気にすべきことは、
この類のバラつきがあることを前提に、例えば、数学的にゼロであるべきときに、丸め誤差が累積してE-15より大きな計算結果が出ないように保証するにはどうするか? とか言うような類のことになります。(あくまで「類」のこと。これは、プログラミングの目的によって異なって来ますので、一概に言えません。)
- 編集済み 外池 2019年10月14日 14:18
-
外池さま
ご検証いただきまして誠にありがとうございます。
> そもそも、数学的な答えがゼロであるべきところに、小生の同一の実行環境でさえも、thetaの値によって、E-17~E-18のあたりでいろんな値が出ています。繰り返しになりますが、これがミソです。なので、他の実行環境で同程度に値がバラついたところで、バラつきかたが異なったとして、気にしません。
え!?、実行するたびに値が変わるのですか?
私の環境ではなんどやっても最後のtargetの値は同じです。.NET Frameworkと.NET Core 3.0で違う結果になるだけです。
実行するたびに値が変わる環境があるのであれば、確かに気にするなとおっしゃるのもうなずけます。
-
一度の計算において、ループでkの値を元にtheta変えて、zPの値をいくつか計算してますよね? 同じことをこちらでもやってます。
これらのzPの値、実数部は数学的には常にゼロのはずですが、thetaの値によってマチマチな結果が出ています。お手元でも同様だと思いますが。以下、k=1~31(thetaで1/2*PI近くまで)のzPのダンプです。コンマの前の数字、数学的にはゼロのはずですが、いろんな値が出ています。このことを言っています。
(-1.49348849259878E-17, 0.0122724623795663
小生の環境の、同じデバッグ操作を行うかぎり、たぶん、毎回同じ結果にはなると思いますが。それは、あまりコロコロ変わるものではありません。が・・・、変わらないとも言えない。最適化を変えたり、数学的には同じことでも計算順序変えたりすれば。あるいは、マルチスレッドにしたり、関数にまとめたりすると、いろんなところで丸め誤差の影響が変化しますので。
(-6.17995238316738E-18, 0.0245486221089254
(4.55364912443912E-18, 0.0368321809948456)
(-2.12503625807159E-17, 0.0491268497694673
(1.60461921527855E-17, 0.0614363525815938)
(-1.99493199737333E-17, 0.0737644315224493
(1.21430643318377E-17, 0.0861148511976279)
(1.04083408558608E-17, 0.0984914033571642)
(-1.38777878078145E-17, 0.110897911595913)
(1.21430643318376E-17, 0.123338236136739)
(1.73472347597681E-17, 0.135816278709388)
(2.42861286636753E-17, 0.148335987538347)
(-1.04083408558608E-17, 0.160901362453489)
(-1.38777878078145E-17, 0.173516460137856)
(-2.77555756156289E-17, 0.186185399527584)
(6.93889390390723E-18, 0.198912367379658)
(-1.38777878078145E-17, 0.211701624023983)
(0, 0.224557509317129)
(6.93889390390723E-18, 0.23748444881607)
(-6.93889390390723E-18, 0.250486960191305)
(-1.38777878078145E-17, 0.263569659899918)
(0, 0.276737270140414)
(0, 0.289994626112606)
(1.38777878078145E-17, 0.303346683607342)
(-1.38777878078145E-17, 0.316798526952604)
(-2.77555756156289E-17, 0.330355377344334)
(-2.77555756156289E-17, 0.344022601592426)
(-1.38777878078145E-17, 0.357805721314524)
(0, 0.371710422612743)
(2.77555756156289E-17, 0.385742566271121)
(0, 0.399908198514537)
(0, 0.414213562373095)
(2.77555756156289E-17, 0.428665109699499)
(2.77555756156289E-17, 0.443269513890864)
(2.77555756156289E-17, 0.458033683370672)
(-2.77555756156289E-17, 0.47296477589132)
(2.77555756156289E-17, 0.488070213722863)
(0, 0.503357699799294)
(-2.77555756156289E-17, 0.518835234899976)
(-5.55111512312578E-17, 0.534511135950792)
(2.77555756156289E-17, 0.550394055537264)
(-2.77555756156289E-17, 0.566493002730344)
(0, 0.582817365334976)
(5.55111512312578E-17, 0.599376933681924)
(5.55111512312578E-17, 0.616181926094866)
(-5.55111512312578E-17, 0.633243016177569)
(-5.55111512312578E-17, 0.650571362080153)
(0, 0.668178637919299)
(0, 0.686077067544863)
(0, 0.704279460865044)
(-5.55111512312578E-17, 0.722799252964206)
(-5.55111512312578E-17, 0.741650546272036)
(0, 0.760848156070251)
(0, 0.780407659653944)
(0, 0.80034544949932)
(5.55111512312578E-17, 0.82067879082866)
(-5.55111512312578E-17, 0.841425884007255)
(-5.55111512312578E-17, 0.86260593225674)
(0, 0.88423921522535)
(5.55111512312578E-17, 0.906347169019147)
(5.55111512312578E-17, 0.928952473370367)
(0, 0.952079146700925)
(0, 0.975752649932377) -
> 小生の環境の、同じデバッグ操作を行うかぎり、たぶん、毎回同じ結果にはなると思いますが。それは、あまりコロコロ変わるものではありません。が・・・、変わらないとも言えない。最適化を変えたり、数学的には同じことでも計算順序変えたりすれば。あるいは、マルチスレッドにしたり、関数にまとめたりすると、いろんなところで丸め誤差の影響が変化しますので。
安心しました。同じ条件で実行する限りは値は変わらないということですね。
私としましては、これまでの経過から.NET Frameworkと.NET Core 3.0に関して以下の結論に至りました。
・Math.XXX()は実装に違いがあり、異なった結果となる可能性がある
・Math.Cos()での違いを確認
・Complex型は未確認だが、怪しい
・整数型と浮動小数点数型の相互変換による違いは見受けられなかった
・浮動小数点数型の四則演算も違いは見つからなかった
・Math.XXX()は実行環境によって違う値を返す可能性がある。ただし、今回の実験では同一環境によるもの
という結論に至りました。
みなさま、長い時間お付き合いいただき、誠にありがとうございました。
回答としてマークをどこに付けるのが最適でしょうか?悩んでおります。
-
>> 小生の環境の、同じデバッグ操作を行うかぎり、たぶん、毎回同じ結果にはなると思いますが。それは、
>>あまりコロコロ変わるものではありません。が・・・、変わらないとも言えない。最適化を変えたり、数学的
>>には同じことでも計算順序変えたりすれば。あるいは、マルチスレッドにしたり、関数にまとめたりすると、
>>いろんなところで丸め誤差の影響が変化しますので。
>安心しました。同じ条件で実行する限りは値は変わらないということですね。いいえ、小生は、そうは言い切れないと申し上げてます。そう言い切る根拠を持ち合わせていません。
ネイティブ機械語の命令(メモリへの退避も含む)の固定した順序で、同一モデルのCPUで、計算を繰り返せば、おそらく同じ結果が得られるとは思います。が、小生の手元の環境でそこまで確認できていないのです。(スレッドやプロセスの切り替えに際して、CPUのレジスタの内容をメモリに格納する時、全てのbitが退避できるのか丸め誤差が入るのか、小生はここがよくわかっていません。一方、小生は、デバッグ用コンパイル・実行やリリース用コンパイル・実行のように、最適化の有無によってこれが変わることは知っています。)当初のVB User1さんの問題提起に対して、小生が申し上げたいことは、IEEE754に従う云々の記述が仕様にあったとしても、実装・実行環境・実行時CPU稼働状態の違いによらず計算結果がbit単位で一致する、というものではない、ということだけです。
- 編集済み 外池 2019年10月15日 0:50
-
.NET Framework
1.6653345369377351E-16
01-00-00-00-00-00-A8-3C
.NET Core 3.0
2.775557561562892E-16
01-00-00-00-00-00-B4-3C
当方の .NET Core 3.0 環境(Win10 1903 および Win10 1803 の 2 箇所) でテストしたところ、下記の結果となりました。
target.ToString("R") = "1.665334536937735E-16" BitConverter.DoubleToInt64Bits(target) = &H3CA8000000000001
Prefer32Bit / x64 / AnyCPU / x86 のいずれでも変化しませんでしたし、コンパイルオプションで最適化の有効/無効を切り替えた場合も同じ結果でした。
追記:
.NET Framework 4.8 with System.Runtime.Numerics.4.3.0
でも試しましたが、やはり H3CA8000000000001 です。どちらのパターンでも、&H3CB4000000000001 という値にはなりませんでした。
※ .NET Core 3.0 の Preview 版でどうなるかは未確認。
- 編集済み 魔界の仮面弁士MVP 2019年10月15日 2:20 .NET Framwork の検証結果を追記
- 回答としてマーク VB User1 2019年10月15日 11:49
- 回答としてマークされていない VB User1 2019年10月15日 11:49
-
01-00-00-00-00-00-A8-3C
01-00-00-00-00-00-B4-3C
可能であれば、この違いの理由を知りたいです。3 対 5 の割合差になりますね。
数値として比較すると、ちょうど「5.0÷3.0」相当。
Dim a = BitConverter.Int64BitsToDouble(&H3CA8000000000001) Dim b = BitConverter.Int64BitsToDouble(&H3CB4000000000001) Dim c As Double = b / a '1.6666666666666667
target = CDbl(2.0 * (zP(16).Real))
zP(16) からだと &H3CA8000000000001 になりますが、
zP(18) からだと &H3CB4000000000001 になります。比較点を間違えている可能性は無いでしょうか?
-
外池さま
魔界の仮面弁士さま
ご検証・コメント、誠にありがとうございます。
下記の点につきまして考えを改める事といたします。
・Math.XXX()は実装に違いがあり、異なった結果となる可能性がある
4台のPC(すべて異なるCPU)の環境で実験を行ったところ、
・.NET Frameworkの値は4台とも同じ
・.NET Core 3.0の値は3台が.NET Frameworkと一致し、一台のみ異なる値を示した
という結果となりました。
よって.NET Frameworkと.NET Core 3.0の違いというよりは、皆様がおっしゃるようにMath.Sin、Cos、Tanなどの環境依存のメソッドを使用したことによる違いの可能性が高いという認識に改めました。
根気よくお付き合いいただいた皆様、誠にありがとうございました。三角関数につきましては自作することを検討しております。
-
>三角関数につきましては自作することを検討しております。
計算科学の学習としては大変良いことだと思いますが、実用としては典型的な「車輪の再発明」になっていると思います。
整数演算は、環境依存性を排除してユニバーサルに結果が一致するプログラムを書くことができますし、そのようなプログラムが実用されています。典型が通信系のプログラム。暗号・復号処理など。そうでなければ通信が成り立ちません。
浮動小数点数演算にも、もちろん仕様で定められた精度というものがあります。実用においては(世に広く提供されているライブラリでは)、工夫して多い目の桁数で計算が行われます。この多い目の桁数のところでは不一致があり得るから、あとは自分で管理してね(丸めてね)という「わりきり」です。自作される場合ても、この轍を踏むことになろうかと思います。
- 編集済み 外池 2019年10月15日 6:33