Protocol-oriented integers
01 何が問題だったのか
Swift 3までの整数プロトコル群(IntegerArithmetic、BitwiseOperations、IntegerType など)は、整数型の実装を共有するための仕組みとしては機能していましたが、整数を対象としたジェネリックなアルゴリズムを書くには不十分でした。
ジェネリックプログラミングに向かない整数プロトコル
当時の IntegerArithmetic は、浮動小数点数と共通化できる設計になっておらず、算術演算子を具体的な整数型ごとに実装する必要があったため、オーバーロード集合が肥大化してコンパイルを遅くする要因にもなっていました。
整数型間の変換は「最大幅整数型(IntMax / UIntMax)を経由する」という前提で組まれており、toIntMax のようなAPIが導入されていました。これは、将来 Int256 のような型をライブラリで定義したいと思っても破綻する、という本質的な限界を抱えていました。
異なる整数型を混ぜて扱えない
整数同士の比較やシフトであっても、型が違うとそのまま書くことができず、いちいち片方を変換する必要がありました。
var x: Int8 = 42
let y = 1
let z = 0
x <<= y // error: binary operator '<<=' cannot be applied to operands of type 'Int8' and 'Int'
if x > z { ... } // error: binary operator '>' cannot be applied to operands of type 'Int8' and 'Int'
Int8 と Int のようなよくある組み合わせですら記述が煩雑になり、ジェネリックな関数を書こうとすると型の整合を取るための記述がさらに増えていました。
シフトの挙動がアーキテクチャ依存
ビットシフトは、シフト量が負だったり型の幅以上だったりした場合の挙動が、LLVM IRレベルでは未定義でした。プラットフォームによってはtrapしたり、結果がアーキテクチャ依存になったりするため、低レベルのビット操作が予期せず危険になりうる状態でした。
任意精度整数(BigInt)を作る土台がない
「最大幅整数型」という概念が表に出ているため、標準ライブラリの外で BigInt のような任意精度整数を効率よく実装する足場が整っていませんでした。桁あふれを伴う乗算・除算の完全な結果を取り出すAPIや、内部表現を machine word 列として取り出す手段も整備されていませんでした。
これらの問題は個別に手当てできるものではなく、整数プロトコル階層そのものを設計し直す必要がありました。
02 どのように解決されるのか
整数プロトコル階層を整理し直し、浮動小数点数とも共有できる Numeric を土台として、BinaryInteger と FixedWidthInteger を中心に据えた新しいモデルを導入します。これに合わせて、異なる整数型間の比較・シフト、安全なシフト演算、任意精度整数の実装に必要なAPIが揃います。
新しいプロトコル階層
新しい階層は次のように整理されます。
Numeric:+、-、*とその代入演算子、init?<T: BinaryInteger>(exactly:)、magnitudeプロパティを要求する。整数と浮動小数点の共通の土台となります。SignedNumeric: 単項-とnegate()を要求する。Numericを refine します。BinaryInteger: 全ての整数型の共通プロトコル。/、%、ビット演算子、シフト演算子、wordsプロパティ、bitWidth、trailingZeroBits、quotientAndRemainder(dividingBy:)、signum()などを要求します。Comparable・Hashable・CustomStringConvertible・Strideableにも適合します。FixedWidthInteger: 固定幅の整数型。max・min、addingReportingOverflow(_:)などのオーバーフロー報告版、multipliedFullWidth(by:)/dividingFullWidth(_:)、エンディアン関連API、マスキングシフト(&>>、&<<)を追加で要求します。SignedInteger/UnsignedInteger:BinaryIntegerを refine する補助プロトコル。
標準ライブラリの Int / UInt ファミリは FixedWidthInteger とそれぞれ SignedInteger / UnsignedInteger に適合します。FloatingPoint(SE-0067)は SignedNumeric を refine する形に揃うため、「整数と浮動小数点を横断するジェネリックコード」が書きやすくなります。
なお、剰余演算 % は整数と浮動小数点とで意味合いが異なるため、Numeric ではなく BinaryInteger 側に置かれています。
異なる整数型を混ぜて書ける
BinaryInteger は、異なる整数型を受け取る比較・等価・シフトの演算子を拡張として提供します。
let a: Int8 = 42
let b: Int = 1
let c: Int = 0
var x = a
x <<= b // Int8 を Int で左シフトできる
if x > c { ... } // Int8 と Int を直接比較できる
任意の BinaryInteger から別の整数型を作る初期化子も整理されます。用途ごとに使い分ける設計です。
let n: Int = 500
let a = Int32(n) // 表現可能であることが前提。無理ならランタイムエラー
let b = Int8(exactly: n) // 表現できなければ nil(Optional)
let c = Int8(clamping: n) // 範囲外は min / max に丸める(この例では 127)
let d = Int8(truncatingIfNeeded: n) // 下位ビットを切り出し、必要なら符号拡張
truncatingIfNeeded は、SE-0104 の改訂で extendingOrTruncating から改名されたものです。「切り詰め」というロスのある操作を明示的に示すためです。
浮動小数点からの変換は BinaryFloatingPoint に対して定義されます。
let x = Int(exactly: 21.0) // Optional(21)
let y = Int(exactly: 21.5) // nil
let z = Int(21.5) // 21(小数点以下を切り捨て)
スマートシフトとマスキングシフト
シフト演算子は2種類に整理されます。
- スマートシフト
>>/<<:BinaryIntegerに定義され、シフト量が負の場合は逆方向にシフトし、型の幅以上のシフトも定義された結果(符号ありなら符号拡張、符号なしなら0)になります。右辺は任意のBinaryIntegerを受け取れます。 - マスキングシフト
&>>/&<<:FixedWidthIntegerに定義され、シフト量を型のビット幅でマスクしてから適用します。多くのCPUのシフト命令そのままの挙動で、高速です。
たとえば、8ビット整数に対するシフトは次のように振る舞います。
let u: UInt8 = 1
u >> 42 // 0(スマートシフト: 幅を超えた分は 0 に飽和)
let s: Int8 = -128
s >> 42 // -1(符号ありなので符号拡張されて全ビット 1)
let v: UInt8 = 1
v &>> 2 // 0(マスキングシフト: 2 ビット右)
スマートシフトはアーキテクチャ差を吸収する代わりに分岐が入ることがあるため、性能が重要でシフト量が確実に範囲内とわかっている場合はマスキングシフトを選ぶ、という使い分けになります。
オーバーフロー報告API
FixedWidthInteger には、オーバーフローの有無をタプルで返すメソッドが揃います。以前は enum 型の ArithmeticOverflow で表現していましたが、単純な Bool(ラベル overflow)に置き換えられています。
let (sum, overflow) = Int8.max.addingReportingOverflow(1)
// sum == Int8.min, overflow == true
同じ系統で subtractingReportingOverflow(_:)、multipliedReportingOverflow(by:)、dividedReportingOverflow(by:) も提供されます。また、オーバーフロー時にラップする算術演算子として &+、&-、&* が FixedWidthInteger 上に定義されます。
倍幅乗算・倍幅除算と DoubleWidth
任意精度整数を自前で実装するために必要な低レベルAPIが追加されます。
let x: UInt8 = 100
let y: UInt8 = 20
let r = x.multipliedFullWidth(by: y)
// r.high == 0b0000_0111, r.low == 0b1101_0000(連結して 2000)
multipliedFullWidth(by:) は、結果が元の型に収まらない乗算の完全な結果を DoubleWidth<Self> として返します。対になる dividingFullWidth(_:) は、倍幅の被除数を自身で割って商と剰余を返します。
DoubleWidth<T: FixedWidthInteger> は、任意の FixedWidthInteger から幅を2倍にした固定幅整数型を合成するジェネリックな型です。FixedWidthInteger の要件を満たすため、DoubleWidth<Int64> のように使えば128ビット整数としても使えます(標準の Int64 を DoubleWidth<Int32> で代用すべきではありません。内部実装が必ずしも効率的ではないためです)。
public enum DoubleWidth<T: FixedWidthInteger> {
case parts(high: T, low: T.Magnitude)
public var high: T { get }
public var low: T.Magnitude { get }
}
enum で表現されているため、単一の値としても、パターンマッチで上位・下位に分解する形でも扱えます。
留意点(非目標)
SE-0104 は、異なる整数型どうしの算術演算を自動で昇格させる「integer promotion」は対象外です。比較やシフトを横断的に扱えるようにするだけでも、ジェネリックな整数コードの書きやすさは大きく改善しますが、Int8 + Int のような異種算術はこれまで通り明示的な変換が必要です。BigInt 本体の実装も含まれませんが、上記のAPI群はその土台となります。
既存コードへの影響
具体的な整数型(Int、UInt、Int32 など)を使うコードは基本的に書き換え不要です。影響が出うるのは、旧来の整数プロトコル(IntegerType、IntegerArithmetic、BitwiseOperations など)を制約として使っていたジェネリックコードで、新しい階層に合わせて制約を BinaryInteger / FixedWidthInteger などに置き換える必要があります。スマートシフトはアーキテクチャ差を吸収する分の分岐が入るため、シフト量が既知でホットな箇所ではマスキングシフトに置き換えると元の性能特性を取り戻せます。