Swift Digest
SE-0457 | Swift Evolution

Expose attosecond representation of Duration

Proposal
SE-0457
Authors
Philipp Gabriel
Review Manager
Stephen Canon
Status
Implemented (Swift 6.2)

01 何が問題だったのか

SE-0329 で導入された Duration は、秒とアト秒(10⁻¹⁸秒)からなる 128 ビット整数で時間の長さを表現する型です。内部的には _high: Int64_low: UInt64 に分割して 128 ビットを保持していますが、これを外から扱う API として公開されているのは次の 2 通りだけでした。

低位 / 高位ビットによる分解(underscored)

public struct Duration: Sendable {
  public var _low: UInt64
  public var _high: Int64
  public init(_high: Int64, low: UInt64) { ... }
}

秒成分とアト秒成分への分解

extension Duration {
  public var components: (seconds: Int64, attoseconds: Int64) { get }
  public init(secondsComponent: Int64, attosecondsComponent: Int64)
}

前者はアンダースコア付きで、直接利用は想定されていません。後者は timespec などの既存 API との相互運用のためのもので、Duration が保持している値を「秒」と「その秒未満のアト秒」の 2 成分に分けて返す設計になっています。したがって、Duration が表す総アト秒数(=内部の 128 ビット整数そのもの)をそのまま受け渡す手段はありませんでした。

Duration 自体は SE-0329 の時点で 128 ビットのアト秒値として定義されていたものの、Swift には当時 128 ビット整数型がなく、「128 ビットとしての総アト秒」を公開型として表現できなかったという経緯があります。その後 Int128 が標準ライブラリに追加されたため、この制約が解消されました。

総アト秒を直接扱えないと、たとえば「指定した上限までのランダムな Duration を作る」といった処理が冗長になります。元 Proposal の例では次のようなコードが必要でした。

func randomDuration(upTo maxDuration: Duration) -> Duration {
  let attosecondsPerSecond: Int128 = 1_000_000_000_000_000_000
  let upperRange =
    Int128(maxDuration.components.seconds) * attosecondsPerSecond
    + Int128(maxDuration.components.attoseconds)
  let (seconds, attoseconds) =
    Int128.random(in: 0..<upperRange).quotientAndRemainder(dividingBy: attosecondsPerSecond)
  return .init(secondsComponent: Int64(seconds), attosecondsComponent: Int64(attoseconds))
}

components で秒とアト秒に分解し、Int128 に再構築して計算したあと、また秒とアト秒に分け直して Duration を作り直す、という変換が何度も必要で、冗長でミスが入り込みやすく、性能面でも不利でした。

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

Duration に、総アト秒を Int128 として直接読み書きする 3 つ目の API を追加します。既存の components / _high_low はそのまま残り、追加のみで互換性を壊しません。

attoseconds computed property と init(attoseconds:)

次の 2 つが Duration に追加されます。

@available(SwiftStdlib 6.0, *)
extension Duration {
  /// The duration represented in attoseconds.
  public var attoseconds: Int128 {
    Int128(_low: _low, _high: _high)
  }

  /// Initializes a `Duration` from the given number of attoseconds.
  public init(attoseconds: Int128) {
    self.init(_high: attoseconds._high, low: attoseconds._low)
  }
}

attosecondsDuration が内部で保持している 128 ビットのアト秒値そのものを Int128 として返します。init(attoseconds:)Int128 値から直接 Duration を構築します。どちらもビット表現をそのまま橋渡しするだけで、スケーリングや丸めは発生しません。

これにより、先ほどのランダム生成の例は次のように大幅に短く書けます。

func randomDuration(upTo maxDuration: Duration) -> Duration {
  return Duration(attoseconds: Int128.random(in: 0..<maxDuration.attoseconds))
}

総アト秒を Int128 としてそのまま扱えるので、秒/アト秒への分解・再構築の往復が不要になり、記述が簡潔になるだけでなく、Duration を使った高精度な算術(ランダム生成、比率計算、スケーリング、シリアライズなど)も書きやすくなります。

ファクトリメソッドではなくイニシャライザにした理由

Duration には nanoseconds(_:)microseconds(_:) のような static func のファクトリメソッドが用意されており、attoseconds(_:) を同じ形で足すことも検討されました。しかし既存のファクトリは BinaryIntegerDouble の両方のオーバーロードを持つのが通例で、アト秒の場合は次の理由からそれが噛み合いません。

  • アト秒より細かい単位は表現できないため、Double オーバーロードは意味を持ちません。
  • BinaryInteger オーバーロードを用意すると、Int128 以外からの変換でスケーリングや切り捨てが必要になり、「Int128 をそのまま 128 ビット値として受け渡す」という今回の API の狙い(単純さと精度の保証)が崩れます。

結果として attoseconds(_:)Int128 専用の浮いたメソッドになってしまい、他のファクトリとの対称性も崩れます。そこで、components 用の init(secondsComponent:attosecondsComponent:) と揃えて、イニシャライザの形で提供することが選ばれました。