https://www.youtube.com/watch?v=EpLGhoecHkU
今回は少し脱線しまして、先日の iOS LT 会で賑わってらした 浮動小数点数
の基礎的なところについて眺めてみることにしますね。
プログラミングの黎明期から今日までをずっと支えてきた仕組みで何気なく使っている割には、いざそれに着目してみると混乱しがちなところに感じたりもするので、ときおりこうしておさらいすると良いことあるかもしれません。
Swift 言語に限らない一般的な話も多くなる回と思うので、iOS/macOS 系な方々に限らず、そのほかの浮動小数点数に関わる特にアソシエイトエンジニアな方々も、ご都合が合えばぜひぜひお越しくださいね。どうぞよろしくお願いします。
———————————————————————— 熊谷さんのやさしい Swift 勉強会 #53
00:00 開始 00:18 余談 01:21 浮動小数点数 02:39 概要 04:53 Swift が提供する浮動小数点数型 05:17 Float80 07:56 Float80 の利用可能環境 09:57 Float80 の特徴 11:18 浮動小数点数型の選び方 12:05 Double 型の処理速度 13:59 浮動小数点数の仕様 14:35 概要(その2) 15:36 浮動小数点数の基本 15:57 固定小数点数 17:33 4つの構成要素 18:05 データ表現 18:52 データサイズに応じた呼称 19:56 精度による利用場面 21:19 擬似四倍精度浮動小数点数 22:46 データ配分 24:07 基数部は 0 ビット 24:47 基数が 2 の浮動小数点数 25:45 符号部 26:11 指数部 26:39 エクセス N 28:34 仮数部 30:10 二進小数 31:41 十進小数を表現できないことがある 32:06 正規化数 33:44 非正規化数 35:03 特別な表現 36:13 無限大 37:27 非数 38:43 二進小数から十進小数への変換 42:36 浮動小数点数に関する誤差 43:07 オーバーフロー 43:49 アンダーフロー 44:30 桁落ち 46:02 情報落ち 46:58 積み残し 47:31 丸め誤差 47:59 Swift での浮動小数点数表現 48:58 クロージング ————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #53
はい、じゃあ今日はちょっと趣向を変えて、不動小数点数のお話に行きたいなと思います。その前に、前回の勉強会でインアウトの話をしたと思いますが、その後、SwiftUIの勉強会を拝見していて、ちょっと興味深い話があったので補足したいと思います。
この勉強会で「これはやばい」とか「これは使うのが危なそうだね」ということがあっても、実際にその危険性を考慮した上で使っていくことは全然ありだと思います。むしろ、個人的にはそうやって使っていくのは良いことだなと思っています。なので、この勉強会で「これはやばいね」と言っても、どんどん皆さん使っていってくれると嬉しいなと思います。ちょっと語弊があるかもしれませんが、軽く補足しておきます。
はい、今日は不動小数点数のお話に入っていこうと思います。この勉強会では「The Swift Programming Language」の本を読みながら進めているので、この番外編として不動小数点数の話をするわけです。そのあたり、「The Swift Programming Language」に書かれている部分を見ていこうかと思ったのですが、「ザ・基本」にほんのちょっとだけしか書いてありませんでしたね。
なぜこの話題を挙げるかというと、前回のiOSLT会の不動小数点数の話がとても面白かったので、実際に考えてみると意外と難しかったり、分かっているつもりで混乱することが多々あることに気づきました。なので、自分の知識の整理も含めて、不動小数点数を見ていこうかなと思います。
まずは「ザ・基本」に書いてあった不動小数点数のところを見ていこうと思います。非常にシンプルなことしか書いていません。不動小数点数とは、小数部を持つ数値(3.14159や-273.15など)のことです。この数字はそれぞれ、円周率と絶対零度(考えられる最低温度)を表しています。物理についての詳しい話は割愛しますが、これは事実上の最低温度です。
さらに、不動小数点数は整数型よりも広範な範囲を表現可能なデータ型であり、大きな値や小さな値を表現できると書かれています。「Swift」では、2つの符号付き不動小数点数型が提供されています。Double
は64ビットの不動小数点数、Float
は32ビットの不動小数点数です。しかし、これには疑問が沸きます。もう一つ、不動小数点数型がありませんか?
最近のM1 Macを使っている方は知らないかもしれませんが、Float80
という80ビットの不動小数点数型があります。Playgroundが動かないので試せないのですが、80ビットの表現ができる不動小数点数型があるのです。
コンパイラーで試してみると、例えば
let doublePi: Double = 3.14159265358979
let floatPi: Float = 3.1415927
let float80Pi: Float80 = 3.1415926535897932384626433832795
とすると、Float32
はFloat
にリンクされ、Float64
はDouble
にリンクされていますが、Float80
は構造体として用意されています。これら3つがあって、それぞれ精度が違います。実際に定義をたどると、Swift
のMath
の中にFloatingPoint
として入っています。
なので、Swiftで提供されているのは3つではないかと思うのですが、「The Swift Programming Language」には記載がありません。これがとても気になるところです。 この辺りについて調べてみると、面白いことが分かります。Float80
というのは、x86環境、要はインテル系のCPUで規定されている不動小数点数です。スーパーコンピューターやその他のインテル系以外のCPUでは、Float80
をCPUでサポートしていないことが多いらしいです。もしプレイグラウンドで試している人がいたとしたら、人によってはFloat80
が使えていないかもしれません。iOS環境ではまず使えませんし、M1でも使えません。型の定義自体が存在していないのです。定義されなくなっているからです。他にFloat80
が使える環境はわずかにありますが、たとえばX68000というPC(モトローラのMC68000)などは対応します。昔のMacもそれを使っていました。そのため、Float80
は特別なもので、コプロセッサーで演算するようです。不動小数点専用の演算装置です。
これからお話ししますが、不動小数点数は世間一般に浸透しているのはIEEEの規格です。その中でも倍精度不動小数点数としてDouble
、単精度としてFloat
、そして4倍精度として128ビットのものはしっかりと規定されていますが、80ビットのものは言葉でさらっと出てくる程度のようです。Float80
は一般的ではなく、特殊な立ち位置の不動小数点数になっているようです。こういった理由から、すべてのプラットフォームで提供されているのはFloat
とDouble
の2つだけという意味なのでしょう。ただしインテルのCPUにおいてはFloat80
も使えます。
このような不動小数点数ですが、選び方としては基本的に精度が異なるため、計算目的に応じて適切な精度のものを選びます。どちらの方がいいか分からない場合や選べる場合、SwiftではDouble
型を推奨しています。選ぶ際には精度と計算速度、この2つを考慮して適切なものを選ぶべきですが、計算速度については詳しくは分かりません。以前、SwiftがDouble
型とコアグラフィックスで使われているCGFloat
を透過的に暗黙ブリッジする機能が組み込まれた際、現代のコンピューティングにおいて、サイズの小さいFloat
もサイズの大きいDouble
も計算速度は同等に扱えるという話があがっていました。これを踏まえると、どこまで適切かは判断できませんが、基本的にはDouble
の方が精度も高いし、計算速度もFloat
と同等なので、Double
を使うことにメリットが多いのではないかと思います。こうして、選んで使っていくのがSwiftの不動小数点数の扱い方だそうです。
The Swift Programming Language に書かれている不動小数点数については、この二つに関してしか触れられていなかったので、ちょっとさらっと書かれているなという印象です。しかし実際には不動小数点数というのは非常に複雑な仕様になっています。今日はその辺りをゆっくりと整理してみようかなと思います。ここからお話しする内容は主にWikipediaを基に、その上でちょっと足りない情報を整理し直したスライドになります。完全にSwiftに関する不動小数点数の話だけでなく、その比較としても捉えていただけたらと思います。Swiftの勉強会ということで、Swiftの視点で眺めていこうと思いますが、不動小数点数とは現代のコンピューティングにおける一般的な小数点数の表現方法ですよね。 この不動小数点数という技術は、主にパーソナルコンピューターで活躍していますが、その前についてはあまりよく知られていません。1970年代から現在に至るまで、不動小数点数は重要な技術として広く使用され続けています。その仕様は意外と複雑でありながら、高度な表現力を持っています。英語では「floating point number」と言い、小数点の位置が動く、不定形の数値表現が特徴です。
これに対して、固定小数点数という表現方法もあります。これは、データの一部を整数部とし、残りを小数点数として定義する方法です。このようなデータ表現は難しくありませんが、不動小数点数が広く使われているため、固定小数点数の存在がつい忘れられがちです。しかし、特定の状況では固定小数点数の方が適している場合もあります。例えば、計算が速く、誤差が生まれにくいという特徴があります。興味がある方は、固定小数点数を調べてみると良いでしょう。
今日の主題である不動小数点数について見ていきましょう。不動小数点数には、32ビット、64ビット、そして80ビットなどがあり、それぞれ異なる精度を持っています。これらは4つのパートで構成されており、符号、仮数、指数、そして比数がその要素です。
具体的な例を挙げると、ある値は符号付きで、仮数 × 比数の指数上、という表現になります。Swiftを使った例では、以下のようになります。
let value = (-1)^sign × mantissa × radix^exponent
基本的に、不動小数点数は5種類存在し、80ビット以外のものはIEEE 754規格で明確に規定されています。80ビットのものだけが少し特殊な扱いを受けています。
32ビットの単精度不動小数点数(Single Precision)、その2倍の精度を持つ64ビットの倍精度不動小数点数(Double Precision)、さらにその2倍の精度を持つ128ビットの四倍精度不動小数点数(Quad Precision)というものがあります。また、32ビットの半分の精度を持つ半精度不動小数点数も存在し、特にGPUでよく使用されます。一方、四倍精度はほとんど使われていないようです。
不動小数点数の詳細なデータサイズを考えると、符号部は1ビットで、指数部と仮数部のサイズがそれぞれ異なります。この設計により、数値の表現力が決まります。仮数部の場合、有効桁数が影響します。
一般には倍精度が最も広く使われていて、次に単精度という順番で使われることが多いようです。そして、128ビットの四倍精度については、特定の研究や画像処理などで使われることがありますが、一般的にはあまり使用されていません。
以上が、不動小数点数とその様々なバリエーションについての概要です。これを理解することで、どの状況でどの型を使うべきかの判断に役立つでしょう。 このビットが多いほど有効桁数が大きくなって、指数部が大きいほど桁数が大きくなったり小さくなったりという表現力を持っています。この仮数部は0ビットすべてのもの。ここが面白いところで、上手なデータ節約方法だなと思います。要は両方持たないけれど、これらの不動小数点数はすべて奇数を2として扱います。要は2進数、そういったふうな表現になっているのです。なので、こんな感じです。
実際のところ、IEEE754の指数はすべて2なので、不合がついて、その後に仮数×2の指数上というふうな表現になっています。ここまでオッケーですかね? ここまではシンプルな感じがしますよね。これだけ分かっていると不動小数点数を完全に理解したような、そんな雰囲気を持っちゃいますけど、意外とこれだけではなかなか難しいところがあります。そのあたりをもうちょっと細かく見ていこうかなと思います。
まず、この符号の部分から見ていこうかと思います。これは何も難しいことはなくて、1ビット表現で、他の一般的な数値表現と同じで、正の値の場合は0、不の値の場合は1で表現するという決まりになっています。これはオッケーですね。
次に、指数部の2の何乗のところです。これが各不動小数点数によって長さやデータの長さが異なっています。ここをビット的には符号なし整数値として解釈します。ただし、符号なし整数値として解釈すると、その値、要は2の整数でしか表現できないので、実質的に小数表現ができないことはないですが、難しく限られてきます。そこで、符号なし整数として解釈された数値の中から、各精度ごとにあらかじめ決められている数字を引いてあげるのです。それにより、マイナスいくつかからプラスいくつかまでの符号なし整数値として指数を表現できるようになっています。こういうふうに、ある一定の数値を引いて値をシフトしてあげる、こういった積みのことを「excess-n」と表現します。初めて聞いたんですけど、この「excess-n」がどれだけシフトするか。だから倍制度だったら「excess 1023」といった表現なんですね。こうやって指数部が表現されています。
さっきも言ったように、大事なところとして、この指数部のデータ長が大きければ大きいほど、桁数2の何乗のところがどんどん大きくしていけるので、二進数的に桁数が増えていくという特徴があります。
仮数部はシグニフィカンドパートと言いますが、IEEEの仕様ではフラクションパートと呼ぶみたいです。Swiftではシグニフィカンドと呼びます。この仮数部も精度によってデータ長が異なるのです。この扱い方としては整数部分が1となってそれに続く小数部分、そこだけを表現しています。なので「1.何何何何」というふうな表現になるのです。そのうちの1を除いて、例えば0.10101とか書いてありますけど、不動小数点数として使うときには実際には「1.10101」という感じで使われますが、仮数部的にはビット列は「1.0101」というふうに表現されています。このビット列は二進小数として解釈されるようになっています。なので「1.0101」だったら0.10101、厳密には「1.10101」と解釈され、それぞれの桁が他の進数の表数と同じように、0.1だったらその桁は10分の1を表現し、その次だと100分の1を表現とまったく同じ感覚で、二進数では0.1だったらその桁が2分の1で、その次の桁は2の1乗分の1、次の桁は2の2乗分の1、次が2の3乗分の1の重みで、次が2の4乗分の1の重みみたいな計算になります。これを計算しやすくすると、1×2分の1が0.5、そのプラス2乗分の1は0.25なので、それをビットを掛けていくといった計算になります。
このように表現されているのが仮数部です。こんな感じで2の何乗で表現していくので、10進数で表現していた数字を必ずしも正確に表現できない場合が多いですけど、こういった積みになっています。この仮数部と指数部を使って不動小数点数が「符号・仮数×2の指数乗」という形で表現されています。不動小数点数の中で特に大事なのが仮数部で、今までの説明がほぼ仮数部の説明だったので、繰り返しになりましたけど、整数部が1であるものとして、それ以下の小数部分、それだけをビット列で表現しているのです。 ですので、正規仮数は必ず 1.何何掛ける2の何乗
という形で表現されます。これは出振数や物理学でもおなじみの 1.何何掛ける10の何乗
といった表現に似ています。こういった表現をするのが正規化された不動小数点数です。
不動小数点数の規格の中で、0.何何掛ける2の何乗
のような数は正規仮数ではなく非正規仮数になります。非正規仮数は 1.何何
という形式で表現できませんが、0.0000何掛ける2の何乗
のようにゼロに近い値を表現することができます。これは、比数部分のビット列を全てゼロにすることで非正規仮数とするもので、この場合、仮数部が暗黙的に 1.何何
ではなくなる制約がなくなり、より小さい値を二進小数として表現できるのです。
また、不動小数点数には特別なルールがあります。比数部のビット列が特定のパターンで別の意味を持つことがあります。例えば、比数部の全てがゼロのとき、非正規仮数と同様にゼロに近い数値ですが、仮数部分が完全にゼロであればぴったりゼロを表現します。そして、符号によってプラスゼロかマイナスゼロかが決まります。
比数部分のビットが全て1の場合、このとき仮数部が全てゼロであれば、無限大を表現できます。この無限大には特殊な性質があり、無限大プラス何かの値は依然として無限大であり、ある値を無限大で割るとゼロに近づきますが、厳密にはゼロになるのかどうかは忘れてしまいました。
その他に比数部のビット列に1が含まれるとき、これは「Not a Number」(NaN:非数)として扱われます。NaN同士の比較は必ずFalseとなるため、特別な扱いを受けます。
このように、比数部のビットパターンによって特殊な意味を持つことが不動小数点数の特性です。
コメントで「実心数小数を二心数小数に変換する方法」がありました。実際には、2を掛けていくことでビットをシフトする効果があります。2
を掛けるとビット的に左シフト、2
で割ると右シフトするのと同じ効果が得られ、これを利用して昔は掛け算・割り算が搭載されていないCPUで演算を行っていました。
たとえば、Z80などでは、この方法で掛け算や割り算を実装していましたが、後のMSXのR800 CPUに掛け算命令が搭載され、これらの手法が不要になりました。昔のPC話ですが、懐かしい思い出です。どこまで通じる人がいるか分かりませんが。 とりあえずビットが大事な軸になっているのがフローティングポイントです。前回のiOS LT会で話題に上がっていたデシマルの誤差の話は、多分、このコメントにあった10進の小数表現と2進の小数表現の変換で誤差が出ているのだと思います。多分というか、ほぼ間違いないですね。
2進表現、例えばダブル型のときに 123.456
がぴったり表現されるという話は、実際にはビット表現の中で2進小数として正確に表現されていません。ただ、限りなく近い精度で 123.456
を表現しようとします。最終的に目に見える形にする際には最下位ビットでゼロまたは一に丸められる形で表示されるだけです。そのため、見かけ上123.456がぴったり表示されるように見えるのです。裏で 123.456
をそのまま保存しているわけではありません。
これは前回のLT会に参加された方向けの話ですが、不動小数点数は符号と仮数かける2の指数上という特殊な表現方法をしています。10進数で見ると、この特殊な表現方法によって誤差が発生します。この話は10進数から見た場合の問題だけでなく、固定バイナリ長というデータ上の制約からも発生する誤差です。不動小数点数を厳密に使おうとする場合は、こういった誤差を意識する必要があります。
表現力を超えた場合、指数部分が表現可能な範囲を超えたときにオーバーフローが発生し、適切な表現ができなくなります。逆に指数部分で表現できる範囲がゼロに限りなく近づいたときには、アンダーフローが発生します。アンダーフローが発生すると正規化数から非正規化数になり、最終的にはゼロとして扱われるようになります。
また、桁落ちという誤差も存在します。ほとんど同じ値同士でゼロに近づく場合(例えば、同じような値同士の引き算や、ある値と絶対値が同じ異なる符号同士の足し算)に、有効桁数が限りなくゼロに近くなり、正規化されるときに情報が落ちてしまうことがあります。この桁落ちの誤差も注意が必要です。
さらに、情報落ちの問題もあります。不動小数点数演算では、指数(何乗の部分)を揃えて計算するため、指数部分が大きく異なる数字(巨大な数字と極めて小さい数字同士)を演算する際には、小さい方の桁数が落ちてしまいます。これが繰り返されると積み残し誤差として現れます。このように、繰り返し計算する場合には注意が必要です。
あと、有効桁は仮数部でのみ表現されているので、溢れた分は二進数で丸められてしまい、誤差が生じます。これは不動小数点数に特有の問題です。
Swiftでは、FloatingPoint
プロトコルで不動小数点数を表現しており、IEEE 754の不動小数点数がサポートされています。興味がある方は、フローティングポイント型の変数にドットを付けてどんなプロパティがあるかを確認してみると、今日の話と照らし合わせて面白いかもしれません。
また、Decimal
型はSwiftではなく、Core Libraryのファンデーションに規定されています。これは10進表現ですが、小数も表現可能で、フローティングポイントに相当する機能を備えています。奇数が10というのも面白いポイントです。
時間になりましたので、これで勉強会を終わりにしようと思います。お疲れ様でした。ありがとうございました。