Skip to content

Instantly share code, notes, and snippets.

@kateinoigakukun
Created October 4, 2022 10:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kateinoigakukun/090c2af523805975edaf2482d736c47b to your computer and use it in GitHub Desktop.
Save kateinoigakukun/090c2af523805975edaf2482d736c47b to your computer and use it in GitHub Desktop.

text: text-scale(0.75), line-height(0.9) code: text-scale(1.0), line-height(1.0) theme: Next, 4 slidenumbers: true

Swift KeyPath Internals

わいわいswiftc #38

@kateinoigakukun


近況

  • 研究: サイズ最適化頑張ってる
    • マイクロベンチでしか効かない最適化 😢
  • Ruby: Venturaで動くように頑張った
  • SwiftWasm: プロダクション事例出てきてうれしい

^ 前回は3月だったらしい。半年ぶり。


KeyPathの特徴


KeyPath 便利

乱暴に言うと gettersetter を取り回す表現

色んな所で使われてる

  • subscript(dynamicMember: KeyPath<X, Y>) -> Z
  • func map<T>(_ keyPath: KeyPath<Self.Output, T>) -> Publishers.MapKeyPath<Self, T>
  • Key Paths Expressions as Functions

KeyPath むずかしい

意味は同じだが、動作は全く違う。

foo[\.bar]
foo.bar

KeyPath おそい

[.column]

struct Foo {
    let bar: Int
}

let foo = Foo(bar: 42)

@_optimize(none)
func getter() { _ = foo.bar }

@_optimize(none)
func keyPathGetter() { _ = foo[keyPath: \.bar] }

print("Simple Getter  =>", ContinuousClock().measure {
    for _ in 1...10000000 { getter() }
})

print("KeyPath Getter =>", ContinuousClock().measure {
    for _ in 1...10000000 { keyPathGetter() }
})

[.column]

$ swiftc main.swift
$ ./main
Simple Getter  => 0.046666295 seconds
KeyPath Getter => 1.590084352 seconds

(最適化しないと) 34倍 おそい 🐢


KeyPath 使ってる…?

使われて無さそうなマイナー機能

  • \[String: Int].["foo"]
  • \[Int].last?.littleEndian
  • class KeyPath: Hashable, Equatable
  • PartialKeyPath.appending(path:)
let arrayDescription: PartialKeyPath<Array<Int>> = \.description
let stringLength: PartialKeyPath<String> = \.count
let arrayDescriptionLength = arrayDescription.appending(path: stringLength)

暗黙に subscript indexキャプチャしてるケース

KeyPathがSendableになれない話 by iceman

@dynamicMemberLookup struct Box<Value> {
    var value: Value
    subscript<T>(dynamicMember keyPath: KeyPath<Value, T>) -> T {
        get { value[keyPath: keyPath] }
    }
}

struct ContentView: View {
  @State private var state = Box(value: Dog())

  var body: some View {
    TextField("", text: $state.name)
//  TextField("", text: _state.projectedValue[dynamicMember: \Box<Dog>.[dynamicMember: \Dog.name]])
  }
}

KeyPath実装の概要


かなり単純化したKeyPathの実装

let kp0 = \Foo.bar
// -> let kp0 = KeyPath(getter: { $0.bar })

let kp1 = \Foo.[42]
// -> let kp1 = KeyPath(getter: { $0[42] })

実際はもっと複雑


KeyPath実装の歩き方(コンパイラ部分)

実行順。意味解析まで省略

File Description
lib/SILGen/SILGenExpr.cpp KeyPathInst SIL命令とアクセサを生成
lib/SILOptimizer/SILCombiner/SILCombinerApplyVisitors.cpp getter/setterアクセスへ最適化
lib/IRGen/GenKeyPath.cpp ランタイムが読むためのKeyPathデータとサンクを生成

KeyPath実装の歩き方(ランタイム部分)

stdlib/public/core/KeyPath.swift

  • コアのロジックすべてが記述されてる
    • インスタンス化
    • アクセス
    • 比較など
  • Swiftで書かれている

KeyPathの実装詳細


もくじ

  1. SIL上のKeyPath表現
  2. LLVM IR上のKeyPath表現
  3. KeyPathのランタイム実装
  4. コンパイラ最適化

SIL上のKeyPath表現


SIL上の表現

[.code-highlight: all] [.code-highlight: 4-6] [.code-highlight: 7-8] [.code-highlight: 9-11] [.code-highlight: 12-17] [.code-highlight: all]

// keyPathGetter()
sil hidden [Onone] @$s4main13keyPathGetteryyF : $@convention(thin) () -> () {
bb0:
  // 1. グローバル変数 `foo` をロード
  %0 = global_addr @$s4main3fooAA3FooVvp : $*Foo  // user: %1
  %1 = load %0 : $*Foo                            // user: %4
  // 2. `\Foo.bar` のKeyPathを生成
  %2 = keypath $KeyPath<Foo, Int>, (root $Foo; stored_property #Foo.bar : $Int) // users: %10, %7
  // 3. `foo` をスタックにアロケート
  %3 = alloc_stack $Foo                           // users: %4, %9, %7
  store %1 to %3 : $*Foo                          // id: %4
  // 4. `foo` からKeyPath経由で `bar` を取得
  %5 = function_ref @swift_getAtKeyPath
    : $@convention(thin) <τ_0_0, τ_0_1> (@in_guaranteed τ_0_0, @guaranteed KeyPath<τ_0_0, τ_0_1>) -> @out τ_0_1 // user: %7
  %6 = alloc_stack $Int                           // users: %8, %7
  %7 = apply %5<Foo, Int>(%6, %3, %2)
    : $@convention(thin) <τ_0_0, τ_0_1> (@in_guaranteed τ_0_0, @guaranteed KeyPath<τ_0_0, τ_0_1>) -> @out τ_0_1
  dealloc_stack %6 : $*Int                        // id: %8
  dealloc_stack %3 : $*Foo                        // id: %9
  strong_release %2 : $KeyPath<Foo, Int>          // id: %10
  %11 = tuple ()                                  // user: %12
  return %11 : $()                                // id: %12
} // end sil function '$s4main13keyPathGetteryyF'

KeyPathInstの構造

keypath /* 得られるKeyPath型 */, (
  root /* ルートの型 */;
  /* パスの要素1 (KeyPathPatternComponent) */
  /* パスの要素2 (KeyPathPatternComponent) */
  /* パスの要素N (KeyPathPatternComponent) */
)

KeyPathPatternComponent の種類

class KeyPathPatternComponent {
    ...
    enum class Kind: unsigned {
      StoredProperty,   // \Foo.bar (stored)
      GettableProperty, // \Foo.bar (getter), \Foo.[42] (subscript)
      SettableProperty, // GettableProperty + setter
      TupleElement,     // \(Int, String).1
      OptionalChain,    // \Foo.bar?.baz
      OptionalForce,    // \Foo.bar!
      OptionalWrap,     // \Foo.bar?.baz
    };
};

StoredProperty

struct Foo {
    let bar: Int
}

// `\Foo.bar` =>
%2 = keypath $KeyPath<Foo, Int>, (
  root $Foo;
  stored_property #Foo.bar : $Int
)

StoredProperty

struct Foo {
    var bar: Int
}

// `\Foo.bar` =>
%2 = keypath $WritableKeyPath<Foo, Int>, (
  root $Foo;
  stored_property #Foo.bar : $Int
)

TupleElement

typealias Foo = (
    bar: Int,
    baz: String
)

// `\Foo.bar` =>
%2 = keypath $WritableKeyPath<(bar: Int, baz: String), Int>, (
  root $(bar: Int, String);
  tuple_element #0 : $Int
)

GettableProperty

struct Foo {
    var bar: Int { 42 }
}

// `\Foo.bar` =>
%2 = keypath $KeyPath<Foo, Int>, (
  root $Foo;
  gettable_property $Int,
    id @$s4main3FooV3barSivg : $@convention(method) (Foo) -> Int,
    getter @$s4main3FooV3barSivpACTK : $@convention(thin) (@in_guaranteed Foo) -> @out Int
)

GettableProperty (Subscript)

struct Foo {
    subscript(x: Int) -> Int { 0xbadbabe }
}

// `\Foo.[42]` =>
%2 = integer_literal $Builtin.Int64, 0xdeadbeef
%3 = struct $Int (%2 : $Builtin.Int64)
%4 = keypath $KeyPath<Foo, Int>, (
  root $Foo;
  gettable_property $Int,
    id @$s4main3FooVyS2icig : $@convention(method) (Int, Foo) -> Int,
    getter @$s4main3FooVyS2icipACTK : $@convention(thin) (@in_guaranteed Foo, UnsafeRawPointer) -> @out Int,
    indices [%$0 : $Int : $Int],
    indices_equals @$sSiTH : $@convention(thin) (UnsafeRawPointer, UnsafeRawPointer) -> Bool,
    indices_hash @$sSiTh : $@convention(thin) (UnsafeRawPointer) -> Int
) (%3)

値(indices)、比較、ハッシュのための関数をキャプチャ


GettableProperty (Subscript)

  • ランタイムから呼ばれる用のサンク
  • すべてのcomputed subscriptを単一のシグネチャで呼び出すために必要
// key path getter for Foo.subscript(_:) : Foo
sil shared [thunk] @$s4main3FooVyS2icipACTK : $@convention(thin) (@in_guaranteed Foo, UnsafeRawPointer) -> @out Int {
// %0                                             // user: %8
// %1                                             // user: %3
// %2                                             // user: %4
bb0(%0 : $*Int, %1 : $*Foo, %2 : $UnsafeRawPointer):
  %3 = load %1 : $*Foo                            // user: %7
  %4 = pointer_to_address %2 : $UnsafeRawPointer to $*Int // user: %5
  %5 = load %4 : $*Int                            // user: %7
  // function_ref Foo.subscript.getter
  %6 = function_ref @$s4main3FooVyS2icig : $@convention(method) (Int, Foo) -> Int // user: %7
  %7 = apply %6(%5, %3) : $@convention(method) (Int, Foo) -> Int // user: %8
  store %7 to %0 : $*Int                          // id: %8
  %9 = tuple ()                                   // user: %10
  return %9 : $()                                 // id: %10
} // end sil function '$s4main3FooVyS2icipACTK'

SettableProperty

struct Foo {
    var bar: Int { get { 42 } set { } }
}

// `\Foo.bar` =>
%2 = keypath $WritableKeyPath<Foo, Int>, (
  root $Foo;
  settable_property $Int,
    id @$s4main3FooV3barSivg : $@convention(method) (Foo) -> Int,
    getter @$s4main3FooV3barSivpACTK : $@convention(thin) (@in_guaranteed Foo) -> @out Int,
    setter @$s4main3FooV3barSivpACTk : $@convention(thin) (@in_guaranteed Int, @inout Foo) -> ()
)

GettableProperty + GettableProperty

struct Foo {
    struct Bar { var baz: Int { 42 } }
    var bar: Bar { Bar() }
}

// `\Foo.bar.baz` =>
%2 = keypath $KeyPath<Foo, Int>, (
  root $Foo;
  gettable_property $Foo.Bar,
    id @$s4main3FooV3barAC3BarVvg : $@convention(method) (Foo) -> Foo.Bar,
    getter @$s4main3FooV3barAC3BarVvpACTK : $@convention(thin) (@in_guaranteed Foo) -> @out Foo.Bar;
  gettable_property $Int,
    id @$s4main3FooV3BarV3bazSivg : $@convention(method) (Foo.Bar) -> Int,
    getter @$s4main3FooV3BarV3bazSivpAETK : $@convention(thin) (@in_guaranteed Foo.Bar) -> @out Int
)

OptionalChain + OptionalWrap

struct Foo {
    struct Bar { var baz: Int { 42 } }
    var bar: Bar? { Bar() }
}

// `\Foo.bar.baz` =>
%2 = keypath $KeyPath<Foo, Optional<Int>>, (
  root $Foo;
  gettable_property $Optional<Foo.Bar>,
    id @$s4main3FooV3barAC3BarVSgvg : $@convention(method) (Foo) -> Optional<Foo.Bar>,
    getter @$s4main3FooV3barAC3BarVSgvpACTK : $@convention(thin) (@in_guaranteed Foo) -> @out Optional<Foo.Bar>;
  optional_chain : $Foo.Bar;
  gettable_property $Int,
    id @$s4main3FooV3BarV3bazSivg : $@convention(method) (Foo.Bar) -> Int,
    getter @$s4main3FooV3BarV3bazSivpAETK : $@convention(thin) (@in_guaranteed Foo.Bar) -> @out Int;
  optional_wrap : $Optional<Int>
)

LLVM IR上のKeyPath表現


ここから先、このコードを対象に話を進める。

struct Foo {
    subscript(x: Int) -> Int { 0xbadbabe }
}

let foo = Foo()

@_optimize(none)
func keyPathGetter() { _ = foo[keyPath: \Foo.[0xdeadbeef]] }

LLVM IR上の表現

  • KeyPath Object (Not ABI)
    • KeyPathインスタンスが保持する情報
    • いつもユーザが取り回してるのはこれ
  • KeyPath Pattern (ABI)
    • Propertyの特性とKeyPathとしての値の特性を表現
    • ランタイムがPatternからKeyPath Objectを生成する

KeyPath Pattern -> KeyPath Object (インスタンス化)

インスタンス化には引数を取る場合がある

  • 引数: Indices、Generic Params
  • 引数が必要ない場合はインスタンスがキャッシュされる
func withIndices(_ x: Int, y: String) -> AnyKeyPath {
  return \Foo.[x, y]
}

func withGenericContext<X: Hashable>(_ x: X.Type) -> AnyKeyPath {
  return \Blah.[x]
}

KeyPath Pattern in LLVM IR (Gettable Subscript)

\Foo.[0xdeadbeef]

@keypath = private global <{ i32, i32, i32, i32, i32, i32, i32, i32, i32, i32, i32, i32 }> <{
; oncePtr (0 if not cacheable, otherwise a relative pointer to the cache space)
i32 0,
; --- KeyPathComponentHeader ---
  ; genericEnvironment
  i32 0,
  ; rootMetadataRef
  i32 cast_relptr (i64 symbolic_ref @$s4main3FooVMn), ; nominal type descriptor for main.Foo
  ; leafMetadataRef
  i32 cast_relptr (i64 symbolic_ref @$sSi),           ; mangled name for Swift.Int ("Si\0")
  ; kvcString
  i32 0,
  ; KeyPathBuffer.Header
  i32 0b00000000000000000000000000011000, ; (size: 24, hasReferencePrefix: false, trivial: false)
  ; --- RawKeyPathComponent ---
    ; RawKeyPathComponent.Header
    i32 0b00000010000010000000000000000000, ; (hasComputedArguments: true, discriminator: computedTag(2))
    ; RawKeyPathComponent.idValue
    i32 cast_relptr (i64 (i64)* @"$s4main3FooVyS2icig" to i64),     ; main.Foo.subscript.getter : (Swift.Int) -> Swift.Int
    ; RawKeyPathComponent.getter
    i32 cast_relptr (i64 (i64)* @"$s4main3FooVyS2icipACTK" to i64), ; key path getter for main.Foo.subscript(Swift.Int) -> Swift.Int : main.Foo
    ; --- KeyPathPatternComputedArguments ---
      ; KeyPathPatternComputedArguments.getLayout
      i32 cast_relptr ({ i64, i64 } (i8*)* @keypath_get_arg_layout to i64),
      ; KeyPathPatternComputedArguments.witnesses
      i32 cast_relptr ({ i8*, void (i8*, i8*, i64)*, i1 (i8*, i8*)*, i64 (i8*)* }* @keypath_witnesses to i64),
      ; KeyPathPatternComputedArguments.initializer
      i32 cast_relptr (void (i8*, i8*)* @keypath_arg_init to i64),
}>, section ".rodata", align 8

^

  • non-generic

KeyPath生成 in LLVM IR

  ; 1. Argument Bufferの組み立て
  %1 = alloca i8, i64 8, align 16
  %2 = getelementptr inbounds i8, i8* %1, i64 0
  %3 = bitcast i8* %2 to %TSi*
  %._value = getelementptr inbounds %TSi, %TSi* %3, i32 0, i32 0
  store i64 0xdeadbeef, i64* %._value, align 8
  ; 2. KeyPath Objectの生成
  %4 = call %swift.refcounted* @swift_getKeyPath(i8* @keypath_pattern, i8* %1)

KeyPathのランタイム実装


swift_getKeyPath

KeyPath patternをargumentsからKeyPath objectを生成するランタイム関数

@_cdecl("swift_getKeyPathImpl")
public func _swift_getKeyPath(pattern: UnsafeMutableRawPointer,
                              arguments: UnsafeRawPointer)
    -> UnsafeRawPointer

swift_getKeyPath

  1. oncePtrのキャッシュチェック(スレッドセーフ)
  2. KeyPathインスタンスの生成
  3. PatternからKeyPathサブクラスとオブジェクトサイズを計算
  4. Patternとargumentsからオブジェクトを組み立て
  5. キャッシュに保存

1. oncePtrのキャッシュチェック

  let oncePtrPtr = pattern
  let oncePtrOffset = oncePtrPtr.load(as: Int32.self)
  let oncePtr: UnsafeRawPointer?
  if oncePtrOffset != 0 {
    let theOncePtr = _resolveRelativeAddress(oncePtrPtr, oncePtrOffset)
    oncePtr = theOncePtr

    // See whether we already instantiated this key path.
    // This is a non-atomic load because the instantiated pointer will be
    // written with a release barrier, and loads of the instantiated key path
    // ought to carry a dependency through this loaded pointer.
    let existingInstance = theOncePtr.load(as: UnsafeRawPointer?.self)
    
    if let existingInstance = existingInstance {
      // Return the instantiated object at +1.
      let object = Unmanaged<AnyKeyPath>.fromOpaque(existingInstance)
      // TODO: This retain will be unnecessary once we support global objects
      // with inert refcounting.
      _ = object.retain()
      return existingInstance
    }
  } else {
    oncePtr = nil
  }

2. KeyPathインスタンスの生成

  // Instantiate a new key path object modeled on the pattern.
  // Do a pass to determine the class of the key path we'll be instantiating
  // and how much space we'll need for it.
  let (keyPathClass, rootType, size, _)
    = _getKeyPathClassAndInstanceSizeFromPattern(patternPtr, arguments)

  // Allocate the instance.
  let instance = keyPathClass._create(capacityInBytes: size) { instanceData in
    // Instantiate the pattern into the instance.
    _instantiateKeyPathBuffer(patternPtr, instanceData, rootType, arguments)
  }

2. KeyPathインスタンスの生成

Tail-allocされたバッファはKeyPathBufferとして使われる

public class AnyKeyPath: Hashable, _AppendKeyPath {
  ...
  internal static func _create(
    capacityInBytes bytes: Int,
    initializedBy body: (UnsafeMutableRawBufferPointer) -> Void
  ) -> Self {
    _internalInvariant(bytes > 0 && bytes % 4 == 0,
                 "capacity must be multiple of 4 bytes")
    let result = Builtin.allocWithTailElems_1(self, (bytes/4)._builtinWordValue,
                                              Int32.self)
    result._kvcKeyPathStringPtr = nil
    let base = UnsafeMutableRawPointer(Builtin.projectTailElems(result,
                                                                Int32.self))
    body(UnsafeMutableRawBufferPointer(start: base, count: bytes))
    return result
  }
  ...
}

^ Tail-allocation


3. キャッシュに保存

[.column]

  • CAS (Compared-and-Swap)でアトミックにキャッシュ領域に書き込み
  • もし他のスレッドがキャッシュ書き込みに成功したらそれを採用して、作ったオブジェクトを廃棄

[.column]

// Try to replace a null pointer in the cache variable with the instance
// pointer.
let instancePtr = Unmanaged.passRetained(instance)

while true {
  let (oldValue, won) = Builtin.cmpxchg_seqcst_seqcst_Word(
    oncePtr._rawValue,
    0._builtinWordValue,
    UInt(bitPattern: instancePtr.toOpaque())._builtinWordValue)

  // If the exchange succeeds, then the instance we formed is the canonical
  // one.
  if Bool(won) {
    break
  }

  // Otherwise, someone raced with us to instantiate the key path pattern
  // and won. Their instance should be just as good as ours, so we can take
  // that one and let ours get deallocated.
  if let existingInstance = UnsafeRawPointer(bitPattern: Int(oldValue)) {
    // Return the instantiated object at +1.
    let object = Unmanaged<AnyKeyPath>.fromOpaque(existingInstance)
    // TODO: This retain will be unnecessary once we support global objects
    // with inert refcounting.
    _ = object.retain()
    // Release the instance we created.
    instancePtr.release()
    return existingInstance
  } else {
    // Try the cmpxchg again if it spuriously failed.
    continue
  }
}

swift_getAtKeyPath

KeyPath objectとRootの値からValueを取り出すランタイム関数

@_silgen_name("swift_getAtKeyPath")
public // COMPILER_INTRINSIC
func _getAtKeyPath<Root, Value>(
  root: Root,
  keyPath: KeyPath<Root, Value>
) -> Value {
  return keyPath._projectReadOnly(from: root)
}

[.column]

  • コンポーネントごとに具体型 を取り出して投影(project)していく
  • ジェネリックタイプなのでOpaque Pointer

[.column]

@usableFromInline
internal final func _projectReadOnly(from root: Root) -> Value {
  // TODO: For perf, we could use a local growable buffer instead of Any
  var curBase: Any = root
  return withBuffer {
    var buffer = $0
    if buffer.data.isEmpty {
      return unsafeBitCast(root, to: Value.self)
    }
    while true {
      let (rawComponent, optNextType) = buffer.next()
      let valueType = optNextType ?? Value.self
      let isLast = optNextType == nil
      
      func project<CurValue>(_ base: CurValue) -> Value? {
        func project2<NewValue>(_: NewValue.Type) -> Value? {
          switch rawComponent._projectReadOnly(base,
            to: NewValue.self, endingWith: Value.self) {
          case .continue(let newBase):
            if isLast {
              _internalInvariant(NewValue.self == Value.self,
                           "key path does not terminate in correct type")
              return unsafeBitCast(newBase, to: Value.self)
            } else {
              curBase = newBase
              return nil
            }
          case .break(let result):
            return result
          }
        }

        return _openExistential(valueType, do: project2)
      }

      if let result = _openExistential(curBase, do: project) {
        return result
      }
    }
  }
}

internal struct RawKeyPathComponent {
  ...
  internal func _projectReadOnly<CurValue, NewValue, LeafValue>(
    _ base: CurValue,
    to: NewValue.Type,
    endingWith: LeafValue.Type
  ) -> ProjectionResult<NewValue, LeafValue> {
    switch value {
    ...
    case .get(id: _, accessors: let accessors, argument: let argument),
         .mutatingGetSet(id: _, accessors: let accessors, argument: let argument),
         .nonmutatingGetSet(id: _, accessors: let accessors, argument: let argument):
      return .continue(accessors.getter()(
        base,
        argument?.data.baseAddress ?? accessors._value,
        argument?.data.count ?? 0)
      )
    ...
  }
  ...
}

getterを呼んだり、storedPropertyを取り出したりする


getter呼び出し

internal typealias Getter<CurValue, NewValue> = @convention(thin)
  (CurValue, UnsafeRawPointer, Int) -> NewValue

accessors.getter()(
  base,
  argument?.data.baseAddress ?? accessors._value,
  argument?.data.count ?? 0
)

呼び出される関数

// key path getter for Foo.subscript(_:) : Foo
sil shared [thunk] @$s4main3FooVyS2icipACTK : $@convention(thin) (@in_guaranteed Foo, UnsafeRawPointer) -> @out Int

余談: getter定義にバファサイズのパラメータが足りてない

[.column]

  • 足りてない
  • サイズやArgument Bufferが必要ないケースでは省略されてしまう
  • シグネチャ不一致はWasmでは エラー
  • 常に3パラメータ吐くようにパッチ当ててる

[.column]

定義

sil shared [thunk] @$s4main3FooVyS2icipACTK : $@convention(thin) (
  @in_guaranteed Foo, // base
  UnsafeRawPointer    // KeyPath Argument Buffer
) -> @out Int

呼び出し側

let ret = accessors.getter()(
  base,
  argument?.data.baseAddress ?? accessors._value,
  argument?.data.count ?? 0
)

KeyPath Argument Bufferのレイアウト

例1. subscript(x: Int) -> Int

Offset Type
0 Int

KeyPath Argument Bufferのレイアウト

例2. subscript<T = Int>(x: T, y: Int) -> Int

Offset Type
0 T = Int
8 Int

KeyPath Argument Bufferのレイアウト

例3. subscript<T>(x: T, y: Int) -> Int

Offset Type
0 T
sizeof(T) Int
sizeof(T) + 8 %swift.type* T
sizeof(T) + 16 witness table for T: Hashable

二重サンク問題


二重サンク問題

key path getterがジェネリックパラメータを持つ場合、LLVM IRでは引数にメタタイプとWitness Tableが渡される。

// key path getter for Bar.subscript<A>(_:_:) : <A>BarA
sil [thunk] @$s4main3BarVySix_SitcluipSHRzlACxTK : $@convention(thin) <T where T : Hashable> (
  @in_guaranteed Bar,
  UnsafeRawPointer
) -> @out Int



define swiftcc void @"$s4main3BarVySix_SitcluipSHRzlACxTK"(
  %TSi* noalias nocapture sret(%TSi) %0,
  %T4main3BarV* noalias nocapture dereferenceable(24) %1,
  i8* %2,
  %swift.type* %T,
  i8** %T.Hashable
)

二重サンク問題

ランタイムは単一のシグネチャでkey path getterを呼び出したいので、 ジェネリックパラメータをArgument Bufferから取り出してkey path getterを呼び出すサンクを更に生成する。

define private swiftcc void @keypath_get(%TSi* noalias nocapture sret(%TSi) %0, %T4main3BarV* noalias nocapture %1, i8* %2, i64 %3) {
entry:
  %4 = sub i64 %3, 16
  %5 = getelementptr inbounds i8, i8* %2, i64 %4
  %6 = bitcast i8* %5 to %swift.type**
  %"\CF\84_0_0" = load %swift.type*, %swift.type** %6, align 8

  %7 = getelementptr inbounds %swift.type*, %swift.type** %6, i32 1
  %8 = bitcast %swift.type** %7 to i8***
  %"\CF\84_0_0.Hashable" = load i8**, i8*** %8, align 8

  ; key path getter for Bar.subscript<A>(_:_:) : <A>BarA
  call swiftcc void @"$s4main3BarVySix_SitcluipSHRzlACxTK"(
    %TSi* noalias nocapture sret(%TSi) %0,                  ; return value
    %T4main3BarV* noalias nocapture dereferenceable(24) %1, ; base
    i8* %2,                                                 ; KeyPath Argument Buffer
    %swift.type* %"\CF\84_0_0",                             ; generic parameter
    i8** %"\CF\84_0_0.Hashable"                             ; Hashable witness table
  )
  ret void
}

二重サンク問題

  • KeyPath getterはSILレベルで作られるやつと、IRGenで作られるサンクがある。
  • IRGenレベルのサンクは、キャプチャされたジェネリックパラメータがあるとき、 バッファからメタタイプを取り出してSILレベルで作られたgetterに フォワードする。
  • 呼び出し規約の実装がIRGenとSILGenに飛び散ってる 😱

KeyPath速くしたい


inline

[.footer: https://gist.github.com/kateinoigakukun/d540dfdabe1948258d0860c7c6f462ee]


KeyPath速くしたい (コンパイラでできること)

ランタイム関数の呼び出しを消す(SILCombiner in SIL Optimizer)

%kp = keypath $KeyPath<Foo, Int>, (
  root $Foo;
  gettable_property $Int,
    id @$s4main3FooV3barSivg : $@convention(method) (Foo) -> Int,
    getter @$s4main3FooV3barSivpACTK : $@convention(thin) (@in_guaranteed Foo) -> @out Int
)
%fn = function_ref @swift_getAtKeyPath
%ret = alloc_stack $Int
apply %fn<Foo, Int>(%ret, %base, %kp)



// function_ref key path getter for Foo.bar : Foo
%getter = function_ref @$s4main3FooV3barSivpACTK
%ret = alloc_stack $Int
apply %getter(%ret, %base) : $@convention(thin) (@in_guaranteed Foo) -> @out Int

SILCombiner の制約

  • Argument Bufferを引数に取る場合は最適化できない。

  • Argument BufferのメモリレイアウトはIRGenで組まれるので、 SIL Optimizerの段階で引数を組み立てられないため。

// key path getter for Bar.subscript<A>(_:_:) : <A>BarA
sil [thunk] @$s4main3BarVySix_SitcluipSHRzlACxTK : $@convention(thin) <T where T : Hashable> (
  @in_guaranteed Bar,
  UnsafeRawPointer
) -> @out Int

KeyPath速くしたい (ユーザコードでできること)

  • 速度が必要ならgetter/setterを使う
  • Argument Buffer尽きのKeyPathならできるだけキャッシュする

展望

  • Wasm用のハックは流石にそのままアップストリームできないので、 先にリファクタリングしたい。
  • リファクタリングの副次効果で、Argument Buffer付きのKeyPathアクセス最適化 できるようになるかも。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment