Swift Digest
SE-0523 | Swift Evolution

Hashable conformance for UnownedTaskExecutor

Proposal
SE-0523
Authors
Fabian Fett, Konrad Malawski
Review Manager
John McCall
Status
Accepted

01 何が問題だったのか

SE-0417 で導入された task executor preference により、withUnsafeCurrentTask を通じて、実行中の Task が現在どのエグゼキュータ上で動いているかを UnownedTaskExecutor として取得できるようになりました。これをキーに「そのエグゼキュータに紐づくリソース」を引くパターンは、コンテキストスイッチを減らしたいパフォーマンス重視のコードで自然に現れます。

典型例はコネクションプールです。ConnectionPool が複数のエグゼキュータにまたがって接続を保持している場合、リクエストしてきた Task と同じエグゼキュータに紐づく接続を優先して返せば、スレッド/エグゼキュータ間の切り替えを避けられます。

ところが、UnownedTaskExecutorEquatable にしか適合していませんでした。そのため、エグゼキュータをキーに接続を検索しようとすると、線形探索するしかありません。

// 今までは O(n) の線形探索
func pickConnection(preferring executor: UnownedTaskExecutor) -> Connection {
    for (e, connection) in executorConnectionList {
        if e == executor { return connection }
    }
    // fallback
    return executorConnectionList.first!.connection
}

エグゼキュータをキーにリソースやキャッシュ、スケジューリング用メタデータを索引付けしたいという要求は、コネクションプールに限らず一般的に存在します。これらをまとめて辞書 (Dictionary) や集合 (Set) で扱えるようにするには、UnownedTaskExecutorHashable にも適合している必要がありました。

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

UnownedTaskExecutorHashable 適合を追加します。

extension UnownedTaskExecutor: Hashable {}

UnownedTaskExecutor は内部的にエグゼキュータ参照のアイデンティティを表す値を保持した struct で、既存の Equatable 適合もそのアイデンティティに基づいて定義されていました。今回追加される Hashable 適合も同じアイデンティティ値をハッシュに使うため、==hash(into:) の整合性(a == b なら a.hashValue == b.hashValue)は自然に保たれます。

これにより、先ほどのコネクションプールの例は辞書引きによる定数時間のルックアップで書けるようになります。

// Hashable によって O(1) の辞書引きに
func pickConnection(preferring executor: UnownedTaskExecutor) -> Connection {
    if let connection = connectionsByExecutor[executor] {
        return connection
    }
    // fallback
    return connectionsByExecutor.values.first!
}

公開 API への変更はこの Hashable 適合の追加のみで、純粋に追加的な変更です。既存のコードはそのままコンパイルでき、Equatable にすでに適合している型が Hashable を取り込むだけなのでソース互換性も保たれます。