text: text-scale(0.75), line-height(0.9) code: text-scale(1.0), line-height(1.0) theme: Next, 4 slidenumbers: true
- 研究: サイズ最適化頑張ってる
- マイクロベンチでしか効かない最適化 😢
- Ruby: Venturaで動くように頑張った
- SwiftWasm: プロダクション事例出てきてうれしい
^ 前回は3月だったらしい。半年ぶり。
乱暴に言うと getter
と setter
を取り回す表現
色んな所で使われてる
subscript(dynamicMember: KeyPath<X, Y>) -> Z
func map<T>(_ keyPath: KeyPath<Self.Output, T>) -> Publishers.MapKeyPath<Self, T>
- Key Paths Expressions as Functions
意味は同じだが、動作は全く違う。
foo[\.bar]
foo.bar
[.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倍 おそい 🐢
使われて無さそうなマイナー機能
\[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)
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]])
}
}
let kp0 = \Foo.bar
// -> let kp0 = KeyPath(getter: { $0.bar })
let kp1 = \Foo.[42]
// -> let kp1 = KeyPath(getter: { $0[42] })
実際はもっと複雑
実行順。意味解析まで省略
File | Description |
---|---|
lib/SILGen/SILGenExpr.cpp |
KeyPathInst SIL命令とアクセサを生成 |
lib/SILOptimizer/SILCombiner/SILCombinerApplyVisitors.cpp |
getter/setterアクセスへ最適化 |
lib/IRGen/GenKeyPath.cpp |
ランタイムが読むためのKeyPathデータとサンクを生成 |
stdlib/public/core/KeyPath.swift
- コアのロジックすべてが記述されてる
- インスタンス化
- アクセス
- 比較など
- Swiftで書かれている
- SIL上のKeyPath表現
- LLVM IR上のKeyPath表現
- KeyPathのランタイム実装
- コンパイラ最適化
[.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'
keypath /* 得られるKeyPath型 */, (
root /* ルートの型 */;
/* パスの要素1 (KeyPathPatternComponent) */
/* パスの要素2 (KeyPathPatternComponent) */
/* パスの要素N (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
};
};
struct Foo {
let bar: Int
}
// `\Foo.bar` =>
%2 = keypath $KeyPath<Foo, Int>, (
root $Foo;
stored_property #Foo.bar : $Int
)
struct Foo {
var bar: Int
}
// `\Foo.bar` =>
%2 = keypath $WritableKeyPath<Foo, Int>, (
root $Foo;
stored_property #Foo.bar : $Int
)
typealias Foo = (
bar: Int,
baz: String
)
// `\Foo.bar` =>
%2 = keypath $WritableKeyPath<(bar: Int, baz: String), Int>, (
root $(bar: Int, String);
tuple_element #0 : $Int
)
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
)
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)、比較、ハッシュのための関数をキャプチャ
- ランタイムから呼ばれる用のサンク
- すべての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'
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) -> ()
)
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
)
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>
)
ここから先、このコードを対象に話を進める。
struct Foo {
subscript(x: Int) -> Int { 0xbadbabe }
}
let foo = Foo()
@_optimize(none)
func keyPathGetter() { _ = foo[keyPath: \Foo.[0xdeadbeef]] }
- KeyPath Object (Not ABI)
KeyPath
インスタンスが保持する情報- いつもユーザが取り回してるのはこれ
- KeyPath Pattern (ABI)
- Propertyの特性と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
; 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 patternをargumentsからKeyPath objectを生成するランタイム関数
@_cdecl("swift_getKeyPathImpl")
public func _swift_getKeyPath(pattern: UnsafeMutableRawPointer,
arguments: UnsafeRawPointer)
-> UnsafeRawPointer
oncePtr
のキャッシュチェック(スレッドセーフ)KeyPath
インスタンスの生成- PatternからKeyPathサブクラスとオブジェクトサイズを計算
- Patternと
arguments
からオブジェクトを組み立て - キャッシュに保存
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
}
// 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
}
}
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
を取り出したりする
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
[.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
)
例1. subscript(x: Int) -> Int
Offset | Type |
---|---|
0 | Int |
例2. subscript<T = Int>(x: T, y: Int) -> Int
Offset | Type |
---|---|
0 | T = Int |
8 | Int |
例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に飛び散ってる 😱
[.footer: https://gist.github.com/kateinoigakukun/d540dfdabe1948258d0860c7c6f462ee]
ランタイム関数の呼び出しを消す(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
-
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
- 速度が必要ならgetter/setterを使う
- Argument Buffer尽きのKeyPathならできるだけキャッシュする
- Wasm用のハックは流石にそのままアップストリームできないので、 先にリファクタリングしたい。
- リファクタリングの副次効果で、Argument Buffer付きのKeyPathアクセス最適化 できるようになるかも。