autoscale: true slidenumber: true
[Pitch] Modify Accessors 1
get
/set
は遅い
struct GetSet {
var storage: String = ""
var property: String {
get {
return storage
}
set {
storage = newValue
}
}
}
a.property.append
という式は、下記の手順にコンパイルされる。
var temp = a.property
temp.append(text)
a.property = temp
- 雑に言えば、無駄なコピーが生じているので遅い。
-
正確には、コピー自体はCoWが効くのでコストはかからない。
-
しかし、
get
して作ったテンポラリコピーのString
の本体は、a.storage
からも参照されているためユニークな参照にならない。 -
ユニークな参照ではないので、
append
のタイミングで全文コピーが生じる。 -
CoWは万能ではなく、コピーのペナルティが顕現する事がある。
func main() {
var a = GetSet()
a.property.append("str")
}
$ swiftc -emit-sil c2.swift
%12 = alloc_stack $String // users: %18, %15, %22, %17
// function_ref GetSet.property.getter
%13 = function_ref @$s2c26GetSetV8propertySSvg :
$@convention(method) (@guaranteed GetSet) -> @owned String // user: %14
%14 = apply %13(%3) : $@convention(method) (@guaranteed GetSet) -> @owned String // user: %15
store %14 to %12 : $*String // id: %15
// function_ref String.append(_:)
%16 = function_ref @$sSS6appendyySSF :
$@convention(method) (@guaranteed String, @inout String) -> () // user: %17
%17 = apply %16(%10, %12) : $@convention(method) (@guaranteed String, @inout String) -> ()
%18 = load %12 : $*String // user: %20
// function_ref GetSet.property.setter
%19 = function_ref @$s2c26GetSetV8propertySSvs :
$@convention(method) (@owned String, @inout GetSet) -> () // user: %20
%20 = apply %19(%18, %11) : $@convention(method) (@owned String, @inout GetSet) -> ()
func main() {
var a = GetSet()
a.storage.append("str")
}
%12 = struct_element_addr %11 : $*GetSet, #GetSet.storage // user: %14
// function_ref String.append(_:)
%13 = function_ref @$sSS6appendyySSF :
$@convention(method) (@guaranteed String, @inout String) -> () // user: %14
%14 = apply %13(%10, %12) : $@convention(method) (@guaranteed String, @inout String) -> ()
- modifyアクセサ
struct GetModify {
var storage: String = ""
var property: String {
get { return storage }
modify {
yield &storage
}
}
}
-
modifyアクセサは
set
と異なり、関数ではなく、コルーチン -
コルーチンの本文では
yield
文が使える
2016 LLVM Developers’ Meeting: G. Nishanov “LLVM Coroutines” 2
- llvmのcoroの紹介
-
yield
ではプロパティの型のinout参照を返す (関数のreturn
ではできない事) -
参照を返す事でコピーを回避して高速化する
- テンポラリコピーで
get
/set
する代わりに、modifyアクセサを実行、返ってきた参照をそのまま利用し、modifyアクセサを再開する。
// 動作イメージ
let coro = a.property
inout temp = coro.resume()
temp.append("str")
coro.resume()
struct GetModify {
var storage: String = ""
var property: String {
get { return storage }
modify {
print("before yield")
yield &storage
print("after yield")
}
}
}
var a = GetModify()
print("before append")
a.property.append("str")
print("after append")
/*
before append
before yield
after yield
after append
*/
- modifyコルーチンは取得した参照を使用している間は中断していて、使用が終わったら再開してから終了される
struct GetModify {
var num: Int?
var property: String {
get { return "" }
modify {
var str = num?.description ?? ""
yield &str
self.num = Int(str)
}
}
}
var a = GetModify()
a.property.append("1")
print(a.num ?? 0) // => 1
a.property.append("2")
print(a.num ?? 0) // => 12
a.property.append("a")
print(a.num ?? 0) // => 0
- コルーチンが中断していても、コルーチンが終了するまではコンテキスト(スコープ)が生きているので、ローカル変数への参照を返却できる
- Pitchに載っている例
extension Array: MutableGenerator {
func generateMutably() {
for i in 0..<count {
yield &self[i]
}
}
}
for &x in myArray {
x += 1
}
- OwnershipManifesto3に載っている例
mutating generator iterateMutable() -> inout Element {
var i = startIndex, e = endIndex
while i != e {
yield &self[i]
self.formIndex(after: &i)
}
}
for inout employee in company.employees {
employee.respected = true
}
Asnyc/Await
プロポーザルドラフト4がコルーチンの導入を提案している
func loadWebResource(_ path: String) async -> Resource
func decodeImage(_ r1: Resource, _ r2: Resource) async -> Image
func dewarpAndCleanupImage(_ i : Image) async -> Image
func processImageData1() async -> Image {
let dataResource = await loadWebResource("dataprofile.txt")
let imageResource = await loadWebResource("imagedata.dat")
let imageTmp = await decodeImage(dataResource, imageResource)
let imageResult = await dewarpAndCleanupImage(imageTmp)
return imageResult
}
- コルーチンの実行中に呼び出し側で例外が発生した場合、コルーチンが中止され、
yield
より後続の処理に戻らない
func storeText(to str: inout String) throws { ... }
var a = GetModify()
try storeText(to: &a.property)
yield
より前に書いたdefer
は中止した場合でも実行される
extension Array {
var first: Element? {
get { isEmpty ? nil : self[0] }
modify {
var tmp: Optional<Element> = nil
if isEmpty {
tmp = nil
yield &tmp
if let newValue = tmp {
self.append(newValue)
}
} else {
// put array into temporarily invalid state
tmp = _storage.move()
// code to restore valid state put in defer
defer {
if let newValue = tmp {
_storage.initialize(to: newValue)
} else {
_storage.moveInitialize(from: _storage + 1, count: self.count - 1)
self.count -= 1
}
}
// yield that moved value to the caller
yield &tmp
}
}
}
-
SILにもコルーチンが実装される
-
LLVM-IRにおいて、コルーチンを実現する通常のコードにLowerする
-
llvm.coro系命令
-
Switched-Resume Lowering: 最初にC++向けに作られた実装
-
Returned-Continuation Lowering: Swift向けに作られた実装
-
2018 LLVM Developers’ Meeting: J. McCall “Coroutine Representations and ABIs in LLVM”5
-
yield
ごとに関数を分けて、「次の関数」を返り値と一緒に返す -
コミット6
- Coroutines in LLVM7
-
modify
は_modify
キーワードで実装済み -
標準ライブラリで既に使用している
- Array.swift8
extension Array: RandomAccessCollection, MutableCollection {
@inlinable
public subscript(index: Int) -> Element {
get {
// This call may be hoisted or eliminated by the optimizer. If
// there is an inout violation, this value may be stale so needs to be
// checked again below.
let wasNativeTypeChecked = _hoistableIsNativeTypeChecked()
// Make sure the index is in range and wasNativeTypeChecked is
// still valid.
let token = _checkSubscript(
index, wasNativeTypeChecked: wasNativeTypeChecked)
return _getElement(
index, wasNativeTypeChecked: wasNativeTypeChecked,
matchingSubscriptCheck: token)
}
_modify {
_makeMutableAndUnique() // makes the array native, too
_checkSubscript_native(index)
let address = _buffer.subscriptBaseAddress + index
yield &address.pointee
}
}
}
struct GetModify {
var storage: String = ""
var property: String {
get {
return storage
}
_modify {
yield &storage
}
}
}
func main() {
var a = GetModify()
a.property.append("str")
}
// GetModify.property.modify
sil hidden @$s2c59GetModifyV8propertySSvM :
$@yield_once @convention(method) (@inout GetModify) -> @yields @inout String {
// %0 // users: %2, %1
bb0(%0 : $*GetModify):
debug_value_addr %0 : $*GetModify, var, name "self", argno 1 // id: %1
%2 = begin_access [modify] [static] %0 : $*GetModify // users: %5, %8, %3
%3 = struct_element_addr %2 : $*GetModify, #GetModify.storage // user: %4
yield %3 : $*String, resume bb1, unwind bb2 // id: %4
bb1: // Preds: bb0
end_access %2 : $*GetModify // id: %5
%6 = tuple () // user: %7
return %6 : $() // id: %7
bb2: // Preds: bb0
end_access %2 : $*GetModify // id: %8
unwind // id: %9
} // end sil function '$s2c59GetModifyV8propertySSvM'
%12 = function_ref @$s2c59GetModifyV8propertySSvM :
$@yield_once @convention(method) (@inout GetModify)
-> @yields @inout String // user: %13
(%13, %14) = begin_apply %12(%11) :
$@yield_once @convention(method) (@inout GetModify) -> @yields @inout String // users: %16, %17
// function_ref String.append(_:)
%15 = function_ref @$sSS6appendyySSF :
$@convention(method) (@guaranteed String, @inout String) -> () // user: %16
%16 = apply %15(%10, %13) : $@convention(method) (@guaranteed String, @inout String) -> ()
end_apply %14 // id: %17
func main() {
var a = GetModify()
try! storeText(to: &a.property)
}
// function_ref GetModify.property.modify
%6 = function_ref @$s2c59GetModifyV8propertySSvM :
$@yield_once @convention(method) (@inout GetModify)
-> @yields @inout String // user: %7
(%7, %8) = begin_apply %6(%5) :
$@yield_once @convention(method) (@inout GetModify) -> @yields @inout String // users: %10, %12, %19
// function_ref storeText(to:)
%9 = function_ref @$s2c59storeText2toySSz_tKF :
$@convention(thin) (@inout String) -> @error Error // user: %10
try_apply %9(%7) : $@convention(thin) (@inout String) -> @error Error, normal bb1, error bb2 // id: %10
bb1(%11 : $()): // Preds: bb0
end_apply %8 // id: %12
...
// %18 // user: %27
bb2(%18 : $Error): // Preds: bb0
abort_apply %8 // id: %19
-
謎のオプションを与えるとコルーチンをLowerしていないLLVMが出る
-
$ swiftc -emit-ir -Xfrontend -disable-swift-specific-llvm-optzns c5.swift
-
これをやるとLLVM-IRは出せるがバイナリは生成できない(必須変換)
; Function Attrs: noinline
define hidden swiftcc { i8*, %TSS* }
@"$s2c59GetModifyV8propertySSvM"(
i8* noalias dereferenceable(32),
%T2c59GetModifyV* nocapture swiftself dereferenceable(16)) #1 {
entry:
%2 = call token @llvm.coro.id.retcon.once(
i32 32, i32 8, i8* %0,
i8* bitcast (void (i8*, i1)* @"$s2c59GetModifyVIetMl_TC" to i8*),
i8* bitcast (i8* (i64)* @malloc to i8*),
i8* bitcast (void (i8*)* @free to i8*))
%3 = call i8* @llvm.coro.begin(token %2, i8* null)
%.storage = getelementptr inbounds %T2c59GetModifyV, %T2c59GetModifyV* %1, i32 0, i32 0
%4 = call i1 (...) @llvm.coro.suspend.retcon.i1(%TSS* %.storage)
br i1 %4, label %6, label %5
; <label>:5: ; preds = %entry
br label %coro.end
; <label>:6: ; preds = %entry
br label %coro.end
coro.end: ; preds = %5, %6
%7 = call i1 @llvm.coro.end(i8* %3, i1 false)
unreachable
}
llvm.coro
系命令が出てくる- シグネチャでは
self
に加えて、謎の32バイトのメモリを引数に受けている coro.suspend
の返り値としてi1
を使い、resume/abortを区別している
%2 = alloca [32 x i8], align 8
...
%10 = getelementptr inbounds [32 x i8], [32 x i8]* %2, i32 0, i32 0
call void @llvm.lifetime.start.p0i8(i64 32, i8* %10)
%11 = call i8* @llvm.coro.prepare.retcon(
i8* bitcast ({ i8*, %TSS* } (i8*, %T2c59GetModifyV*)* @"$s2c59GetModifyV8propertySSvM" to i8*))
%12 = bitcast i8* %11 to { i8*, %TSS* } (i8*, %T2c59GetModifyV*)*
%13 = call swiftcc { i8*, %TSS* } %12(
i8* noalias dereferenceable(32) %10,
%T2c59GetModifyV* nocapture swiftself dereferenceable(16) %0)
%14 = extractvalue { i8*, %TSS* } %13, 0
%15 = extractvalue { i8*, %TSS* } %13, 1
call swiftcc void @"$sSS6appendyySSF"(
i64 %8, %swift.bridge* %9, %TSS* nocapture swiftself dereferenceable(16) %15)
%16 = bitcast i8* %14 to void (i8*, i1)*
call swiftcc void %16(i8* noalias dereferenceable(32) %10, i1 false)
call void @llvm.lifetime.end.p0i8(i64 32, i8* %10)
coro.prepare.retcon
が関数ポインタ(%11
,%12
)を返す- それを呼び出すと、また関数ポインタ(
%13
,%14
)を返す - それを呼び出す(
%14
,%16
)ときにresumeを意味するfalse
を渡す
-
普通にやるとコルーチンがLowerされたものが出る。
-
$ swiftc -emit-ir c5.swift
%"$s2c59GetModifyV8propertySSvM.Frame" = type {}
; Function Attrs: noinline
define hidden swiftcc { i8*, %TSS* } @"$s2c59GetModifyV8propertySSvM"(
i8* noalias dereferenceable(32), %T2c59GetModifyV* nocapture swiftself dereferenceable(16)) #1 {
entry:
%.storage = getelementptr inbounds %T2c59GetModifyV, %T2c59GetModifyV* %1, i32 0, i32 0
%2 = insertvalue { i8*, %TSS* }
{ i8* bitcast (void (i8*, i1)* @"$s2c59GetModifyV8propertySSvM.resume.0" to i8*), %TSS* undef },
%TSS* %.storage, 1
ret { i8*, %TSS* } %2
}
define internal swiftcc void @"$s2c59GetModifyV8propertySSvM.resume.0"(
i8* noalias nonnull dereferenceable(32), i1) #0 {
entryresume.0:
%FramePtr = bitcast i8* %0 to %"$s2c59GetModifyV8propertySSvM.Frame"*
%vFrame = bitcast %"$s2c59GetModifyV8propertySSvM.Frame"* %FramePtr to i8*
ret void
}
- 関数
resume.0
が生成され、アクセサはこれとストレージポインタを返す resume.0
の実装から、32バイトのメモリはフレームメモリへのポインタだとわかるresume.0
の第2引数はresumeフラグ
%2 = alloca [32 x i8], align 8
...
%10 = getelementptr inbounds [32 x i8], [32 x i8]* %2, i32 0, i32 0
call void @llvm.lifetime.start.p0i8(i64 32, i8* %10)
%11 = call swiftcc { i8*, %TSS* } @"$s2c59GetModifyV8propertySSvM"(
i8* noalias dereferenceable(32) %10, %T2c59GetModifyV* nocapture swiftself dereferenceable(16) %0)
%12 = extractvalue { i8*, %TSS* } %11, 0
%13 = extractvalue { i8*, %TSS* } %11, 1
call swiftcc void @"$sSS6appendyySSF"(
i64 %8, %swift.bridge* %9, %TSS* nocapture swiftself dereferenceable(16) %13)
%14 = bitcast i8* %12 to void (i8*, i1)*
call swiftcc void %14(i8* noalias dereferenceable(32) %10, i1 false)
call void @llvm.lifetime.end.p0i8(i64 32, i8* %10)
- アクセサを呼ぶと関数ポインタとストレージポインタが返ってくる
- 関数ポインタにresumeフラグを渡して呼んで終わり
rendered: https://speakerdeck.com/omochi/swiftfalsemodifyakusesatokorutin