Skip to content

Instantly share code, notes, and snippets.

@kitasuke
Created May 12, 2018 09:43
Show Gist options
  • Save kitasuke/61403ca600d86f1eec4421e4dacaa97c to your computer and use it in GitHub Desktop.
Save kitasuke/61403ca600d86f1eec4421e4dacaa97c to your computer and use it in GitHub Desktop.
var-let-sil

var vs let in SIL

Hi all, I'm @kitasuke, iOS Engineer.

This is my first post, "var vs let in SIL" as series of "Swift type in SIL". Today I'm going to share what I've learned about how var and let work in SIL.

var and let

We use var and let a lot on a daily basis, but let me briefly explain about them.

var

As var is known as a variable, a value of var can be set or modified multiple times.

var x: Int
x = 1
x = 10

let

On the other hand, as let is known as a constant, a value of let can’t be changed once it’s set.

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

So it's quite simple. The difference is literally variable or constant.

SIL

Next, let's take a look at how they are represented in SIL.

Examples

There is a simple function number() which returns Int value. The only difference between two files is whether the Int value's declared as var or 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

Let's generate raw SIL for var.swift with swiftc command below.

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

Below is var.silgen which is raw SIL of var.swift. You might see unfamiliar functions, but there is no need to understand them yet.

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                            

Same swiftc command for let.swift.

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

Below is let.silgen which is raw SIL of let.swift.

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                            

Diff

I'll highlight the differences so that you could easily see them.

If you closely look at the diff, you can see alloc_box and begin_access in var.silgen.

%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 }              

However, you can see alloc_stack, not alloc_box in let.silgen.

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

What's the difference between alloc_box and alloc_stack? I guess this can guide us to deep understanding of what var and let are.

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.

According to documentation, alloc_box allocates a reference counted value on heap. It has to be manually managed by retain count.

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.

According to documentation, alloc_stack allocates a value on stack. It's not reference counting. All alloc_stack allocations must be deallocated prior to returning from a function.

The big difference here would be a lifetime of value. For example, if you have variable declared outside of closure but it's used in the closure, its value might be modified. For that case, it should be retain counted by alloc_box. However, if you have variable declared inside of function, it should be deallocated by alloc_stack.

I was thinking that var enables us to just modify its value multiple times, but it can also be done even outside of scope. That's why alloc_box is used for reference counting.

Come to think of it, In our example, we just used a local var inside function and its never modified out of scope. Does it really have to use alloc_box for the case? Let's look at canonical SIL next.

canonical SIL

Here is swiftc command to emit canonical SIL.

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

Below is var.sil which is canonical SIL of var.swift.

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                              

It's a bit different from var.silgen. As expected, alloc_box is replaced with alloc_stack in var.sil. How does it happen? This is a part of optimizations in swiftc. To be more specific, it's "box to stack promotion" in AllocBoxToStack module. The idea is that swiftc promotes unnecessary heap allocation to stack. Please see more details the link below.

https://github.com/apple/swift/blob/master/lib/SILOptimizer/Transforms/AllocBoxToStack.cpp

Same swiftc command for let.swift.

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

Below is let.sil which is canonical SIL of let.swift.

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                      

There is no major differences here. Raw SIL was enough simple, so I guess there is nothing to optimize in this pass.

Summary

Today, we dived into var and let in SIL. We found out that there is a lifetime for Swift values. Also swift compiler is really smart. It has lots of optimizations, not just the one I explained in this post. I thought that var and let are just simple, but they are well considered behind the scenes in the compiler. It might be too detailed knowledge, but it's always good to know how it works as Swift developer.

References

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment