Swift Digest
SE-0067 | Swift Evolution

Enhanced Floating Point Protocols

Proposal
SE-0067
Authors
Stephen Canon
Review Manager
Chris Lattner
Status
Implemented (Swift 3.0)

01 何が問題だったのか

Swift 2 時点の FloatingPoint プロトコルは、IEEE 754 に準拠した浮動小数点型が本来備えるべき機能のごく一部しか提供しておらず、Float / Double を汎用に扱うための足場として貧弱でした。具体的には次のような不足・不整合がありました。

Equatable / Comparable を要求していない

FloatingPoint プロトコル自体は Equatable にも Comparable にも適合していませんでした。実際の FloatDouble はもちろん ==< で比較できますが、ジェネリックなコードで「FloatingPoint に適合する任意の型」を比較したい場合には、その前提が型レベルで保証されていませんでした。

数値リテラルからの生成を保証できない

同様に、FloatLiteralConvertible への適合も要求されていませんでした。ジェネリック関数内で 0.01.0 といった浮動小数点リテラルをそのまま使うことができず、汎用的な数値アルゴリズムを書く上での障害になっていました。

基本定数が揃っていない

C の <float.h> が提供する DBL_MAX / DBL_MIN / DBL_EPSILON / M_PI のような基本定数に相当するものが、Swift の浮動小数点型には十分に揃っていませんでした。最大・最小の有限値、最小の正の正規数、1.0 の次の表現可能値との差、円周率 π といった、数値計算では頻繁に必要になる値を、言語の標準機能として取り出す手立てが不足していました。

% 演算子の誤用を招きやすい

浮動小数点数に対しても整数と同じ % 演算子が使えましたが、挙動は C の fmod に準じた打ち切り除算の剰余で、IEEE 754 が規定する「最も近い整数で割った余り」とは異なります。名前が整数の剰余と同じため、意図せず誤用しやすい状態でした。

Float80FloatingPoint に適合していない

x86 の拡張倍精度である Float80 は、FloatDouble と同じように扱えるはずの型でしたが、当時は FloatingPoint に適合しておらず、ジェネリックなコードから「もう一つの浮動小数点型」として透過的に扱えませんでした。

IEEE 754 の基本操作が一通り揃っていない

符号・指数・仮数(significand)からの生成、符号のコピー(copysign)、FMA(融合積和)、nextUp / nextDownremainderminNum / maxNum、全順序比較(totalOrder)、分類(正規数・非正規数・無限大・NaN の判定)など、IEEE 754 clause 5 が定める基本操作の多くが、プロトコルとしては整備されていませんでした。結果として、「浮動小数点型ならどれでも同じ API でこれらを呼べる」という保証がなく、汎用の数値コードを書くときに具体型に張り付いた実装を書かざるを得ない状況でした。

これらの不足が重なり、浮動小数点を対象としたジェネリックプログラミングや、数値計算ライブラリの基盤としての FloatingPoint は、実質的にはほとんど使いものになっていませんでした。

02 どのように解決されるのか

FloatingPoint プロトコルを全面的に拡充し、IEEE 754 clause 5 が定める基本操作を一通り備えた形に作り直します。あわせて、基数(radix)を 2 に固定したときにのみ意味を持つ API を切り出した BinaryFloatingPoint プロトコルを追加し、Float / Double / Float80 / CGFloat のすべてをこの二つのプロトコルに適合させます。Swift 3.0 で実装されています。

プロトコル階層

FloatingPointComparable(したがって Equatable)と IntegerLiteralConvertible、および符号付き数値を表す SignedNumber に適合します。さらに基数 2 専用の追加 API を持つ BinaryFloatingPoint を派生させ、こちらは FloatLiteralConvertible にも適合します。将来 10 進浮動小数点が追加される場合には、別途 DecimalFloatingPoint を派生させる余地が残されています。

public protocol FloatingPoint: Comparable, IntegerLiteralConvertible, SignedNumber {
  associatedtype Exponent: SignedInteger
  // ...
}

public protocol BinaryFloatingPoint: FloatingPoint, FloatLiteralConvertible {
  associatedtype RawSignificand: UnsignedInteger
  associatedtype RawExponent: UnsignedInteger
  // ...
}

プロトコル側に ComparableFloatLiteralConvertible への適合が入ったことで、ジェネリックコードから == / < による比較や、0.0 / 1.0 といった浮動小数点リテラルをそのまま使えるようになります。

基本定数

C の <float.h><math.h> に相当する基本定数が、型のスタティックプロパティとして揃います。

Double.infinity                  // +∞
Double.nan                       // quiet NaN
Double.signalingNaN              // signaling NaN
Double.greatestFiniteMagnitude   // DBL_MAX 相当
Double.leastNormalMagnitude      // DBL_MIN 相当(最小の正の正規数)
Double.leastNonzeroMagnitude     // 最小の正の数(非正規数を含む)
Double.ulpOfOne                  // DBL_EPSILON 相当(1.0 と次の表現可能値の差)
Double.pi                        // 円周率 π

また、インスタンスプロパティ x.ulp は「x の最下位桁の単位」、つまり多くの x について「x と次に大きな表現可能値との差」を返します。greatestFiniteMagnitude.ulp は有限値、NaN.ulp は NaN といったエッジケースまで定義されています。

名称として「epsilon」ではなく ulpOfOne / ulp を採用しているのは、「マシンイプシロン」という言葉が歴史的に複数の意味を持ち、かつ浮動小数点比較の許容誤差として誤用されがちであるためです。

符号・指数・仮数からの構築

IEEE 754 の scaleB(符号・指数・仮数から値を組み立てる操作)と copysign(符号だけをコピーする操作)がイニシャライザとして提供されます。

// value == (sign == .minus ? -1 : 1) * significand * radix**exponent
init(sign: Sign, exponent: Exponent, significand: Self)

// magnitude の符号を signOf から取り直す
init(signOf: Self, magnitudeOf other: Self)

Sign.plus / .minus の 2 ケースを持つ列挙型で、x.sign プロパティから取り出せます。x.sign == .minusx < 0 と同じではない点に注意が必要です。-0.0 の符号は .minus ですが -0.0 < 0 は偽であり、NaN については < がつねに偽になるのに対し、x.sign.plus にも .minus にもなり得ます。

対応するプロパティ x.exponentx.significand も提供されます。x.exponent|x| の底 r 対数の整数部、x.significandx = sign * significand * radix**exponent を満たすように取り出した仮数です。x が 0 や無限大や NaN の場合のエッジケースも明確に定義されており、次の式は常に x を正規化した値を返します。

Self(sign: x.sign, exponent: x.exponent, significand: x.significand)

算術操作と % 演算子の扱い

加減乗除・平方根・FMA(融合積和)といった IEEE 754 の基本演算が、メソッドとしてプロトコルに組み込まれます。

func adding(_ other: Self) -> Self
mutating func add(_ other: Self)

func multiplied(by other: Self) -> Self
mutating func multiply(by other: Self)

func divided(by other: Self) -> Self

func squareRoot() -> Self

// self + lhs * rhs を中間の丸めなしで計算する(IEEE 754 fusedMultiplyAdd)
func addingProduct(_ lhs: Self, _ rhs: Self) -> Self

通常の + / - / * / / / sqrt / == / < などの演算子・自由関数は、これらのメソッドを呼び出すジェネリックな実装として提供されます。

一方、浮動小数点型に対する % 演算子は廃止されます。代わりに、IEEE 754 が定める「最も近い整数で割った剰余」は remainder(dividingBy:)、C の fmod に相当する「切り捨て除算の剰余」は truncatingRemainder(dividingBy:) として、名前で挙動を区別できるようにします。

let a = 10.0
let b = 3.0
a.remainder(dividingBy: b)          // 1.0 (10 = 3 * 3 + 1)
a.truncatingRemainder(dividingBy: b) // 1.0
(-10.0).truncatingRemainder(dividingBy: 3.0) // -1.0(符号は被除数に従う)
(-10.0).remainder(dividingBy: 3.0)           // -1.0 or 0.5 系とは異なる丸め規則

比較と全順序

通常の == / < / <= は、NaN が絡むと三分律が崩れるため、プロトコル側には次の比較メソッドが用意され、演算子はこれらをフックとして呼びます。

func isEqual(to other: Self) -> Bool
func isLess(than other: Self) -> Bool
func isLessThanOrEqualTo(_ other: Self) -> Bool

さらに、非正準エンコーディングや符号付きゼロ、NaN まで含めてすべての値に全順序を与える isTotallyOrdered(below:) が用意されます。普段の比較では使いませんが、テーブルのソートキーのように「NaN を含む任意の値を一意に並べたい」場面で使えます。

最小・最大・大小の絶対値版

IEEE 754 の minNum / maxNum / minNumMag / maxNumMag がスタティックメソッドとして提供されます。片方が NaN のときにもう一方を返す、という IEEE 754 の規約に従います。

Double.minimum(1.0, .nan)         // 1.0
Double.maximum(1.0, 2.0)          // 2.0
Double.minimumMagnitude(-3.0, 2.0) // 2.0
Double.maximumMagnitude(-3.0, 2.0) // -3.0

nextUp / nextDown

x.nextUp は「x より大きい最小の表現可能値」、x.nextDown は「x より小さい最大の表現可能値」を返します。-0.0.nextUpleastNonzeroMagnitudegreatestFiniteMagnitude.nextUpinfinity になる、といったエッジケースまで揃っています。

分類と述語

数の種類を調べる述語も一通り整理されます。

x.isNormal        // 正規化された有限の非零値か
x.isFinite        // 有限か(0 / 非正規数 / 正規数のいずれか)
x.isZero          // 0 か
x.isSubnormal     // 非正規数か
x.isInfinite      // ±∞ か
x.isNaN           // NaN(quiet / signaling 両方)か
x.isSignalingNaN  // signaling NaN か
x.isCanonical     // 正準エンコーディングか(Float80 には非正準値が存在する)
x.floatingPointClass // 10 種のケースを持つ列挙型で分類を取得

BinaryFloatingPoint 固有の API

基数 2 の浮動小数点型に限って意味を持つ API は、BinaryFloatingPoint に集約されます。

  • exponentBitCount / significandBitCount: ビット幅(Float で 8 / 23、Double で 11 / 52 など)
  • exponentBitPattern / significandBitPattern: 符号以外のビットパターン
  • init(sign:exponentBitPattern:significandBitPattern:): ビットパターンから直接組み立てる
  • binade: 同じ符号・指数で仮数を 1.0 にした値(x == ±significand * 2**exponent に対する ±2**exponent
  • significandWidth: 仮数を表現するのに必要な実際のビット数

NaN ペイロード付きイニシャライザ

具体型 Float / Double / Float80 / CGFloat には、NaN のペイロードを指定できるイニシャライザが追加されます。プロトコル要件にはせず、具体型にのみ提供されます。

init(nan payload: Self.RawSignificand, signaling: Bool)

既存コードへの影響

  • 浮動小数点型に対する % 演算子は使えなくなり、formTruncatingRemainder(dividingBy:) または truncatingRemainder(dividingBy:) に置き換える必要があります。
  • 命名規約に合わせ、NaN / quietNaN / isSignaling といった名前は nan / isNaN / isSignalingNaN に整理されます(quietNaN は冗長なため削除)。

今後の見通し

この提案の API のうち、Integer プロトコル側の整備に依存するもの(FloatingPoint と整数型の汎用変換イニシャライザや、BinaryFloatingPoint 同士の汎用比較メソッドなど)は、整数プロトコル刷新(後の SE-0104)が入ったタイミングで順次実装されることになります。また、丸め関連のメソッドは SE-0113 で別途追加されます。これらは本提案で整理された土台の上に積み重なる形で、浮動小数点型のジェネリック基盤を完成させていく流れになります。