zshで$((0.1))が0.10000000000000001に拡張されるのはなぜですか?

zshで$((0.1))が0.10000000000000001に拡張されるのはなぜですか?

存在するzsh

$ echo $((0.1))
0.10000000000000001

そして浮動小数点演算拡張を持つ他のシェルでは:

$ ksh93 -c 'echo $((0.1))'
0.1
$ yash -c 'echo $((0.1))'
0.1

またはawk:

$ awk 'BEGIN{print 0.1 + 0}'
0.1

なぜ?


これはフォローアップです。チャットディスカッション

ベストアンサー1


長い話を短く

zshdouble情報が完全に保存され、算術式に安全に再入力されるように、浮動小数点算術を評価するために使用される2進数の10進数表現を選択します。これは化粧品を犠牲にして達成されます。これには17個の有効数字が必要で、再入力時に浮動小数点数として扱うように拡張に常に等が含まれるようにする.必要があります。e

doubleこの「完全精度」の10進数表現は、2進精度機械数字と人間が読める数字の間の中間形式と考えることができます。浮動小数点小数表現を理解するすべてのツールが理解する中間形式です。

0.1が算術式に使用される場合、doubleの0.1に最も近い17桁の10進表現は0.100000000000000001です。これは、double の精度制限と丸めによって発生するアーティファクトです。

他のシェルは形状に関して特権を持ち、10進数に変換すると一部の情報が失われます(追加の制約内でできるだけ多くの精度を維持しようとしますが)。どちらの方法にも長所と短所があります。詳しくは下記をご覧ください。

awkシェルではなく、浮動小数点を操作するときに2進数と10進数の表現の間を常に前後に変換する必要がないため、このような問題はありません。

zshメソッド

zsh算術演算は、他の多くのプログラミング言語(、を含む)や浮動小数点数を処理するシェルで使用される多くのツール(たとえば、...)など、yashこれらのksh93数値awkのバイナリ表現に対して実行されます。printf

これらの作業はCコンパイラでサポートされ、ほとんどのアーキテクチャでプロセッサ自体で実行されるため、便利で効率的です。

zshdouble間違いの内部表現にはCタイプを使用します。

ほとんどのアーキテクチャ(およびほとんどのコンパイラ)は、IEEE 754倍精度バイナリ浮動小数点を使用して実装されています。

これらの実装は、1.12e4エンジニアリング表記法の10進数と多少似ていますが、10進数(10進数)ではなく2進数(2進数)で実装されています。仮数は53ビット(暗黙の1ビット)、指数は11ビット(符号ビットを含む)です。これは通常必要なものよりも高い精度を提供します。

このような算術式が評価されると1. / 10(ここにはオペランドの1つとしてリテラル浮動小数点定数がある)、zsh内部的にリテラル10進表現からdoubles(標準strtod()関数を使用)に変換され、実行されますdouble

1/10 は 0.1 または 1e-1 で 10 進数で表現できますが、10 進数で 1/3 を表現できないように (3、6、9 進数は可能です)、1/10 も 2 進数で表現できません (10はそうではないからです)。 2電力)。 1/3 が 10 進数で 0.333333 adlib のように1/102進数でp-4

この値のうち52ビットしか保存できないため、1001...aの1/10はdouble1.1001100110011001100110011001100110011001100110011010p-4になります(最後の2桁は丸められます)。

これはsで得られる最も近い1/10表現ですdouble。これを10進数に変換すると、次のような結果が得られます。

#         1         2
#12345678901234567890
.1000000000000000055511151231257827021181583404541015625

doubleのもの(1.1001100110011001100110011001100110011001100110011001p-4は次のとおりです。

.09999999999999999167332731531132594682276248931884765625

そしてそれ以降のもの(1.1001100110011001100110011001100110011001100110011011p-4):

.10000000000000001942890293094023945741355419158935546875

それほど近いわけではありません。

まず、zshコマンドラインソルバーであるシェルがあります。すぐに算術式の結果を浮動小数点数としてコマンドに渡す必要があります。シェル以外のプログラミング言語は、double呼び出したい関数を渡します。しかし、シェルでは次のように渡すことができます。ひもコマンドに。生のバイト値doubleはNULバイトを含む可能性が高く、コマンドはそれをどのように処理するかわからないため、渡すことはできません。

したがって、これをコマンドが理解できる文字列表現に戻す必要があります。 IEEE 754バイナリ浮動小数点数を簡単に表すことができるC99 0xc.ccccccccccccccdd-7浮動小数点16進表記のような記号がありますが、まだ広くサポートされておらず、一般にほとんどの人にとって意味がありません。人々は0.1)上の視力を認識する。したがって、算術拡張の結果は、$((...))実際には10進表記で表される浮動小数点数です。

今、.1000000000000000055511151231257827021181583404541015625は少し長く、doubles(および算術式の結果)がそれほど高い精度を持っていないことを考慮すると、そのような高い精度があります。実際にdouble.1000000000000000055511151231257827021181583404541015625 、.1000000000000000005551115123125782

yashこのように(浮動小数点演算に内部的にsを使用する)15桁に切り捨てるとdouble0.1が得られますが、他の2つのdoublesに対しても0.1が得られます。したがって、私たちは3であるため、これらを区別することはできません。数字が異なるため、情報が失われます。 16ビットにカットしても、まだ2つの異なるdoublesが得られ、0.1になります。

IEEE 754 doubleに保存されている情報の損失を防ぐために、有効な10進数17桁を保存する必要があります。 〜のように倍精度ウィキペディア記事(IEEE 754の主な設計者であるWilliam Kahanによる論文の引用):

IEEE 754 doubleを有効数字が17以上の10進文字列に変換してから再度double表現に変換する場合、最終結果は元の数値と一致する必要があります。

逆に、少ないビットを使用すると、上記の例のようにdouble再変換すると同じ値が得られないいくつかのバイナリ値があります。double

これはzsh、バイナリ形式の完全な精度をdouble算術拡張の結果によって提供される10進表現として維持するように選択し、再使用時に何か(例えばzshの独自の算術式...)に変換できるようにするawkことprintf "%17f"です。 )aに戻ってdouble戻っても同じですdouble

コードが示すようにzsh(浮動小数点サポートが追加された2000年以降でしたzsh):

    /*
     * Conversion from a floating point expression without using
     * a variable.  The best bet in this case just seems to be
     * to use the general %g format with something like the maximum
     * double precision.
     */

.また、算術式で再使用されたときに浮動小数点数として処理されるように切り捨て、追加すると、小数部のない浮動小数点数を拡張することもわかります。

$ zsh -c 'echo $((0.5 * 4))'
2.

それ以外の場合、算術式で再使用されると、浮動小数点数ではなく整数として扱われ、使用される演算の動作に影響します(たとえば、2/4は整数除算であるため、0と2を生成します。結果は0.5です。

今、有効数字を選択することは、0.1を入力として使用すると、2進数1.10011001100110011001100110011001100110011010p-4 double(0.1に最も近い値)が0.10000000000000です。エラーが別の方向である場合、状況はさらに悪化します。たとえば、0.3は0.299999999999999999になります。

サポートされているアプリケーションにその番号を渡すときに逆の問題もあります。もっとsより精度が高い場合は、double実際に0.000000000000001エラー(0.1などのユーザー入力値から)を渡してから、そのエラーが重要になります。

$ v=$((0.1)) awk 'BEGIN{print ENVIRON["v"] == 0.1}'
1
$ v=$((0.1)) yash -c 'echo "$((v == 0.1))"'
1

いいですね。 sを使用するawkのと同じです。しかし:yashdoublezsh

$ echo "$((0.1)) == 0.1" | bc
0
$ v=$((0.1)) ksh93 -c 'echo "$((v == 0.1))"'
0

bc私のシステムでは、任意の精度と拡張精度を使用しているので、それはうまくいきません。ksh93

元の10進数入力が0.1(1/10)ではなく0.11111111111111111(または1/9の他の任意の近似)である場合、テーブルが反転して浮動小数点数の等価比較が非常に絶望的であることを示します。

ヒューマンディスプレイアーティファクトの問題は、精度を指定することで解決できます。表示するとき(すべての計算が完全な精度で完了した後)たとえば、次のようにしますprintf

$ x=$((1./10)); printf '%s %g\n' $x $x
0.10000000000000001 0.1

( 、 の浮動小数点数の基本出力形式の省略形に似ています%g)。これにより、整数の浮動小数点数の追加の末尾も削除されます。%.6gawk.

yash(およびksh93の)メソッド

yash精度を犠牲にしてアーティファクトを削除することを選択した10進数15桁は、$((0.1))数字を10進数から2進数に変換するか、逆に変換するときにこのアーティファクトが表示されないようにする有効な10進数の最大数です。

実際、2進数の情報は10進数に変換されると失われ、これは他の形式のアーティファクトを引き起こす可能性がある。

$ yash -c 'x=$((1./3)); echo "$((x == 1./3)) $((1./3 == 1./3))"'
0 1

(内部)等価比較は通常、浮動小数点に対して安全ではありません。ここではまったく同じ作業の結果なので、同じことがx期待できます。1./3

返品:

$ yash -c  'x=$((0.5 * 3)); y=$((1.25 * 4)); echo "$((x / y))"'
0.3
$ yash -c  'x=$((0.5 * 6)); y=$((1.25 * 4)); echo "$((x / y))"'
0

(yash は浮動小数点結果の 10 進表現に常に.OR を含まないので、e次の算術演算は最終的に整数演算または浮動小数点演算になる可能性があります.)

または:

$ yash -c 'a=$((1e15)); echo $((a*100000))'
1e+20
$ yash -c 'a=$((1e14)); echo $((a*100000))'
-8446744073709551616

(floatに$((1e15))展開して100000000000000に拡張すると、整数のように機能し、実際に浮動小数点乗算ではなく整数乗算を実行するため、オーバーフローが発生します。)1e+15$((1e14))

上記のように表示精度を減らしてアーティファクトの問題を解決する方法がありますがzsh、他のシェルでは精度損失を回復する方法はありません。

$ yash -c 'printf "%.17g\n" $((5./9))'
0.555555555555556

(まだ15桁しか残っていません)

それにもかかわらず、切り捨てがいくら短くても、算術拡張の結果として常にアーティファクトが発生する可能性があります。エラーは浮動小数点表現に固有のものです。

$ yash -c 'echo $((10.1 - 10))'
0.0999999999999996

浮動小数点で恒等演算子を実際に使用できない理由の別の例は次のとおりです。

$ zsh -c 'echo $((10.1 - 10 == 0.1))'
0
$ yash -c 'echo "$((10.1 - 10 == 0.1))"'
0

クッシュ 93

ksh93の状況はより複雑です。

ksh93 は、可能であればlong doubles を代わりに使用します。 s は C だけで少なくとも s だけ大きくなることが保証されます。実際には、コンパイラとアーキテクチャに応じて、通常sなどのIEEE 754倍精度(64ビット)、IEEE 754四重精度(128ビット)、または拡張精度(80ビット精度、通常128ビットに格納されます)です。 )ksh93がx86で動作するGNU / Linuxシステム用に構築されたものと同じです。doublelong doubledoubledouble

10進数で完全かつ明確に表現するには、それぞれ17、36、または21の有効数字が必要です。

ksh93は有効数字18桁で切り捨てられます。

long double現在はx86アーキテクチャでしかテストできませんが、sに似たシステムではdouble同じ結果が得られることがわかりますzsh(より悪くは17ではなく18桁を使用します)。

doublesの精度が80ビットまたは128ビットの場合、sと同じ問題があります。 ksh93は必要以上の精度を提供し、それだけの精度を維持するため、yashsを使用するツールと対話するときに優れています。double

$ ksh93 -c 'x=$((1./3)); echo "$((x == 1. / 3))"'
0

それでも「問題」ですが、以下ではありません。

$ ksh93 -c 'x=$((1./3)) awk "BEGIN{print ENVIRON[\"x\"] == 1/3}"'
1

大丈夫です。

しかし、行動が次善策である場所はtypeset -F<n>/-E<n>使用されます。この場合、ksh93は<n>15より大きい値を要求しても変数に値を割り当てると15桁の有効数字に切り捨てられます。

$ ksh93 -c 'typeset -F21 x; ((x = y = 1./3)); echo "$((x == y))"'
0
$ ksh93 -c 'typeset -F21 x; ((y = 1./3)); x=$y; echo "$((x == y))"'
0

動作に違いがあり、これはロケールの10進基数文字(3.14または3,14がksh93使用/認識されるかどうか)を処理するときに算術式内で算術拡張結果を再入力する機能に影響します。 zshは、ユーザーのロケールに関係なく、算術式で拡張された結果を常に使用できるという点で再び一貫性があります。zshyash

アッ

awkそのうちの一つプログラミング言語これはシェルではなく、浮動小数点数を処理します。同じように適用されますperl...

その変数は文字列に制限されず、通常、数値を内部でバイナリとして格納しますdoublegawk任意の精度の数値も拡張としてサポートされています)。文字列10進表現への変換は、次の場合にのみ発生します。印刷次の数字:

$ awk 'BEGIN {print 0.1}'
0.1

この場合、特殊変数OFMT%.6gデフォルトでは)で指定された形式を使用しますが、任意に大きくすることができます。

$ awk -v OFMT=%.80g 'BEGIN{print 0.1}'
0.1000000000000000055511151231257827021181583404541015625

subtr()または、文字列演算子(接続、...など)を使用する場合など、数値を文字列に暗黙的に変換するときは、index()CONVFMT変数(整数を除く)が使用されます。

$ awk -v OFMT=%.0e -v CONVFMT=%.17g 'BEGIN{x=0.1; print x, ""x}'
1e-01 0.10000000000000001

またはprintf明示的に使用されるとき。

10進数と2進数の表現の間を前後に変換しないため、一般に内部精度の損失の問題はありません。出力側では、どの程度の精度を与えるかを決めることができます。

結論として

最後に個人的な意見だけ申し上げます。

シェル浮動小数点演算は私が頻繁に使用するものではありません。ほとんどの場合、浮動小数点数を6桁の精度で印刷する自動ローディング電卓機能zshによって行われます。zcalcほとんどの場合、小数点の後の最初の3桁はこれらの使用タイプのノイズです。

算術拡張には高精度が必要です。いくつかのアーティファクトを避けながら、完全な精度であるか最高の精度であるかはそれほど重要ではないかもしれません。特に、誰もシェルを使用して多くの浮動小数点計算を実行しないことを考慮すると、さらにそうです。

小数点へのラウンドトリップは追加のエラーが発生しないという事実が私にとって慰めですがzsh、拡張の結果が算術式に安全に使用できることを知ることがより重要であることがわかりました。浮動小数点数は浮動小数点のままです,。スクリプトは、10 進数と同じロケールで使用しても機能し続けます。


1 zshは、10以外の進数で算術拡張を実行できる唯一のKorn様シェルですが、これは整数でのみ機能します。

おすすめ記事