Skip to content

Instantly share code, notes, and snippets.

@kitasuke
Last active April 17, 2018 15:53
Show Gist options
  • Save kitasuke/d8b94398c4b3ca98f45ce6fa317afb06 to your computer and use it in GitHub Desktop.
Save kitasuke/d8b94398c4b3ca98f45ce6fa317afb06 to your computer and use it in GitHub Desktop.
var-let-sil-jp

varとletをSILで読んでみる

こんにちは。iOSエンジニアの@kitasukeです。

「SwiftをSILで読んでみる」第1回は「varとletをSILで読んでみる」を紹介します。 まだ理解不足なところもあるので、間違いがあればご指摘頂けると嬉しいです。

varとlet

varletは良く使用するのでご存知だとは思いますが、それぞれの性質を説明します。

var

Swiftのvarvariablesと言われる通り、変数です。値を変数に何回でも代入できます。

var x: Int
x = 1
x = 10

let

Swiftのletconstantsと言われる通り、定数です。値を一度代入して初期化するとそれ以降は値を変更できません。

let x: Int
x = 1
x = 10 // error

SIL

それでは、上記の性質がSILでどのように表現されているかを見てみましょう。

使用するメソッド

今回は単純にIntを返すメソッドを例として使用します。それぞれのメソッドで違う箇所は、xvarで宣言されているかletで宣言されているかのみです。

var.swift

func number() -> Int {
    var x: Int
    x = 1
    return x
}

let.swift

func number() -> Int {
    let x: Int
    x = 1
    return x
}

raw SIL

さて、swiftcコマンドでraw SILを出力してみましょう。出力された結果の全てが関連しているわけではないので、分からない箇所があっても特に問題ないです。

$swiftc -emit-silgen var.swift -o var.silgen

var.swift のraw SILは下記の通りです。関連箇所のみ抜粋しています。

var.silgen

%0 = alloc_box ${ var Int }, var, name "x"      
%1 = mark_uninitialized [var] %0 : ${ var Int }
%2 = project_box %1 : ${ var Int }, 0          
%3 = metatype $@thin Int.Type                  
%4 = integer_literal $Builtin.Int2048, 1        
// function_ref Int.init(_builtinIntegerLiteral:)
%5 = function_ref @$SSi22_builtinIntegerLiteralSiBi2048__tcfC :
    $@convention(method) (Builtin.Int2048, @thin Int.Type) -> Int
%6 = apply %5(%4, %3) : $@convention(method) (Builtin.Int2048, @thin Int.Type) -> Int
%7 = begin_access [modify] [unknown] %2 : $*Int
assign %6 to %7 : $*Int                     
end_access %7 : $*Int                       
%10 = begin_access [read] [unknown] %2 : $*Int
%11 = load [trivial] %10 : $*Int            
end_access %10 : $*Int                      
destroy_value %1 : ${ var Int }              
return %11 : $Int                            

こちらが let.swift のraw SILです。こちらも関連箇所のみ抜粋しています。

$swiftc -emit-silgen let.swift -o let.silgen

let.silgen

%0 = alloc_stack $Int, let, name "x"           
%1 = mark_uninitialized [var] %0 : $*Int       
%2 = metatype $@thin Int.Type                  
%3 = integer_literal $Builtin.Int2048, 1       
// function_ref Int.init(_builtinIntegerLiteral:)
%4 = function_ref @$SSi22_builtinIntegerLiteralSiBi2048__tcfC :
    $@convention(method) (Builtin.Int2048, @thin Int.Type) -> Int
%5 = apply %4(%3, %2) : $@convention(method) (Builtin.Int2048, @thin Int.Type) -> Int
assign %5 to %1 : $*Int                     
%7 = load [trivial] %1 : $*Int              
dealloc_stack %0 : $*Int                    
return %7 : $Int                            

ざっと見てみると少し違いがありますね。varの場合とletの場合で特徴となる部分を抜粋してみます。

var.silgen ではalloc_boxbegin_accessという命令が使用されています。

%0 = alloc_box ${ var Int }, var, name "x"      
%1 = mark_uninitialized [var] %0 : ${ var Int }
%2 = project_box %1 : ${ var Int }, 0          
%7 = begin_access [modify] [unknown] %2 : $*Int
end_access %7 : $*Int                       
%10 = begin_access [read] [unknown] %2 : $*Int
end_access %10 : $*Int                      
destroy_value %1 : ${ var Int }              

let.silgenではalloc_boxではなく、alloc_stackという命令が使用されています。

%0 = alloc_stack $Int, let, name "x"           
%1 = mark_uninitialized [var] %0 : $*Int      
dealloc_stack %0 : $*Int   

これらの命令がどういう意味なのか調べると、SILにおけるvarletの挙動も分かりそうです。

alloc_box vs alloc_stack

alloc_box

Allocates a reference-counted @box on the heap large enough to hold a value of type T, along with a retain count and any other metadata required by the runtime. The result of the instruction is the reference-counted @box reference that owns the box. The project_box instruction is used to retrieve the address of the value inside the box.
The box will be initialized with a retain count of 1; the storage will be uninitialized. The box owns the contained value, and releasing it to a retain count of zero destroys the contained value as if by destroy_addr. Releasing a box is undefined behavior if the box’s value is uninitialized. To deallocate a box whose value has not been initialized, dealloc_box should be used.

alloc_boxは、リファレンスカウンタ方式のボックスが ヒープ に確保されます。ボックスの初期化時に参照カウントが1になり、参照カウントが0になると解放されます。

alloc_stack

Allocates uninitialized memory that is sufficiently aligned on the stack to contain a value of type T. The result of the instruction is the address of the allocated memory.
If a type is runtime-sized, the compiler must emit code to potentially dynamically allocate memory. So there is no guarantee that the allocated memory is really located on the stack.
alloc_stack marks the start of the lifetime of the value; the allocation must be balanced with a dealloc_stack instruction to mark the end of its lifetime. All alloc_stack allocations must be deallocated prior to returning from a function. If a block has multiple predecessors, the stack height and order of allocations must be consistent coming from all predecessor blocks. alloc_stack allocations must be deallocated in last-in, first-out stack order.
The memory is not retainable. To allocate a retainable box for a value type, use alloc_box.

alloc_stackは、スタック に確保されます。こちらはリファレンスカウンタ方式ではありません。メソッドのスコープ内で解放する必要があります。

両者で大きく違うのは、ライフタイムです。例えばクロージャを使用する場合は、スコープ外で変数の値を変更する可能性もあります。その場合はalloc_boxによる参照カウンタで変数を管理する必要があります。一方で、メソッド内でのみ使うローカル変数はスコープを抜けると解放するのでalloc_stackを使います。

このように、varは単に何回も値を代入できる性質もありますが、定義されたスコープ外でも値を代入できると分かりました。

ただ、よくよく考えてみると今回使用したメソッドではvarはスコープ内でしか使われていません。その場合もalloc_boxを使うのが正しいのでしょうか?それでは、raw SILの次のステップとなるcanonical SILを読んでみましょう。

canonical SIL

下記のswiftcコマンドでcanonical SILを出力してみましょう。

$swiftc -emit-sil var.swift -o var.sil

var.sil

%0 = alloc_stack $Int, var, name "x"          
%1 = integer_literal $Builtin.Int64, 1        
%2 = struct $Int (%1 : $Builtin.Int64)        
%3 = begin_access [modify] [static] %0 : $*Int
store %2 to %3 : $*Int                        
end_access %3 : $*Int                         
%6 = begin_access [read] [static] %0 : $*Int  
end_access %6 : $*Int                         
dealloc_stack %0 : $*Int                      
return %2 : $Int                              

先程のvar.silgenの出力結果とかなり変わりました。そして予想通り、alloc_boxalloc_stackに置き換わってますね。これはcanonical SILを出力する過程で行われる最適化の影響です。具体的にはAllocBoxToStackモジュールで、不要なalloc_boxからalloc_stackにプロモーションしています。 https://github.com/apple/swift/blob/master/lib/SILOptimizer/Transforms/AllocBoxToStack.cpp

実装に関する説明はこちらの記事が詳しいので、興味のある方はご覧になってください。 https://blog.waft.me/2018/01/11/swift-sil-2/

let.swiftのcanonical SILも出力してみましょう。

$swiftc -emit-sil let.swift -o let.sil

let.sil

%0 = alloc_stack $Int, let, name "x"  
%1 = integer_literal $Builtin.Int64, 1
%2 = struct $Int (%1 : $Builtin.Int64)
store %2 to %0 : $*Int                
dealloc_stack %0 : $*Int              
return %2 : $Int                      

こちらは特に大きな変化は無いですね。かなり単純なメソッドなので、最適化できるところが少なかったのでしょう。

まとめ

今回は普段よく使うvarletをSILで読んでみました。SILでどのように表現されているかを見て、Swiftのライフタイムにおける値の扱いについての気づきもありましたし、Swift Compilerが良い感じに最適化してくれていることも知りました。単純だと思っていた文法も、実は様々なことがSwift Compiler内で考慮されていて非常に面白かったです。このおかげで明日からはよりSwiftらしいコードを書けるような気がしました。皆さんもSwiftで気になることがあればSILを読んでみましょう。

References

var x: Int
x = 1
x = 10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment