slidenumber: true autoscale: true
後から真の型を変更可能 リコンパイルせずに変更可能
どのように実現するのか?
真の型がわかるときは、その型で特殊化して取り扱う
どのように実現するのか?
- ORTのアイデンティティに対応するOpaque Type Descriptor(OTD)が作られる
- OTDからはランタイム関数を介してMetatypeとProtocol Witness Tableが取得できる
- もちろん、MetatypeからはValue Witness Tableが取得できる
- 呼び出し側でポインタをキャストする
protocol P { func print() }
extension Int : P {}
func f() -> some P { return Int(3) }
func callF() {
let p1 = f() // ORTを返す関数の呼び出し
let p2 = p1 // ORT型同士のコピー
p2.print() // プロトコルメソッドの呼び出し
}
// f()
sil hidden @$s1a1fQryF : $@convention(thin) ()
-> @out @_opaqueReturnTypeOf("$s1a1fQryF", 0) 🦸 {
// %0 // user: %1
bb0(%0 : $*@_opaqueReturnTypeOf("$s1a1fQryF", 0) 🦸):
%1 = unchecked_addr_cast %0 :
$*@_opaqueReturnTypeOf("$s1a1fQryF", 0) 🦸 to $*Int // user: %4
%2 = integer_literal $Builtin.Int64, 3 // user: %3
%3 = struct $Int (%2 : $Builtin.Int64) // user: %4
store %3 to %1 : $*Int // id: %4
%5 = tuple () // user: %6
return %5 : $() // id: %6
} // end sil function '$s1a1fQryF'
-
返り値の型の表現は
@_opaqueReturnTypeOf("$s1a1fQryF", 0)
型名に関数名が埋め込まれている -
@out
がついているので、ポインタによる間接返し -
bb0の引数の型を見てもポインタだとわかる
-
unchecked_addr_cast
でIntのポインタにキャスト -
store
でそのポインタに結果を書き込む
🦸
// lib/AST/ASTPrinter.cpp
Printer << "@_opaqueReturnTypeOf(";
OpaqueTypeDecl *decl = T->getDecl();
Printer.printEscapedStringLiteral(
decl->getOpaqueReturnTypeIdentifier().str());
Printer << ", " << T->getInterfaceType()
->castTo<GenericTypeParamType>()
->getIndex();
Printer << u8") \U0001F9B8";
printGenericArgs(T->getSubstitutions().getReplacementTypes());
謎
// callF()
sil hidden @$s1a5callFyyF : $@convention(thin) () -> () {
bb0:
%0 = alloc_stack $@_opaqueReturnTypeOf("$s1a1fQryF", 0) 🦸,
let, name "p1" // users: %10, %9, %4, %2
// function_ref f()
%1 = function_ref @$s1a1fQryF :
$@convention(thin) ()
-> @out @_opaqueReturnTypeOf("$s1a1fQryF", 0) 🦸 // user: %2
%2 = apply %1(%0) : $@convention(thin) ()
-> @out @_opaqueReturnTypeOf("$s1a1fQryF", 0) 🦸
%3 = alloc_stack $@_opaqueReturnTypeOf("$s1a1fQryF", 0) 🦸,
let, name "p2" // users: %8, %7, %6, %4
copy_addr %0 to [initialization] %3 :
$*@_opaqueReturnTypeOf("$s1a1fQryF", 0) 🦸 // id: %4
%5 = witness_method $@_opaqueReturnTypeOf("$s1a1fQryF", 0) 🦸, #P.print!1 :
<Self where Self : P> (Self) -> () -> () :
$@convention(witness_method: P) <τ_0_0 where τ_0_0 : P>
(@in_guaranteed τ_0_0) -> () // user: %6
%6 = apply %5<@_opaqueReturnTypeOf("$s1a1fQryF", 0) 🦸>(%3) :
$@convention(witness_method: P) <τ_0_0 where τ_0_0 : P>
(@in_guaranteed τ_0_0) -> ()
destroy_addr %3 : $*@_opaqueReturnTypeOf("$s1a1fQryF", 0) 🦸 // id: %7
dealloc_stack %3 : $*@_opaqueReturnTypeOf("$s1a1fQryF", 0) 🦸 // id: %8
destroy_addr %0 : $*@_opaqueReturnTypeOf("$s1a1fQryF", 0) 🦸 // id: %9
dealloc_stack %0 : $*@_opaqueReturnTypeOf("$s1a1fQryF", 0) 🦸 // id: %10
%11 = tuple () // user: %12
return %11 : $() // id: %12
} // end sil function '$s1a5callFyyF'
-
$@_opaqueReturnTypeOf("$s1a1fQryF", 0)
型 でalloc_stack
して、 結果を受け取る変数をスタックに確保 そのアドレスを%0
に代入 -
function_ref
,apply
でf
を呼び出す 引数に確保したメモリ領域を渡す -
呼び出し後は
%0
に結果が入っている
-
コピー先変数を同様に作成、
%3
でアドレスを保持 -
copy_addr
で%0
から%3
にコピー
-
witness_method
で、$@_opaqueReturnTypeOf("$s1a1fQryF", 0)
型 のP.print
メソッドを取得 -
apply
で呼び出す
destroy_addr
で%3
の中身を破棄dealloc_stack
で%3
のメモリを解放destroy_addr
で%0
の中身を破棄dealloc_stack
で%0
のメモリを解放
-
関数名を埋め込んだ型名、
$@_opaqueReturnTypeOf("$s1a1fQryF", 0)
で表現 -
calleeは真の型をキャストして書き込み
-
callerは型名以外普通
define hidden swiftcc void @"$s1a1fQryF"
(%swift.opaque* noalias nocapture sret) #0 {
entry:
%1 = bitcast %swift.opaque* %0 to %TSi*
%._value = getelementptr inbounds %TSi, %TSi* %1, i32 0, i32 0
store i64 3, i64* %._value, align 1
ret void
}
- 型名は消えている
- 引数でopaqueポインタを受け取る
- それを
Int
のポインタにキャスト store
で書き込み
define hidden swiftcc void @"$s1a5callFyyF"() #0 {
entry:
%p2.debug = alloca i8*, align 8
%0 = bitcast i8** %p2.debug to i8*
call void @llvm.memset.p0i8.i64(i8* align 8 %0, i8 0, i64 8, i1 false)
%p1.debug = alloca i8*, align 8
%1 = bitcast i8** %p1.debug to i8*
call void @llvm.memset.p0i8.i64(i8* align 8 %1, i8 0, i64 8, i1 false)
%2 = call swiftcc %swift.metadata_response
@swift_getOpaqueTypeMetadata(
i64 0, i8* undef,
%swift.type_descriptor* bitcast (
<{ i32, i32, i16, i16, i16, i16, i8, i8, i8, i8, i32, i32, i32, i32, i32 }>*
@"$s1a1fQryFQOMQ"
to %swift.type_descriptor*
), i64 0
) #5
%3 = extractvalue %swift.metadata_response %2, 0
%4 = bitcast %swift.type* %3 to i8***
%5 = getelementptr inbounds i8**, i8*** %4, i64 -1
%.valueWitnesses = load i8**, i8*** %5, align 8, !invariant.load !42, !dereferenceable !43
%6 = bitcast i8** %.valueWitnesses to %swift.vwtable*
%7 = getelementptr inbounds %swift.vwtable, %swift.vwtable* %6, i32 0, i32 8
%size = load i64, i64* %7, align 8, !invariant.load !42
%p2 = alloca i8, i64 %size, align 16
call void @llvm.lifetime.start.p0i8(i64 -1, i8* %p2)
%8 = bitcast i8* %p2 to %swift.opaque*
store i8* %p2, i8** %p2.debug, align 8
%p1 = alloca i8, i64 %size, align 16
call void @llvm.lifetime.start.p0i8(i64 -1, i8* %p1)
%9 = bitcast i8* %p1 to %swift.opaque*
store i8* %p1, i8** %p1.debug, align 8
call swiftcc void @"$s1a1fQryF"(%swift.opaque* noalias nocapture sret %9)
%10 = getelementptr inbounds i8*, i8** %.valueWitnesses, i32 2
%11 = load i8*, i8** %10, align 8, !invariant.load !42
%initializeWithCopy = bitcast i8* %11 to %swift.opaque* (%swift.opaque*, %swift.opaque*, %swift.type*)*
%12 = call %swift.opaque* %initializeWithCopy(%swift.opaque* noalias %8, %swift.opaque* noalias %9, %swift.type* %3) #3
%13 = call swiftcc i8**
@swift_getOpaqueTypeConformance(
i8* undef,
%swift.type_descriptor* bitcast (
<{ i32, i32, i16, i16, i16, i16, i8, i8, i8, i8, i32, i32, i32, i32, i32 }>*
@"$s1a1fQryFQOMQ"
to %swift.type_descriptor*
), i64 1
) #5
%14 = getelementptr inbounds i8*, i8** %13, i32 1
%15 = load i8*, i8** %14, align 8, !invariant.load !42
%16 = bitcast i8* %15 to void (%swift.opaque*, %swift.type*, i8**)*
call swiftcc void %16(%swift.opaque* noalias nocapture swiftself %8, %swift.type* %3, i8** %13)
%17 = getelementptr inbounds i8*, i8** %.valueWitnesses, i32 1
%18 = load i8*, i8** %17, align 8, !invariant.load !42
%destroy = bitcast i8* %18 to void (%swift.opaque*, %swift.type*)*
call void %destroy(%swift.opaque* noalias %8, %swift.type* %3) #3
call void %destroy(%swift.opaque* noalias %9, %swift.type* %3) #3
%19 = bitcast %swift.opaque* %9 to i8*
call void @llvm.lifetime.end.p0i8(i64 -1, i8* %19)
%20 = bitcast %swift.opaque* %8 to i8*
call void @llvm.lifetime.end.p0i8(i64 -1, i8* %20)
ret void
}
-
swift_getOpaqueTypeMetadata
というランタイム関数に、$s1a1fQryFQOMQ
を渡している -
$s1a1fQryFQOMQ
はopaque type descriptor for <<opaque return type of a.f() -> some>>
-
これで
%3
にMetatypeが取り出される 実際には真の型であるIntのMetatypeが得られる
-
getelementptr
の-1
で%5
にValue Witness Tableを取り出す -
getelementptr
の8
で%7
に型のメモリサイズを取り出す -
このサイズを使って、
alloca
でメモリ確保して、%p1
と%p2
にアドレスを代入
-
bitcast
で%p1
をopaqueポインタにキャスト -
call
でf
を呼び出し、そのポインタを引数に渡す
-
さっき取り出したVWTから
getelementptr
の2
でinitializeWithCopy
を取り出す -
call
でそれを呼び出してコピーを実行
-
swift_getOpaqueTypeConformance
というランタイム関数に、 OTDと1
を渡している。 この1
はこの型が準拠するプロトコルリストからP
を指定する番号 -
これによって
%13
にProtocol Witness Tableが取り出せる 実際には真の型であるIntのPのPWT
-
getelementptr
の1
でPWTからメソッドを%14
に取り出す -
call
でプロトコルメソッドの呼び出し
-
getelementptr
の1
でVWTからdestroy
を取り出す -
call
でそれを呼び出してp1
とp2
を破棄 -
メモリ解放は
alloca
なのでコード無し
-
Opaque Type Descriptorが
f
のコンパイル時に一緒に生成されているので、f
の返す真の型が変更されたときには、OTDの中身も一緒に変更される。 -
caller側では、OTDをランタイム関数に渡して、Metatype, VWT, PWTなどを取得して動作するので、OTDの中身が変わっても互換性を維持できる。
最適化でf
の呼び出しが消去されないように、
f
に@inline(never)
を付ける。
sil hidden [noinline] @$s1b1fQryF : $@convention(thin) ()
-> @out @_opaqueReturnTypeOf("$s1b1fQryF", 0) 🦸 {
// %0 // user: %1
bb0(%0 : $*@_opaqueReturnTypeOf("$s1b1fQryF", 0) 🦸):
%1 = unchecked_addr_cast %0 :
$*@_opaqueReturnTypeOf("$s1b1fQryF", 0) 🦸 to $*Int // user: %4
%2 = integer_literal $Builtin.Int64, 3 // user: %3
%3 = struct $Int (%2 : $Builtin.Int64) // user: %4
store %3 to %1 : $*Int // id: %4
%5 = tuple () // user: %6
return %5 : $() // id: %6
} // end sil function '$s1b1fQryF'
- 最適化前と変化無し!
// callF()
sil hidden @$s1b5callFyyF : $@convention(thin) () -> () {
bb0:
%0 = alloc_stack $Int // users: %2, %7, %4
// function_ref f()
%1 = function_ref @$s1b1fQryF :
$@convention(thin) ()
-> @out @_opaqueReturnTypeOf("$s1b1fQryF", 0) 🦸 // user: %3
%2 = unchecked_addr_cast %0 :
$*Int to $*@_opaqueReturnTypeOf("$s1b1fQryF", 0) 🦸 // user: %3
%3 = apply %1(%2) :
$@convention(thin) ()
-> @out @_opaqueReturnTypeOf("$s1b1fQryF", 0) 🦸
%4 = load %0 : $*Int // user: %6
// function_ref Int.print()
%5 = function_ref @$sSi1bE5printyyF :
$@convention(method) (Int) -> () // user: %6
%6 = apply %5(%4) : $@convention(method) (Int) -> ()
dealloc_stack %0 : $*Int // id: %7
%8 = tuple () // user: %9
return %8 : $() // id: %9
} // end sil function '$s1b5callFyyF'
-
真の型
Int
でalloc_stack
して、アドレスを%0
に代入 -
それを
@_opaqueReturnTypeOf("$s1b1fQryF", 0)
型のポインタにキャストして、f
の呼び出しに渡している -
Int
のprint
メソッド$sSi1bE5printyyF
を直接呼び出し -
dealloc_stack
でメモリ解放
-
ORT型での操作がなくなり、
Int
を直接扱っている -
f
を呼び出す時に、Int
へのポインタをORT型へのポインタにキャストしただけ
; Function Attrs: noinline norecurse nounwind writeonly
define hidden swiftcc void @"$s1b1fQryF"
(%swift.opaque* noalias nocapture sret) local_unnamed_addr #5 {
entry:
%._value = bitcast %swift.opaque* %0 to i64*
store i64 3, i64* %._value, align 1
ret void
}
- ほぼ変化無し!
define hidden swiftcc void @"$s1b5callFyyF"() local_unnamed_addr #1 {
entry:
%0 = alloca %TSi, align 8
%1 = bitcast %TSi* %0 to i8*
call void @llvm.lifetime.start.p0i8(i64 8, i8* nonnull %1)
%2 = bitcast %TSi* %0 to %swift.opaque*
call swiftcc void @"$s1b1fQryF"(%swift.opaque* noalias nocapture nonnull sret %2)
%._value = getelementptr inbounds %TSi, %TSi* %0, i64 0, i32 0
%3 = load i64, i64* %._value, align 8
tail call swiftcc void @"$sSi1bE5printyyF"(i64 %3)
call void @llvm.lifetime.end.p0i8(i64 8, i8* nonnull %1)
ret void
}
alloca
でInt
型の変数を確保- そのアドレスを
%swift.opaque*
にキャスト - それを引数に渡して
f
を呼び出す Int
のprint
メソッド$sSi1bE5printyyF
を呼び出し
- 返り値がopaque pointerでの間接返しになっている事以外は、普通に
Int
を扱うコードと同じ。 - 最適化により、OTDとランタイム関数の使用が消去されている。
- 抽象化によるパフォーマンスのロスが無い事がわかる。