teratail header banner
teratail header banner
質問するログイン新規登録
C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

意見交換

クローズ

18回答

3045閲覧

C# TimeSpanのTotalMinutesなどで丸目誤差は発生しないものと考えて良いか

naitou

総合スコア141

C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

0グッド

0クリップ

投稿2023/06/12 02:37

編集2023/06/12 02:38

0

0

TimeSpanのTotalMinutesなどはdobule型であることから、
それらの値を整数型にキャストする際は丸目誤差も考慮すべきでしょうか。
例えば2の時に1.9999999999999999999....となるようなケースです。

.NET Framework 4.8のソースを見ると下記のようになっており、
_ticksに対して、必ず切りあがる様なMinutesPerTick をかけることにより、
丸目誤差が発生しないように出来てるっぽいです。
すごく危うく感じるんですが、丸目誤差は発生しないものと考えて良いか、
みなさんのご意見をお聞かせ下さい。

これまで念の為に+0.1などしていました。。。

https://referencesource.microsoft.com/#mscorlib/system/timespan.cs
一部抜粋

C#

1internal long _ticks; 2 3public const long TicksPerMinute = TicksPerSecond * 60; // 600,000,000 4 5private const double MinutesPerTick = 1.0 / TicksPerMinute; // 1.6666666666667e-9 6 7public double TotalMinutes { 8 get { return (double)_ticks * MinutesPerTick; } 9} 10

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

回答18

#1

退会済みユーザー

退会済みユーザー

総合スコア0

投稿2023/06/12 03:28

編集2023/06/12 08:12

意見交換と言うより質問じゃないのでしょうか?

Microsoft のドキュメントによると、"TimeSpan オブジェクトの値は、表されている時間間隔と等しいタイマ刻みの数です。タイマ刻みは 100 ナノ秒に相当します" ということですが、そのあたりは認識されているでしょうか?

そこ(100 ナノ秒の精度)は認識されているとして、TimeSpan オブジェクトの値は正確に 0.1 分なのだが、それを double で表すとマルメの誤差が出るということを問題にしているのですか? それの何が危ういのか分かりません。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

#2

Zuishin

総合スコア28675

投稿2023/06/12 03:53

編集2023/06/12 04:19

丸め誤差とは何を指しているんでしょうか?
TotalMinutes は、600000000 ticks に対して 1.0 を返し、599999999 に対して 0.999999998333333 を返します。
0.999999998333333 は丸められているので、当然誤差はあります。

つまり、ticks で表される時間を分に直したものが TotalMinutes で、それは整数ではなく double の範囲で丸めた実数です。

なお、600000000 ticks(1 分) で小数部が消えて整数部だけになる以上、それを整数倍したものは整数になります。
ただし、桁が増えすぎて小数部が生まれた場合はその限りではありません。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

#3

naitou

総合スコア141

投稿2023/06/12 05:25

#1
TotalMinutesはdobule型で返されるため、単純に丸め誤差を考慮する必要があるのでは思い、Microsoftドキュメントにおける1ティックが 100 ナノ秒に相当するという記述がそれを担保する意味になるのか理解できませんでしたが、言われてみるとそう思えてきました。

#2
600000000ticks のTotalMinutesが 0.99999999999999999
となってしまうような丸目誤差です。

もし1ticks のTotalMinutesが 0.0000000016666666666666667ではなく、
0.0000000016666666666666666だと切り下がって、丸め誤差が出るのではと思いました。
どうして誤差のない0.0000000016666666666666667となるかは理解はできていません。
そのため危ういといった感想が出た次第です。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

#4

退会済みユーザー

退会済みユーザー

総合スコア0

投稿2023/06/12 05:34

#3

何も分かってないように思えますけど。「危うい」って言ってる言葉の意味自体、分かって言ってるようには思えませんよ。0.1 を double で表す場合必ずマルメの誤差が出るというのと、TimeSpan とは何のつながりもないです。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

#5

退会済みユーザー

退会済みユーザー

総合スコア0

投稿2023/06/12 05:41

#3

思い出しました。あなたはこのスレッド https://teratail.com/questions/v1izscllp4q8wb を立てた人ですよね。考え方が私の考えの範囲をはるかに超越していて、私が理解できないような気がします。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

#6

naitou

総合スコア141

投稿2023/06/12 09:35

#4
「0.1 を double で表す場合必ずマルメの誤差が出るというのと、TimeSpan とは何のつながりもないの」はわかっています。

1.0をNで割った後にNを掛けた後に元の1.0に戻る数字と戻らない数字がありますよね。
下記のソースコードを実行して出力されるような数字です。

「危うい」と表現したのは、1ticks のTotalMinutes(MinutesPerTick)は1.0を元に戻る数字(600000000)で割っているから良いものの、もし戻らない数字だったら丸め誤差の問題が起こるんじゃないか思いました。

実際は戻る数字で割っているし問題ないだろと言われると「その通りですね」ということで、TimeSpanのTotal~系メンバで誤差は出ないということで納得しました。

C#

1for( int i = 1 ; i < 200 ;i++){ 2 int a = (int)(1.0 / i * i); 3 if( a != 1){ 4 Console.WriteLine(i); 5 } 6}

実行結果
49
98
103
107
161
187
196
197

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

#7

退会済みユーザー

退会済みユーザー

総合スコア0

投稿2023/06/13 01:10

#5 の回答が「攻撃的な表現などを含む不快な回答」だそうですので一言。

前のスレッド https://teratail.com/questions/v1izscllp4q8wb の質問者の考えが私が考えられる範囲をはるかに超越しているというのはその通り。ちなみに前のスレッドの話は、WinForms アプリで複数の MessageBox を同時に開くということ。意図を聞いても答えないので理解できない。

このスレッドも同様で質問者が何を考えているのか自分は理解できない気がすると言っているのですよ。どこが攻撃的?

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

#8

退会済みユーザー

退会済みユーザー

総合スコア0

投稿2023/06/13 02:18

編集2023/06/13 02:19

#6

まず、「丸め誤差」というのは「小数点以下の数を 2 進数で正確に表現できないことにより発生する誤差」と言うのが私が理解です。

「浮動小数点 丸め誤差」をキーワードにググってみてください、そういう理解は私だけではないようです。

上の回答に書いた、

TimeSpan オブジェクトの値は正確に 0.1 分なのだが、それを double で表すとマルメの誤差が出る

というのはそのことを言ってます。詳しくは以下の記事を見てください。

小数(浮動小数点数型)の計算が思った結果にならない理由と解決法
https://dobon.net/vb/dotnet/beginner/floatingpointerror.html

"例えば十進数の「0.1」を2進数に変換すると「0.0001100110011…」となり、「0011」の部分が永遠に循環します。よって「0.1」をSingleやDouble型に格納するには、適当な桁で丸める必要があります"

一方、質問者さんのいう「丸の誤差」の意味が分かりません。

最初の質問に書いてあった、

TimeSpanのTotalMinutesなどはdobule型であることから、それらの値を整数型にキャストする際は丸目誤差も考慮すべきでしょうか。例えば2の時に1.9999999999999999999....となるようなケースです。

と言うところが疑問の始まりだと思っていますが、double を int にキャストすれば当然小数点以下は切り捨てられます。そういうのが質問者さんの言う「丸め誤差」ですかね? 違うかな?

また、正確に 2 分、すなわち TimeSpan.Ticks が 1200000000 なら TotalMinutes は 1.9999999999999999999.... にはなりません。

イメージ説明

それから、TotalMinutes の計算ですが、割り算ではなくて掛け算にしているのは、想像ですが、掛け算の方が割り算より演算速度が速いケースがあるからだろうと思います。

というわけで、何も分かってないように思えます。「危うい」って言ってる言葉の意味自体、分かって言ってるようには思えません。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

#9

naitou

総合スコア141

投稿2023/06/13 03:58

#7
MessageBox を同時に開くのは既にあるシステムの仕様であり、どうしてそんな仕様になっているか問われても説明は控えさせて頂きました。

#8
>double を int にキャストすれば当然小数点以下は切り捨てられます
この時に意図しない整数値を得てしまうことを「丸め誤差」と表現していましたが、この部分の認識に齟齬があるようですね。正しい名称をご教授頂ければと思います。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

#10

退会済みユーザー

退会済みユーザー

総合スコア0

投稿2023/06/13 04:21

編集2023/06/13 04:22

#9

MessageBox を同時に開くのは既にあるシステムの仕様であり

それは「仕様」が間違ってます。そもそもMessageBox を複数開くことはあり得ないのだから。「仕様」を直すのが筋です。

この時に意図しない整数値を得てしまうことを「丸め誤差」と表現していましたが、この部分の認識に齟齬があるようですね。正しい名称をご教授頂ければと思います。

正しいかどうかは置いといて、誤解なく理解される言い方は「double を int にキャストすることによる小数点以下の切り捨て」だと思います。

「意図しない」とか言ってますが、多少 C# の文法を知っていれば意図しないなんてことはあり得ません。

いろいろ言われてあれこれ取り繕うのは止めましょう。指摘されたら素直に認めてあなたの考えを修正してください。でないと話をしても「意見交換」にならず時間の無駄になるばかりです。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

#11

naitou

総合スコア141

投稿2023/06/13 04:27

#10

「意図しない」とか言ってますが、多少 C# の文法を知っていれば意図しないなんてことはあり得ません。
確かにその通りのなのですが、ミスしやすいなどのニュアンスでそう表現していました。

名称に関しては承知しました。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

#12

退会済みユーザー

退会済みユーザー

総合スコア0

投稿2023/06/13 10:27

編集2023/06/13 10:29

丸め誤差を考慮すべきかは作りたいアプリやシステムに依存し、
必要な有効桁数で四捨五入するなどすれば良いだけではないか?

例えば12:00:00.000を基準として
①12:02:00.000
②12:01:59.999
③12:01:59.000
があったとする
人間の感覚からすれば、分で表現するならどれも2分で困らないだろう
適当なところで四捨五入して2分となるようにすれば解決できる
これは分という荒い単位で表現する時にどこまで正確性が必要かという仕様の問題
システム上の丸め誤差も考慮すべきだが、人にどう見せたいか、そのために何をすべきかでしかない

厳密に識別したいのであれば、そもそもTotalMinutesを使うべきではなく
long表現であるTicksを使い自分が求める結果になるよう整数のみで計算すれば良いだけのこと
XY問題ではないが結局何を求めているのかを整理した方がよい

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

#13

退会済みユーザー

退会済みユーザー

総合スコア0

投稿2023/06/16 00:05

#12

丸め誤差を考慮すべきかは作りたいアプリやシステムに依存し、必要な有効桁数で四捨五入するなどすれば良いだけではないか?

質問者さんとしてはそういう話ではなくて、

質問者さんの言う「丸目誤差」=「double を int にキャストすることによる小数点以下の切り捨て」ということだそうなので、表題の、

TimeSpanのTotalMinutesなどで丸目誤差は発生しないものと考えて良いか

を書き直すと、

TimeSpanのTotalMinutesなどの double を int にキャストすることによる小数点以下の切り捨ては発生しないものと考えて良いか

・・・ということになるはずです。

質問者さんの考えの範囲内での「意見交換」の話として#12 の答えは違うかもしれません。

いかがですか? > 質問者さん

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

#14

naitou

総合スコア141

投稿2023/06/16 09:10

#13
度々回答頂き恐れ入ります。
書き直して頂いた質問の文面がより適切でした。

#12
直接的に求めている回答ではありませんが、参考にさせて頂きます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

#15

ikedas

総合スコア4441

投稿2023/06/20 07:11

編集2023/06/21 05:10

まず、1.6666666666667e-9 という表示は計算の際の丸め (英語のround offの訳語です。「丸目」は差し金です) による誤差の結果ではありません。一般に、小数部を持つ2進数を有限桁の10進数に正確に変換することはできませんしようとすると不必要に長くなってしまいがちです。どこかで表示を打ち切らなければならないため、打ち切った桁の表示が不正確になっているにすぎません。実際、ためしに同じ数を小数点以下1718桁まで表示してみると、やっぱり...6667になります。通常はdoubleの精度は53ビットしかありませんから (後述)、10進での小数点以下1718桁目なんかにはなんの意味もないのです。とにかくこれは計算による丸め誤差ではありません。

では丸め誤差がどこで発生するかというと、1.0 / TicksPerMinute という計算を2進数で行った結果に誤差が発生します。

1.0 / 600 000 000 を2進数で計算すると、正確には

1.C A2 13 D8 40 BA F7 D4 ... [hex] × 2^{-30}

と、無限小数になります (2進数の小数をそのまま書くとあまりにも見づらいので、16進数で書いています。[hex] は前の数が16進数であることを示します)。

しかし、実際にCのプログラム (末尾に掲載します) で計算してみると、

1.C A2 13 D8 40 BA F8 [hex] × 2^{-30}

などとなります。double型の数値の仮数部の精度は (一般的に使われるIEEE 754形式の場合) 52ビット (整数部の1を除く) しかないため、それを超える桁 (ビット) は何らかの方法で丸めるしかありません。ここで丸め誤差が発生しています。

しかし、_ticks * MinutesPerTick の計算でも丸めは発生します。ですからそのときに丸めが相殺されて正確な結果が出るのなら、計算途中での丸め誤差は問題にならないわけですよね。さて、そううまくいくもんだろうか…というのがご質問の意図なのかと思います。

そこでC#での浮動小数点演算でどのような丸め規則を採用しているのか、ちょっと調べたんですけどわかりませんでした。しかたがないのでC言語で標準化されている4種の丸めモードでどうなるか確認してみました。

Denominator dec: 600000000 hex: 41 C1 E1 A3 00 00 00 00 <+1.1 E1 A3 00 00 00 00 [hex] x 2^{29}> Default round mode: FE_TONEAREST FE_DOWNWARD: ---- Reciprocal dec: 1.6666666666666664e-09 hex: 3E 1C A2 13 D8 40 BA F7 <+1.C A2 13 D8 40 BA F7 [hex] x 2^{-30}> R x D dec: 0.99999999999999988 hex: 3F EF FF FF FF FF FF FF <+1.F FF FF FF FF FF FF [hex] x 2^{-1}> FE_TONEAREST: ---- Reciprocal dec: 1.6666666666666667e-09 hex: 3E 1C A2 13 D8 40 BA F8 <+1.C A2 13 D8 40 BA F8 [hex] x 2^{-30}> R x D dec: 1 hex: 3F F0 00 00 00 00 00 00 <+1.0 00 00 00 00 00 00 [hex] x 2^{0}> FE_TOWARDZERO: ---- Reciprocal dec: 1.6666666666666664e-09 hex: 3E 1C A2 13 D8 40 BA F7 <+1.C A2 13 D8 40 BA F7 [hex] x 2^{-30}> R x D dec: 0.99999999999999988 hex: 3F EF FF FF FF FF FF FF <+1.F FF FF FF FF FF FF [hex] x 2^{-1}> FE_UPWARD: ---- Reciprocal dec: 1.6666666666666668e-09 hex: 3E 1C A2 13 D8 40 BA F8 <+1.C A2 13 D8 40 BA F8 [hex] x 2^{-30}> R x D dec: 1.0000000000000003 hex: 3F F0 00 00 00 00 00 01 <+1.0 00 00 00 00 00 01 [hex] x 2^{0}>

Reciprocal (R) は 1.0Denominator (D) で割った結果です。それに再びDを掛けて、1.0 になればよいわけです。

見ての通り、うまくいくのは丸めモードがFE_TONEAREST (最近接偶数丸め) の場合だけです。FE_UPWARD (正の無限大への丸め)、FE_DOWNWARD (負の無限大への丸め)、FE_TOWARDZERO (0への丸め) では丸め誤差は相殺されません。また、C#ではdoubleから整数への変換の際には0方向へ丸める (参照) ので、モードが FE_DOWNWARDFE_TOWARDZERO だと、結果を整数として使いたいときに意図しない結果になる可能性があります。

FE_TONEAREST ではうまくいく理由ですが、このモードでは仮数部の丸め誤差の絶対値が常に2^{-53}以下である (ほかのモードでは2^{-52}未満と、より大きくなり得る) こと、また丸め方向がひとつの方向に固定されないため計算を繰り返す中で誤差が均されやすいこと、があると考えられます。

ひとまずこんなところで。これは意見交換ではないなー。

C

1#include <stdio.h> 2#include <fenv.h> 3 4void decode_double(char * label, double x) { 5 union { 6 double d; 7 unsigned char c[8]; 8 } res; 9 int i; 10 11 printf("%s\n", label); 12 13 printf("dec: %.17g\n", x); 14 15 res.d = x; 16 printf("hex: "); 17 for (i = 7; i >= 0; i--) 18 printf("%02X ", res.c[i]); 19 20 printf(" <"); 21 printf(res.c[7] & 0x80 ? "-" : "+"); 22 printf("1.%1X" , res.c[6] & 0x0F); 23 for (i = 5; i >= 0; i--) 24 printf(" %02X", res.c[i]); 25 printf(" [hex]"); 26 printf(" x 2^{%d}", ((res.c[7] & 0x7F) << 4) + (res.c[6] >> 4) - 1023); 27 printf(">\n\n"); 28} 29 30int main() 31{ 32 long denom; 33 double recip; 34 35 scanf("%ld", &denom); 36 decode_double("Denominator", denom); 37 38 printf("Default round mode: "); 39 switch (fegetround()) { 40 case FE_DOWNWARD: 41 printf("FE_DOWNWARD"); 42 break; 43 case FE_TONEAREST: 44 printf("FE_TONEAREST"); 45 break; 46 case FE_TOWARDZERO: 47 printf("FE_TOWARDZERO"); 48 break; 49 case FE_UPWARD: 50 printf("FE_UPWARD"); 51 break; 52 default: 53 printf("Unknown"); 54 } 55 printf("\n\n"); 56 57 fesetround(FE_DOWNWARD); 58 printf("FE_DOWNWARD:\n----\n"); 59 recip = 1.0 / denom; 60 decode_double("Reciprocal", recip); 61 decode_double("R x D", recip * denom); 62 63 fesetround(FE_TONEAREST); 64 printf("FE_TONEAREST:\n----\n"); 65 recip = 1.0 / denom; 66 decode_double("Reciprocal", recip); 67 decode_double("R x D", recip * denom); 68 69 fesetround(FE_TOWARDZERO); 70 printf("FE_TOWARDZERO:\n----\n"); 71 recip = 1.0 / denom; 72 decode_double("Reciprocal", recip); 73 decode_double("R x D", recip * denom); 74 75 fesetround(FE_UPWARD); 76 printf("FE_UPWARD:\n----\n"); 77 recip = 1.0 / denom; 78 decode_double("Reciprocal", recip); 79 decode_double("R x D", recip * denom); 80 81 return 0; 82}

実行すると、標準入力からTicksPerMinuteの値を読み込みます。コンパイラによる最適化を避けるためにこうしています。


2023-06-20 1箇所訂正、1箇所微修正。

2023-06-21 2箇所訂正、1箇所微修正。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

#16

naitou

総合スコア141

投稿2023/06/20 11:46

#15
実行結果より2回の丸めで最近接偶数丸めは誤差がもっとも少なく、上手く整数に戻っていることがよくわかりました。
TimeSpanのTotalMinutesなどの double で int にキャストすることによる小数点以下の切り捨てが発生しないのは、600000000という数字が上手く戻れる数値であることに加えて、端数処理が最近接偶数丸めであることによって成立していると理解しました。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

#17

ikedas

総合スコア4441

投稿2023/06/21 00:36

編集2023/06/21 03:27

#15

計算途中で丸めが入る浮動小数点演算であるのに、結果がこれだけきれいに整数になるのは意外ですね。

丸めモードの効果もありますが、ほかの理由としてはMinutesPerTick の値も比較的「良い性質」を持っているように思います。たとえば、_ticks の値がTicksPerMinuteのちょうど整数倍になることは現実にはほぼないわけですが、ちょうどでなくても整数倍にごく近ければ、TotalMinutes() の値が整数になりやすくなっているようです (きちんと理論的に分析したわけではないので感覚的な言いかたになりますが)。

とはいえ、この結果も最近接偶数丸めモードを用いていることが前提です。もしもC#の言語仕様でどの丸めモードを用いるかが規定されていないのならば、結果は使っているC#処理系の実装に依存するということになってしまいます。その場合、ご質問でおっしゃっているような対策も考える必要がありそうですね。 規定されていました。

The rounding mode defined in IEC 60559:1989[*1] shall be set by the CLI to "round to the nearest number"[*2], and neither the CIL nor the class library provide a mechanism for modifying this setting. (...)

[*1] IEEE 754と同一内容の国際規格。
[*2] IEEE 754での属性名はroundTiesToEvenで、最近接偶数丸めを意味する。

ということで、.NET Framework共通言語仕様では浮動小数点演算の丸めモードとして最近接偶数丸めのみを提供しています。ほかの丸めモードによって期待しない結果になることはないと言えます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

#18

naitou

総合スコア141

投稿2023/06/21 12:18

#17
C#の浮動小数点演算の丸めモードとして最近接偶数丸めが使われている件、とても参考になりました。ありがとうございます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

最新の回答から1ヶ月経過したため この意見交換はクローズされました

意見をやりとりしたい話題がある場合は質問してみましょう!

質問する

関連した質問