web-dev-qa-db-ja.com

通貨にBigDecimalを使用することがdoubleを使用するよりも厳密に優れている現実的な例

know 通貨にdoubleを使用するとエラーが発生しやすく、推奨されません。ただし、 realistic の例はまだ見ていません。BigDecimalは機能しますが、doubleは失敗し、単純に丸めることで修正できません。


些細な問題に注意してください

double total = 0.0;
for (int i = 0; i < 10; i++) total += 0.1;
for (int i = 0; i < 10; i++) total -= 0.1;
assertTrue(total == 0.0);

丸めによって簡単に解決されるため、カウントしないでください(この例では、小数点以下0桁から16桁までは何でもかまいません)。


大きな値の合計を伴う計算には、いくつかの中間ルーティングが必要になる場合がありますが、 流通通貨合計USD 1e12、Java double(つまり、標準の IEEE倍精度 )が15桁の10進数である場合でも、セントにとって十分なイベントです。


除算を含む計算は、BigDecimalでも一般的に不正確です。 doublesでは実行できない計算を構築できますが、BigDecimalでは100のスケールを使用して実行できますが、実際には遭遇することはありません。


そのような現実的な例が存在しないと主張するのではなく、まだ見たことがないだけです

また、doubleを使用するとエラーが発生しやすくなることに同意します。

私が探しているのは、次のような方法です(Roland Illigの答えに基づいて)

/** 
  * Given an input which has three decimal places,
  * round it to two decimal places using HALF_EVEN.
*/
BigDecimal roundToTwoPlaces(BigDecimal n) {
    // To make sure, that the input has three decimal places.
    checkArgument(n.scale() == 3);
    return n.round(new MathContext(2, RoundingMode.HALF_EVEN));
}

のようなテストと一緒に

public void testRoundToTwoPlaces() {
    final BigDecimal n = new BigDecimal("0.615");
    final BigDecimal expected = new BigDecimal("0.62");
    final BigDecimal actual = roundToTwoPlaces(n);
    Assert.assertEquals(expected, actual);
}

これがdoubleを使用して単純に書き換えられると、テストは失敗する可能性があります(指定された入力に対しては行われませんが、他の入力に対しては行われます)。ただし、正しく実行できます。

static double roundToTwoPlaces(double n) {
    final long m = Math.round(1000.0 * n);
    final double x = 0.1 * m;
    final long r = (long) Math.rint(x);
    return r / 100.0;
}

くてエラーが発生しやすい(そしておそらくは単純化できます)が、どこかに簡単にカプセル化できます。それが私がもっと答えを探している理由です。

41
maaartinus

通貨計算を扱うときにdoubleがあなたを台無しにすることができる4つの基本的な方法を見ることができます。

仮数が小さすぎる

仮数部に15桁以下の精度があると、それ以上の量を扱うときはいつでも間違った結果を得ることになります。セントを追跡している場合、問題は10より前に発生し始めます。13 (10兆)ドル。

これは大きな数字ですが、それほど大きくはありません。米国のGDPは18兆円を超えているため、国や企業規模の金額を扱うものはすべて間違った答えを容易に得る可能性があります。

さらに、計算中にはるかに少ない量がこのしきい値を超える可能性のある方法がたくさんあります。成長予測を行っているか、数年にわたって、最終的な値が大きくなる可能性があります。 「what if」シナリオ分析を実行して、さまざまな可能なパラメーターを調べ、パラメーターの組み合わせによって非常に大きな値になる場合があります。 セントの端数 を許可する金融ルールの下で作業している可能性があります。これにより、範囲からさらに2桁以上切り取られ、 単なる個人の富とほぼ一致します USDで。

最後に、米国を中心とした物事の見方を避けましょう。他の通貨はどうですか?インドネシアルピアは おおよそ 13,000 USDであるため、その通貨で通貨額を追跡するのに必要なもう1桁の大きさです(「セント」がないと仮定!)。あなたは、ただの人間にとって関心のある量にほぼ達しつつあります。

5eで1e9から始まる成長予測の計算が間違っています:

_method   year                         amount           delta
double      0             $ 1,000,000,000.00
Decimal     0             $ 1,000,000,000.00  (0.0000000000)
double     10             $ 1,628,894,626.78
Decimal    10             $ 1,628,894,626.78  (0.0000004768)
double     20             $ 2,653,297,705.14
Decimal    20             $ 2,653,297,705.14  (0.0000023842)
double     30             $ 4,321,942,375.15
Decimal    30             $ 4,321,942,375.15  (0.0000057220)
double     40             $ 7,039,988,712.12
Decimal    40             $ 7,039,988,712.12  (0.0000123978)
double     50            $ 11,467,399,785.75
Decimal    50            $ 11,467,399,785.75  (0.0000247955)
double     60            $ 18,679,185,894.12
Decimal    60            $ 18,679,185,894.12  (0.0000534058)
double     70            $ 30,426,425,535.51
Decimal    70            $ 30,426,425,535.51  (0.0000915527)
double     80            $ 49,561,441,066.84
Decimal    80            $ 49,561,441,066.84  (0.0001678467)
double     90            $ 80,730,365,049.13
Decimal    90            $ 80,730,365,049.13  (0.0003051758)
double    100           $ 131,501,257,846.30
Decimal   100           $ 131,501,257,846.30  (0.0005645752)
double    110           $ 214,201,692,320.32
Decimal   110           $ 214,201,692,320.32  (0.0010375977)
double    120           $ 348,911,985,667.20
Decimal   120           $ 348,911,985,667.20  (0.0017700195)
double    130           $ 568,340,858,671.56
Decimal   130           $ 568,340,858,671.55  (0.0030517578)
double    140           $ 925,767,370,868.17
Decimal   140           $ 925,767,370,868.17  (0.0053710938)
double    150         $ 1,507,977,496,053.05
Decimal   150         $ 1,507,977,496,053.04  (0.0097656250)
double    160         $ 2,456,336,440,622.11
Decimal   160         $ 2,456,336,440,622.10  (0.0166015625)
double    170         $ 4,001,113,229,686.99
Decimal   170         $ 4,001,113,229,686.96  (0.0288085938)
double    180         $ 6,517,391,840,965.27
Decimal   180         $ 6,517,391,840,965.22  (0.0498046875)
double    190        $ 10,616,144,550,351.47
Decimal   190        $ 10,616,144,550,351.38  (0.0859375000)
_

デルタ(doubleBigDecimalの差は、160年で最初に1セントを超え、約2兆(これから160年も経っていない可能性があります)になります。悪い。

もちろん、53ビットの仮数は、この種の計算のrelativeエラーが非常に小さい可能性が高いことを意味します(うまくいけば、1パーセント以上仕事を失うことはありません) 2兆)。実際、相対誤差は基本的にほとんどの例でかなり安定しています。ただし、仮数部の精度を落として(たとえば)さまざまな2つを減算して、任意の大きなエラー(読者までの運動)が発生するように、確かに整理できます。

セマンティクスの変更

したがって、あなたはかなり賢いと思い、doubleを使用してローカルJVMでメソッドを徹底的にテストできる丸めスキームを考え出すことができました。先に進んで展開してください。明日、来週、またはあなたにとって最悪のときはいつでも、結果が変わり、トリックが壊れます。

他のほとんどすべての基本言語式とは異なり、確かに整数やBigDecimal算術とは異なり、多くの浮動小数点式の結果には、デフォルトで strictfp 機能のために単一の標準定義値がありません。プラットフォームは、自由裁量で、より高精度の中間体を自由に使用できます。これにより、異なるハードウェア、JVMバージョンなどで異なる結果が得られる場合があります。同じ入力の場合、結果は 実行時に異なる whenメソッドは、解釈済みからJITコンパイル済みに切り替わります!

Java 1.2以前のバージョンでコードを書いていた場合、Java 1.2がデフォルトの変数FPの動作を突然導入すると、かなりうんざりするでしょう。あらゆる場所でstrictfpを使用するだけの誘惑に駆られ、 関連するバグの多重度 のいずれにも遭遇しないことを望みますが、一部のプラットフォームでは、そもそもあなたを二重に買ったパフォーマンス。

FPハードウェアのさらなる変更に対応するためにJVM仕様が今後再び変更されないこと、またはJVM実装者がデフォルトのnon-strictfpのロープを使用しないことを言うことはありません。振る舞いは彼らに何か難しいことをさせる。

不正確な表現

Rolandが彼の答えで指摘したように、doubleの重要な問題は、いくつかの最も非整数値の正確な表現がないことです。 _0.1_のような単一の非正確な値は、一部のシナリオ(たとえば、Double.toString(0.1).equals("0.1"))で「往復」することがよくありますが、これらの不正確な値を計算するとすぐにエラーが悪化します。これは回復不能です。

特に、丸めポイントに「近い」場合(例:〜1.005)、真の値が1.0050000001 ...、、またはその逆の場合、値1.00499999 ...を取得する可能性があります-versa。エラーは双方向に発生するため、これを修正できる丸めマジックはありません。 1.004999999 ...の値を増やす必要があるかどうかを判断する方法はありません。 roundToTwoPlaces()メソッド(二重丸めの一種)は、1.0049999を上げる必要がある場合を処理したためにのみ機能しますが、累積エラーが原因で1.0050000000001が発生する場合など、境界を越えることはできません。 1.00499999999999になりましたが、修正できません。

これをヒットするために大きな数字や小さな数字は必要ありません。計算が必要なだけで、結果が境界に近づくために必要です。数学が多ければ多いほど、真の結果からの逸脱が大きくなり、境界をまたぐ可能性が高くなります。

ここで要求されているように 検索テスト これは単純な計算を行います:_amount * tax_そして小数点以下2桁(つまり、ドルとセント)に丸めます。そこにはいくつかの丸め方法があります。現在使用されているものは、roundToTwoPlacesBがあなたのものの改良版です1 (最初の丸めでnの乗数を増やすことで、より敏感になります-元のバージョンは些細な入力ですぐに失敗します)。

このテストでは、見つかった失敗が吐き出され、それらがまとめられます。たとえば、最初のいくつかの失敗:

_Failed for 1234.57 * 0.5000 = 617.28 vs 617.29
Raw result : 617.2850000000000000000000, Double.toString(): 617.29
Failed for 1234.61 * 0.5000 = 617.30 vs 617.31
Raw result : 617.3050000000000000000000, Double.toString(): 617.31
Failed for 1234.65 * 0.5000 = 617.32 vs 617.33
Raw result : 617.3250000000000000000000, Double.toString(): 617.33
Failed for 1234.69 * 0.5000 = 617.34 vs 617.35
Raw result : 617.3450000000000000000000, Double.toString(): 617.35
_

「生の結果」(つまり、正確な丸められていない結果)は常に_x.xx5000_境界に近いことに注意してください。丸め方法は、高い側と低い側の両方でエラーが発生します。一般的に修正することはできません。

不正確な計算

_Java.lang.Math_メソッドのいくつかは、正しく丸められた結果を必要としませんが、最大2.5 ulpのエラーを許可します。確かに、おそらく通貨では双曲線関数をあまり使用することはないでしょうが、exp()pow()などの関数はしばしば通貨計算に使われ、これらは正確さしかありません1 ulpの。そのため、返されるとき、番号はすでに「間違っています」。

これは、「不正確な表現」問題と相互作用します。このタイプのエラーは、少なくともdoubleの表現可能なドメインから可能な限り最良の値を選択する通常の数学演算のエラーよりもはるかに深刻だからです。つまり、これらのメソッドを使用すると、さらに多くの境界を越えるイベントを作成できます。

24
BeeOnRope

double price = 0.615小数点以下2桁にすると、0.61(切り捨て)になりますが、おそらく0.62(5のために切り上げられます)になります。

これは、double 0.615が実際には0.6149999999999999911182158029987476766109466552734375であるためです。

19
Roland Illig

実際に直面している主な問題は、round(a) + round(b)round(a+b)と必ずしも同じではないという事実に関連しています。 BigDecimalを使用することにより、丸め処理を細かく制御できるため、合計が正しく表示されるようになります。

18%VATなどの税金を計算する場合、正確に表現すると小数点以下3桁以上の値を取得するのは簡単です。そのため、丸めが問題になります。

あなたはそれぞれ1.3ドルで2つの記事を購入すると仮定しましょう

Article  Price  Price+VAT (exact)  Price+VAT (rounded)
A        1.3    1.534              1.53
B        1.3    1.534              1.53
sum      2.6    3.068              3.06
exact rounded   3.07

したがって、結果を印刷するために2回だけのラウンドで計算を行うと、合計3.07が得られますが、請求書の金額は実際には3.06になります。

11
Henry

ここで「技術的ではなく、哲学的」な答えをしましょう。通貨を扱うときに「Cobol」が浮動小数点演算を使用していないのはなぜだと思いますか?!

(引用符で囲まれた「Cobol」。既存のレガシーアプローチで現実のビジネス上の問題を解決します)。

意味:ほぼ50年前、人々がビジネスまたは金融作業にコンピューターを使用し始めたとき、彼らはすぐに「浮動小数点」表現は金融業界では機能しないことを認識しました)。

覚えておいてください:当時、abstractionsは本当に高価でした!ここに少しとそこにレジスタがあるほど高価でした。それでも、肩を抱える巨人にはすぐに明らかになります。「浮動小数点」を使用しても問題を解決できないことは明らかです。そして、彼らは何か他のものに頼らなければならなかったこと。より抽象的な-より高価です!

私たちの業界には、「通貨に有効な浮動小数点」を思い付くまで50年以上ありました。そして、common答えはまだです:やらないでください。代わりに、BigDecimalなどのソリューションを使用します。

9
GhostCat

例は必要ありません。あなただけの第四形式の数学が必要です。浮動小数点の小数部は2進基数で表され、2進基数は10進基数と通約できません。 10年生のもの。

したがって、常に丸めと近似が行われ、いずれの方法、形状、または形式のアカウンティングでもどちらも受け入れられません。帳簿は最後のセントまでバランスを取る必要があるため、FYIは毎日の終わりに銀行支店を行い、定期的に銀行全体を行います。

四捨五入エラーに苦しんでいる表現はカウントされません」

とんでもない。これはis問題です。丸めエラーを除外すると、問題全体が除外されます。

6
user207421

1000000000001.5(1e12の範囲内)のお金があるとします。そして、その117%を計算する必要があります。

ダブルでは、1170000000001.7549になります(この数はすでに不正確ですです)。次に、ラウンドアルゴリズムを適用すると、1170000000001.75になります。

正確な計算では、1170000000001.7550になり、1170000000001.76に丸められます。 痛い、あなたは1セントを失った

Doubleは正確な演算よりも劣る現実的な例だと思います。

もちろん、これを何らかの方法で修正できます(さらに、double arihmeticを使用してBigDecimalを実装できるため、ある意味でdoubleをすべてに使用でき、正確になります)が、ポイントは何ですか?

数値が2 ^ 53未満の場合、整数演算にdoubleを使用できます。これらの制約内で数学を表現できる場合、計算は正確になります(もちろん、分割には特別な注意が必要です)。この領域を離れるとすぐに、計算が不正確になる可能性があります。

ご覧のとおり、53ビットでは十分ではありませんdoubleでは不十分。ただし、10進固定小数点数でお金を保存する場合(つまり、数字money*100(セント精度が必要な場合)、64ビットで十分な場合があるため、BigDecimalの代わりに64ビット整数を使用できます。

3
geza

最下部のラインアップ:

doubleが失敗する単純で現実的な例:

すべてのより大きな数値型は、より小さな数値型のリストを使用し、符号や小数点以下の桁数などの記録を維持することにより、より小さな数値型によって完全にシミュレートできます。したがって、数値タイプは、使用するとコードの複雑度が高くなり、速度が遅くなる場合にのみ失敗します。

BigDecimalは、アンダーフローを回避するためにdoubleの乗算と除算を処理する方法を知っている場合、コードの複雑さをそれほど低下させません。ただし、BigDecimaldoubleよりも潜在的に高速である場合があります

ただし、doubleよりも(数学的に)strictlyの方がよい場合はありません。どうして? double計算は、最新のプロセッサ(1サイクル中)の単位演算として実装されているため、効率的な大精度浮動小数点計算は、その基礎で、何らかの種類の_double-esque_数値型を使用するため、または最適よりも遅いです。

つまり、doubleがブリックの場合、BigDecimalはブリックのスタックです。



そこで、最初に、「doubleは財務分析にとって悪い」という文脈で「悪い」とはどういう意味かを定義しましょう。


double浮動小数点数は、バイナリ状態のリストです。したがって、アクセスできるのがクラスと32ビット整数だけである場合は、小数点の位置、符号などを記録し、整数のリストを維持するだけで、doubleを「再作成」できます。

このプロセスの欠点は、これを管理するためのはるかに複雑でバグのあるコードベースが必要になることです。さらに、doubleは64ビットプロセッサのワードサイズに等しいため、整数のリストを含むクラスでは計算が遅くなります。



現在、コンピューターは非常に高速です。ずさんなコードを書いているのでない限り、doubleと、O(n)操作(1つのループ)の整数のリストを持つクラスとの違いに気付かないでしょう。 。

したがって、ここでの主な問題は、記述されたコードの複雑さ(使用、読み取りなどの複雑さ)です。



コードの複雑さが主要な問題であるため、小数を何回も掛ける財務状況を考慮してください。

これによりアンダーフローが発生する可能性があります。

アンダーフローの修正はログを取ることです:

_// small numbers a and b
double a = ...
double b = ...

double underflowed_number = a*pow(b,15); // this is potentially an inaccurate calculation. 

double accurate_number = pow(e,log(a) + 15*log(b)); // this is accurate
_

さて、質問は次のとおりです。これはあなたが処理するにはコードが複雑すぎますか?

または、さらに良いこと:仲間の従業員が処理するのは複雑すぎますか?誰かが一緒に来て、「うわー、これは本当に非効率に見えます。それをa*pow(b,15)に戻します」と言うでしょうか?

はいの場合、BigDecimal;を使用します。それ以外の場合:doubleは、アンダーフローの計算を除き、使用と構文の点でより軽量になります...そして、とにかく書かれたコードの複雑さはそれほど大したことではありません。


1つの注意点:銀行のバックエンドで実行される内部サブルーチンのネストされたforループなど、計算が複雑な設定でアンダーフロー回避策を含む重要な計算を行う場合は、BigDecimalを使用してテストする必要があります。これは実際には速いかもしれません。

だからあなたの質問への答え:

_// at some point, for some large_number this *might* be slower, 
// depending on hardware, and should be tested:
for (i=1; i<large_number; i++){
    for(j=1;j<large_number;j++){
        for(k=1;k<large_number;k++){
            // switched log to base 2 for speed
            double n = pow(2,log2(a) + 15*log2(b));
        }
    }
}

// this *might* be faster:
for (i=1; i<large_number; i++){
    for(j=1;j<large_number;j++){
        for(k=1;k<large_number;k++){
            BigDecimal n = a * pow(b,15);
        }
    }
}
_

時間があれば、漸近プロットを追加します。

0
donlan

以下は、「最も近いペニーに切り捨てる」必要があるメソッドの適切な実装のようです。

private static double roundDowntoPenny(double d ) {
    double e = d * 100;
    return ((int)e) / 100.0;
}

ただし、次の出力は、動作が期待したものとは異なることを示しています。

public static void main(String[] args) {
    System.out.println(roundDowntoPenny(10.30001));
    System.out.println(roundDowntoPenny(10.3000));
    System.out.println(roundDowntoPenny(10.20001));
    System.out.println(roundDowntoPenny(10.2000));
}

出力:

10.3
10.3
10.2
10.19 // Not expected!

もちろん、必要な出力を生成するメソッドを作成できます。問題は、実際にそれを行うのが非常に難しいことです(そして、価格を操作する必要があるすべての場所でそうすること)。

有限の桁数を持つすべての数字システム(10進数、2進数、16進数など)には、正確に格納できない有理数があります。たとえば、10進数では1/3を(有限桁で)格納できません。同様に、3/10は(有限桁で)base-2に格納できません。

任意の有理数を格納するために数字システムを選択する必要がある場合、どのシステムを選択したかは関係ありません。選択したシステムには、正確に格納できない有理数があります。

しかし、人間はコンピューターシステムの開発の前に物事に価格を割り当て始めました。したがって、5 + 1/3ではなく5.30のような価格が表示されます。たとえば、当社の証券取引所では10進数の価格が使用されます。つまり、10進数で表現できる価格でのみ注文を受け付け、見積もりを発行します。同様に、ベース2で正確に表すことができない価格で注文を発行し、注文を受け入れることができることを意味します。

これらの価格をベース2に格納(送信、操作)することにより、(正確な)ベース2(の表現)の数値を常に(正確な)ベース10の表現に常に正しく丸める丸めロジックに本質的に依存しています。

BigDecimalの使用は、暗号通貨(BTC、LTCなど)、株などの高価値のデジタル通貨を扱う場合に最も必要です。これらのような状況では、多くの場合、7または8つの有効数字。コードで3桁または4桁のfigで丸めエラーが誤って発生した場合、損失は非常に大きくなる可能性があります。丸め誤差のためにお金を失うことは、特にクライアント向けの場合、楽しいことではありません。

もちろん、すべてを正しく行うことができれば、すべてにDoubleを使用しても大丈夫かもしれませんが、特にゼロから始める場合は、リスクを冒さない方が良いでしょう。

0
Ryan - Llaver