原文出处:Swift编译器结构分析

Swift介绍

Swift是一种高性能的语言,拥有整洁现代的语法。swift可以和C、OC的代码和框架无缝衔接,并且swift默认是内存安全的。 Swift的代码仓库包含了Swift编译器和标准库的源码,还有例如用于IDE集成的SourceKit等相关组件。

Swift编译器

概况来说,Swift编译器负责将Swift源码转成高效的可执行机器码。下面我们对Swift编译器的几个主要组件进行说明。

编译器基本知识

构建过程

构建过程分为

Swift编译器结构

我们下面分析的Swift编译器,负责的流程是上面构建过程中的第二步:(狭义的)编译

Swift的编译器主要组件包含以下几部分:

Parsing:语法分析

Swift的语法分析器是一个简单的,对整体通过递归向下的方式进行语法分析的手工编码词法分析器,他是在lib/Parse内实现的。该分析器负责生成不包含语义和类型信息的抽象语法树,简称AST(Abstract Syntax Tree)。这个阶段生成的AST也不包含警告和错误的注入。

lib/Parse

我们分析一下lib/Parse内的文件结构

文件管理:CMakeLists.txt 内部包含一个CMakeLists.txt文件和多个以cpp为后缀名的C++文件。 CMakeLists.txt是CMake的管理文件,想了解CMake可以在文末查看。CMakeLists.txt内容为

add_swift_library(swiftParse STATIC
  xxx.cpp
  ...
  LINK_LIBRARIES
  swiftAST
  swiftSyntax

源码分析文件:ParseXXX.cpp 内部的cpp文件是用于对代码进行语法分析的,例如Lexer.cppParseDecl.cppParseGeneric.cppParseStmt.cpp等文件,从命名我们就能知道他们的功能是对声明,表达式或者泛型等进行语法分析。 语法分析是将代码逐字分析,通过特定的正则表达式匹配,在词的层面上拆分成多个token,之后生成AST,想了解具体的知识可以查看之前整理的编译器学习系列,这里不展开细节。

流程管理文件 除了用于对源码进行具体分析的文件外,lib/Parse内部还包含一些流程控制文件。 通过文件PersistentParserState.cppSyntaxParsingCache.cppSyntaxParsingContext.cpp的命名和描述我们可以推断他们是用于对语法分析状态进行存储,缓存以及通过编译器不同条件的指令执行或者略过某些步骤的。

语法分析总结

语法分析使用以上的几种文件,对Swift源码进行逐字分析,并且可以通过编译器指令控制分析的流程和细节,词法分析生成的AST是不包含语义和类型信息的,也不 包含警告和错误等信息。

Senmantic analysis:语义分析

语义分析负责接收语法分析生成的AST,并将其转换成格式正确,进行了全面类型检查的AST,并且在源码中嵌入警告和错误等信息。这些功能是在lib/Sema内实现的。语义分析包含类型推断,确保可以从进行了类型检查的AST安全的生成代码。

lib/Sema

我们对lib/Sema内的结构进行一下分析:

CMakeLists.txt

语义分析的CMakeLists.txt内容如下

if (SWIFT_FORCE_OPTIMIZED_TYPECHECKER)
  set(EXTRA_TYPECHECKER_FLAGS "FORCE_BUILD_OPTIMIZED")
endif()

add_swift_library(swiftSema STATIC
  xxx.cpp
  ...
  LINK_LIBRARIES
  swiftParse
  swiftAST
  swiftSerialization
  ${EXTRA_TYPECHECKER_FLAGS}
)

和语法分析不同的是,

  1. 对条件SWIFT_FORCE_OPTIMIZED_TYPECHECKER是否进行强制类型检查优化进行了判断,这些条件可以在终端调试时通过指令进行控制,之后我们会在调试Swift编译器内进行说明。

  2. 链接库为AST库,语法分析库和Serialization库,以及是否增加额外类型检查的标记

语义分析文件

lib/Sema内部有大量使用C和C++语言编写的分析类,语义分析因为要进行符合当前语言规则对应的检查,所以相比语法分析,文件量要大很多。

我们进行粗略的归纳,可以划分为:

1. CSxxx.cpp/.h

约束系统的解决文件(solution to constraint system)

2. xxxDiag.cpp/.h

生成诊断的文件,我们在编译过程中的诊断,用于静态代码检查的诊断都是在语义分析阶段,通过这些诊断类去管理的。

3. CodeSyncxxx.cpp/.h

用于对表达式,声明等各种类型进行语义分析的文件。

4. Constraintxxx.cpp/.h

各种约束类

5. Debugxxx.cpp/.h & Instrumenterxxx.cpp/.h

用于支持调试的类

6. Derivedxxx.cpp/.h

各种协议的隐式派生实现

7. NameBinding.cpp

实现Swift的name binding

8. TypeCheckxxx.cpp/.h

对各种类型进行检查和语义分析,以及检查修正

9. 略
语义分析总结

语义分析生成经过了类型检查的AST,嵌入警告和错误,以及进行类型推断。这一阶段将语法分析生成的不带有语义内容的AST转换成符合Swift语义规则的AST,这些推断和类型检查为后续的优化提供了基础。

Clang importer

这个模块用于导入Clang组件Clang modules,并且将C和Objective-C输出的API对应到正确的Swift API。导入的AST的结果也会传递给语义分析。lib/ClangImporter内实现的。

lib/ClangImporter

我们分析一下lib/ClangImporter内的结构

CMakeLists.txt

set(SWIFT_GYB_FLAGS
  "-DCFDatabaseFile=${SWIFT_SOURCE_DIR}/lib/ClangImporter/CFDatabase.def")

add_gyb_target(generated_sorted_cf_database
  SortedCFDatabase.def.gyb)

add_swift_library(swiftClangImporter STATIC
  xxx.cpp
  LINK_LIBRARIES
  swiftAST
  swiftParse
)

## This property is only set by calls to clang_tablegen. It will not be set on
## standalone builds, so it can always be safely passed.
get_property(CLANG_TABLEGEN_TARGETS GLOBAL PROPERTY CLANG_TABLEGEN_TARGETS)
add_dependencies(swiftClangImporter
  generated_sorted_cf_database
  ${CLANG_TABLEGEN_TARGETS})

GYB(Generate Your Boilerplate)指模板生成,用于添加数据库模块。

链接库为swiftASTswiftParseAST和语法分析。

工具类

内容还有使用C和C++语言编写的工具类,用于方便的导入Clang实体提供正式的接口。内部的类有ImportEnumInfo.cpp/.hImportName.cpp/.hImportType.cpp/.h等,通过查看这些类的内容,我们发现包含#include "swift/AST/ASTContext.h"#include "clang/AST/ASTContext.h"等内容。

由此我们可以知道,Swift在使用Objective-C的代码时,通过lib/ClangImporter导入的不是Objective-C的源码,而是OC通过Clang编译器生成的AST.

Swfit不能使用Objective-C的宏

Swift是不同使用OC的宏的,原因如下:

上面的编译器基本知识说到了,宏的展开是在编译的前一个阶段,预处理阶段进行的。在预处理阶段将宏展开之后,Clang才对代码进行编译。Swift导入的是OC代码通过Clang生成的抽象语法树,OC内的宏是未经处理的代码段,所以Swift不能使用OC的宏。

SIL generation:SIL生成

SIL(Swift Intermediate Language),Swift的中间语言是一个Swift特定的用于进一步分析和优化Swift代码的高级的中间语言。

SIL的生成阶段将类型检查的AST降级为"raw"SIL。这些内容是在lib/SILGen内实现的。SIL的设计文档为docs/SIL.rst

lib/SILGen

分析组件结构:

CMakeLists.txt

add_swift_library(swiftSILGen STATIC
  xxx.cpp
  ...
  LINK_LIBRARIES
  swiftSIL
)

链接库只有swiftSIL

生成SIL的工具类

其他的SILGenxxx.cpp是用于生成SIL的工具类。

SIL详细分析

SIL是Swift定制的中间语言,针对Swift进行了大量的优化,使得Swift性能得到提升。SIL也是Swift编译器的精髓所在。这一部分内容我们在SIL详解内进行专项说明。

SIL guaranteed transformations:确保转换

该模块负责对影响程序准确性的数据流进行额外的诊断,例如使用未初始化的变量。转换的结果是生成"正式"(canonical)SIL。这些内容在lib/Analysislib/ARClib/LoopTransformslib/Transforms内实现。

LLVM IR Generation:生成LLVM的中间语言

该模块将SIL降级为LLVM IR,LLVM的中间语言,使得LLVM在此基础上可以进行进一步的优化,并且生成机器码。这些内容是在lib/IRGen内实现的。

信息补充

Cmake和Ninja介绍

我们这里介绍一下CMakeNinja,对它们有个基本的认识. CMake是一个开源,跨平台的用于构建,测试和打包的软件工具.CMake使用简单的平台和编译器单独配置文件来控制软件的编译流程,通过生成本地makefiles文件和workspace,之后用于用户选择编译环境.

Ninja是一个小而快的构建系统.它和其他构建系统的区别主要有两点: 1.它旨在将输入文件通过高级别的构建系统来生成.2.致力于快速构建,极致的追求速度.Ninja意在取代在执行增量构建时非常慢的Make.Make在处理类似于Google Chrome这种单次就会编译超过30,000个输入文件的大型项目时,速度缓慢会体现的更加明显.Google Chrome是Ninja的主要用户,Ninja还支持安卓编译,也支持大部分使用LLVM的项目.

参考资料


原文出处:Swift的高级中间语言:SIL

简介

在LLVM的官方文档中对Swift的编译器设计描述如下: Swift编程语言是在LLVM上构建,并且使用LLVM IR和LLVM的后端去生成代码。但是Swift编译器还包含新的高级别的中间语言,称为SIL。SIL会对Swift进行较高级别的语义分析和优化。 我们下面分析一下SIL设计的动机和SIL的应用,包括高级别的语义分析,诊断转换,去虚拟化,特化,引用计数优化,TBAA(Type Based Alias Analysis)等。并且会在某些流程中加入对SIL和LLVM IR对比。

SIL介绍

SIL是为了实现swift编程语言而设计的,包含高级语义信息的SSA格式的中间语言.SIL包含以下功能:

和LLVM IR不同,SIL一般是target无关的独立格式的表示,可用于代码分发.但是也可以和LLVM一样表达具体target概念. 如果想查看更多SIL的实现和SIL通道的开发信息,可以查看SIL开发手册(原英文文档为SILProgrammersManual.md)。

我们下面对Clang的Swift编译器的传递流程进行对比:

编译流程对比

Clang编译器流程

Clang编译流程存在以下问题:

Swift编译器流程

Swift作为一个高级别和安全的语言具有以下特点:

高级别语言

安全语言

Swift编译流程图如下:

Swift编译器提供的SIL具有以下优势:

SIL的设计

SIL流程分析

Swift编译器作为高级编译器,具有以下严格的传递流程结构。 Swift编译器的流程如下

SIL操作流程分析

SILGen

SILGen遍历Swift进行了类型检查的AST,产生 raw SIL.SILGen产生的SIL格式具有如下属性:

这些特性会被接下来的确保优化诊断检查使用,这两项在 raw SIL上一定会运行。

确保优化和诊断检查

SILGen之后,会在raw SIL上运行确定顺序的优化。我们并不希望编译器产生的诊断改变编译器的进展,所以这些优化的设计是简单和可预测.

透明函数即,如果一个函数只会受到入参的变化,那么这个函数每次的调用都会是相同的,同样的入参一定会返回一样的返回值,在确定入参的时候,返回值是可预测的。这样的 函数,就可以进行内联优化。

alloc_box结构优化为alloc_stack

提升无暴露地址(non_address-exposed)的alloc_stack说明到SSA注册.

这个算法的核心作用体现为:流程图中的临界如果在流分析前被拆分的话,会使得运算更近高效. 原文: A key point in the algorithm is that it can be much more effective if the critical edges in the flowgraph have been split before the flow analysis is performed.

如果诊断通道完成后,会产生规范SIL.

说完了处理raw SIL的特定流程,我们对上面提到的优化通道: optimization passes进行下说明.

泛型优化

SIL获取语言特定的类型信息,使得无法在LLVM IR实现的高级优化在swift编译器中得以实现.

func min<T: Comparable>(x: T, y: T) -> T {
  return y < x ? y : x
}

从普通的泛型展开

func min<T: Comparable>(x: T, y: T, FTable: FunctionTable) -> T {
  let xCopy = FTable.copy(x)
  let yCopy = FTable.copy(y)
  let m = FTable.lessThan(yCopy, xCopy) ? y : x
  FTable.release(x)
  FTable.release(y)
  return m
}

在确定入参类型时,比如Int,可以优化为

func min<Int>(x: Int, y: Int) -> Int {
  return y < x ? y : x
}

从而减少泛型调用的开销

SIL语法

SIL依赖于swift的类型系统和声明,所以SIL语法是swift的延伸.一个.sil文件是一个增加了SIL定义的swift源文件.swift源文件只会针对声明进行语法分析.swift的func方法体(除了嵌套声明)和最高阶的代码会被SIL语法分析器忽略.在.sil文件中没有隐式import.如果使用swift或者Buildin标准组件的话必须明确的引入. 以下是一个.sil文件的示例

sil_stage canonical

import Swift

// 定义用于SIL函数的类型

struct Point {
  var x : Double
  var y : Double
}

class Button {
  func onClick()
  func onMouseDown()
  func onMouseUp()
}

// 定义一个swift函数,函数体会被SIL忽略
func taxicabNorm(_ a:Point) -> Double {
  return a.x + a.y
}

// 定义一个SIL函数
// @_T5norms11taxicabNormfT1aV5norms5Point_Sd 是swift函数名taxicabNorm重整之后的命名
sil @_T5norms11taxicabNormfT1aV5norms5Point_Sd : $(Point) -> Double {
bb0(%0 : $Point):
  // func Swift.+(Double, Double) -> Double
  %1 = function_ref @_Tsoi1pfTSdSd_Sd
  %2 = struct_extract %0 : $Point, #Point.x    //萃取Point结构体内的x
  %3 = struct_extract %0 : $Point, #Point.y    ////萃取Point结构体内的y
  %4 = apply %1(%2, %3) : $(Double, Double) -> Double  //冒号前为计算体实现通过引用的展开,冒号后为类型说明
  return %4 : Double  //返回值
}

// 定义一个SIL虚函数表,匹配的是动态分派中函数实现的id,这个动态分派是在已知的静态类的类型虚函数表中
sil_vtable Button {
  #Button.onClick!1: @_TC5norms6Button7onClickfS0_FT_T_
  #Button.onMouseDown!1: @_TC5norms6Button11onMouseDownfS0_FT_T_
  #Button.onMouseUp!1: @_TC5norms6Button9onMouseUpfS0_FT_T_
}

SIL阶段

decl ::= sil-stage-decl
sil-stage-decl ::= 'sil_stage' sil-stage

sil-stage ::= 'raw'
sil-stage ::= 'canonical'

基于操作的不同阶段,SIL拥有不同的声明.

SIL类型

sil-type ::= '/pre> '*'? generic-parameter-list? type

SIL的类型是通过$符号进行标记的。SIL的类型系统和swift的密切相关.所以$之后的类型会根据swift的类型语法进行语法分析。

类型降级: type lowering

swift的正式类型系统,倾向于对大量的类型信息进行抽象概括.但是SIL目标是展示更多的实现细节,这个区别也体现在SIL的类型系统中.所以把正式类型降级为较低类型的操作称为类型降级。

提取区别:Abstraction Difference

包含未约束类型的通用函数一定会被非直接调用.比如分配充足内存和创建地址指针指向这块地址。如下的泛型函数

func generateArray<T>(n : Int, generator : () -> T) -> [T]

函数generator会通过一个隐式指针,指向存储在一个非直接调用的地址中,(可以参考之前static vs dynamic dispatch中虚函数表的设计和实现).在处理任意类型值时操作都是一样的.

但是我们也不希望现有的通用系统对我们的非通用代码进行低效处理。例如,我们希望()->Int可以直接返回结果。但是()->Int()->T的代替(subsitution),对于generateArray<Int>的调用应该向generator传递()->Int。所以一个正式类型在通用上下文中的表现可能会因为正式类型的的代替而不同.我们将这种不同成为提取区别.

SIL对于类型的提取区别的设计是,在每个级别的代替中,提取数值都可以被使用。

为了可以实现如上设计,泛型实例的正式类型应该一直使用非替换正式类型的提取方式进行降级.例如

struct Generator<T> {
  var fn : () -> T
}
var intGen : Generator<Int>

其中intGen.fn拥有代替类型()->Int,可以被降级为@callee_owned () -> Int,可以直接返回结果.但是如果更恰当的使用非代替方式,()->T就会变成@callee_owned () -> @out Int

当使用非代替的提取方式进行类型降级时,可以看做将拥有相同构造的类型中的具体类型替换为现有类型,以此来实现类型降级. 对于gGenerator<(Int, Int) -> Float>,g.fn是使用()->T进行降级的,简单理解就是,类型是否是具体类型,如果是,才能进行提取方式进行降级,不然只能产生

@callee_owned () -> @owned @callee_owned (@in (Int, Int)) -> @out Float.

所以提取区别来代替通用函数中类型的标准是:是否是具体类型.is materializable or not这个系统具有通过重复代替的方式实现提取方式的属性.所以可以把降级的类型看做提取方式的编码. SILGen已经拥有了使用提取方式转换类型的工序. 目前只有函数和元祖类型会通过提取区别进行改变.

合法的SIL类型

SIL类型的值应该是这样的:

类型T满足一下条件才是一个合法的_SIL_类型

注意,在递归条件内的类型,还需要是正式类型。例如泛型内的参数,仍然是Swift类型,而不是SIL降级类型。

地址类型

地址类型$*T指针指向的是任意引用的值或者$T
地址不是引用计数指针,不能被retained或released。

Box类型

本地变量和非直接的数值类型都是存储在堆上的,@box T是一个引用计数类型,指向的是包含了多种T的盒子。盒子使用的是Swift的原生引用计数。

Metatype类型

SIL内的metatype类型必须描述自身表示:

函数类型

SIL中的函数类型和Swift中的函数类型有以下区别:

VTables

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm decl ::= sil-vtable
sil-vtable ::= 'sil_vtable' identifier '{' sil-vtable-entry* '}'

sil-vtable-entry ::= sil-decl-ref ':' sil-linkage? sil-function-name

SIL使用class_method, super_method, objc_method,和 objc_super_method来表示类方法的动态分派dynamic dispatch class_methodsuper_method的实现是通过sil_vtable进行追踪的.sil_vtable的声明包含一个类的所有方法.

class A {
  func foo()
  func bar()
  func bas()
}

sil @A_foo : $@convention(thin) (@owned A) -> ()
sil @A_bar : $@convention(thin) (@owned A) -> ()
sil @A_bas : $@convention(thin) (@owned A) -> ()

sil_vtable A {
  #A.foo!1: @A_foo
  #A.bar!1: @A_bar
  #A.bas!1: @A_bas
}

class B : A {
  func bar()
}

sil @B_bar : $@convention(thin) (@owned B) -> ()

sil_vtable B {
  #A.foo!1: @A_foo
  #A.bar!1: @B_bar
  #A.bas!1: @A_bas
}

class C : B {
  func bas()
}

sil @C_bas : $@convention(thin) (@owned C) -> ()

sil_vtable C {
  #A.foo!1: @A_foo
  #A.bar!1: @B_bar
  #A.bas!1: @C_bas
}

swift的AST包含重载关系,可以用于在SIL的虚函数表中查找衍生类重载方法. 为了避免SIL方法是thunks),方法名是连接在原始方法实现之前.

Witness Tables

decl ::= sil-witness-table
sil-witness-table ::= 'sil_witness_table' sil-linkage?
  normal-protocol-conformance '{' sil-witness-entry* '}'

SIL将通用类型动态分派所需的信息编码为witness表.这些信息用于在生成二进制码时产生运行时分配表(runtime dispatch table).也可以用于对特定通用函数的SIL优化.每个明确的一致性声明都会产生witness表.通用类型的所有实例共享一个通用witness表.衍生类会继承基类的witness表.

protocol-conformance ::= normal-protocol-conformance
protocol-conformance ::= 'inherit' '(' protocol-conformance ')'
protocol-conformance ::= 'specialize' '<' substitution* '>'
  '(' protocol-conformance ')'
protocol-conformance ::= 'dependent'
normal-protocol-conformance ::= identifier ':' identifier 'module' identifier

witness的关键在于协议一致性.它是对于具体类型协议一致性的唯一标识.

sil-witness-entry ::= 'base_protocol' identifier ':' protocol-conformance
sil-witness-entry ::= 'method' sil-decl-ref ':' sil-function-name
sil-witness-entry ::= 'associated_type' identifier
sil-witness-entry ::= 'associated_type_protocol'
  '(' identifier ':' identifier ')' ':' protocol-conformance

witness table由以下内容构成

witness table作用

swift中的协议是通过结构体实现的,可以支持交互.例如参数,属性都可以是结构体.当将结构体传递给协议参数时,结构体特定的部分可能会丢失(在编译期).协议的witness table就可以发挥作用(在运行时).

Default Witness Tables

decl ::= sil-default-witness-table
sil-default-witness-table ::= 'sil_default_witness_table'
  identifier minimum-witness-table-size
  '{' sil-default-witness-entry* '}'
minimum-witness-table-size ::= integer

SIL编码要求默认witness table有开放(resilient)的默认实现.包含以下条件

强制方法的开放的默认实现,存储在协议的元数据中. 默认witness表关键在在自身协议.只有公共可见协议才需要默认witness表.私有协议和内部协议是对外部组件不可见的,所以他们没有增加新的强制方法的开放性问题.

sil-default-witness-entry ::= 'method' sil-decl-ref ':' sil-function-name

默认witness表目前只包含一项内容

全局变量

数据流错误

数据流错误可能存在于Raw SIL中,swift从语义上将那些条件定义为错误,所以他们必须使用诊断通道进行诊断,并且不能存在于规范SIL中.定义初始化 swift要求所有的本地变量在使用前必须被初始化.在构造函数中,结构体,枚举或类类型的实例变量必须在对象被使用前初始化.未全面覆盖(unreachable)的控制流 unreachableraw SIL中生成,标记错误的控制流.例如对于非Void的函数没有返回值,或者switch没有完全覆盖所有的条件.这种dead code消解的保证,可以避免unreachable的基础block,也可以避免方法返回不合法的空类型.

运行时错误

一些操作,比如无条件的检查转换次数失败或者编译器Buildin.trap.都会引起运行时错误,这种错误会无条件的终止当前操作.如果可以检验运行时错误会发生或者已经发生.只要将它们排列到程序操作之后就可以将这些运行时错误重新安排.例如对于没有确定开始和结尾的for循环代码

// Given unknown start and end values, this loop may overflow
for var i = unknownStartValue; i != unknownEndValue; ++i {
  ...
}

会将内存溢出挂起,产生loop的关联运行时错误,之后检测循环的起始和结束点.只要循环体对于当前的操作没有可见影响即可.

未定义的行为

某些操作的错误使用成为未定义行为.例如对于Buildin.RawPointer的不可用未检测的类型转换.或者使用低于LLVM说明的编译器内建函数,调用当前LLVM不支持的行为.SIL程序中的未定义行为是无意义的,就像C中的未定义行为一样,没有语义对其进行预测.未定义行为不应该被合法的SIL文件触发,但是在SIL级别不一定会被检测和证实.

调用协议

以下内容讨论swift函数是如何生成SIL的.

swift调用协议 @convention(swift) swift本地方法默认使用siwft调用协议是. 入参为原组的函数被递归解构为单独的参数,即包含被调用的基础块的入口,也包含调用者的apply说明

func foo(_ x:Int, y:Int)

sil @foo : $(x:Int, y:Int) -> () {
entry(%x : $Int, %y : $Int):
  ...
}

func bar(_ x:Int, y:(Int, Int))

sil @bar : $(x:Int, y:(Int, Int)) -> () {
entry(%x : $Int, %y0 : $Int, %y1 : $Int):
  ...
}

func call_foo_and_bar() {
  foo(1, 2)
  bar(4, (5, 6))
}

sil @call_foo_and_bar : $() -> () {
entry:
  ...
  %foo = function_ref @foo : $(x:Int, y:Int) -> ()
  %foo_result = apply %foo(%1, %2) : $(x:Int, y:Int) -> ()
  ...
  %bar = function_ref @bar : $(x:Int, y:(Int, Int)) -> ()
  %bar_result = apply %bar(%4, %5, %6) : $(x:Int, y:(Int, Int)) -> ()
}

调用以繁琐数据类型作为入参和输出值的函数时

func foo(_ x:Int, y:Float) -> UnicodeScalar

foo(x, y)

SIL内如下体现

%foo = constant_ref $(Int, Float) -> UnicodeScalar, @foo
%z = apply %foo(%x, %y) : $(Int, Float) -> UnicodeScalar

swift方法调用协议@convention(method) 方法调用协议用于独立方法的调用协议.柯里化method,使用self作为内部和外部参数.如果是非柯里化函数,self会在最后被传入

struct Foo {
  func method(_ x:Int) -> Int {}
}

sil @Foo_method_1 : $((x : Int), @inout Foo) -> Int { ... }

witness方法调用协议@convention(witness_method) witness方法调用协议是用于witness tables中的协议witness方法.它几乎等同于方法调用协议,只有对通用类型参数处理方面不同.对于非witness方法来说,机器协议可能会通过方法类型将方法签名进行静态转换.但是因为witness必须在Self类型下进行多态分配,所以Self相关的元数据必须通过最大化的提取规则传输.

C调用协议@convention(c) 在swift的C组件编译器中,C类型会被SIL对照到swift类型.C的函数入参和返回值,都会被SIL平台调用协议忽略. SIL和swift目前都不能调用包含可变参数的C的方法.

OC调用协议@convention(objc_method) SIL中的OC方法使用规范和ARC内一致.也可以从OC定义中引入属性. 使用@convention(block)并不会影响block的引用计数.

SIL中OC方法的self参数是非柯里化的最后一个参数.就像原生swift方法

@objc class NSString {
  func stringByPaddingToLength(Int) withString(NSString) startingAtIndex(Int)
}

sil @NSString_stringByPaddingToLength_withString_startingAtIndex \
  : $((Int, NSString, Int), NSString)

IR级别的将self作为第一个参数的行为在SIL中提取了的.比如现存的_cmd方法参数.

基于类型的别名分析: type based alias analysis

SIL提供了两种类型别名分析(TBAA: Type Based Alias Analysis):类TBAA和类型访问TBAA

指令集

感兴趣可以自行查看SIL指令集

初始化和销毁

alloc_stack
sil-instruction ::= 'alloc_stack' sil-type (',' debug-var-attr)*

%1 = alloc_stack $T
// %1 has type $*T

在栈区开辟充分符合T类型的内存空间。指令的返回结果是初始化的内存地址。

如果类型的尺寸在运行时才能确定,编译器必须动态初始化内存。所以并不能确保内存一定是初始化在栈区,例如如果是特别大的数值,可能会在堆区初始化,栈区持有指针。

alloc_stack标记了值声明周期的开始。在结束时必须使用dealloc_stack销毁。

内存不能被retain,如果想初始化可retain的类型,使用alloc_box

总结alloc_stack在栈区为值类型开辟内存。不使用引用计数。

alloc_box
sil-instruction ::= 'alloc_box' sil-type (',' debug-var-attr)*

%1 = alloc_box $T
//   %1 has type $@box T

在堆上开辟足够大的内存来支持各种类型的T,以@box持有引用计数。这个指令的结果是@box的引用计数持有的box,project_box是要来过去box内部的值的地址的。

box初始化时引用计数为1,但是内存并不会被初始化。box持有内部的值,在引用计数为0时使用destory_addr对内部值进行释放,无法释放box的值 没有被初始化的情况。这时候需要用到dealloc_box

总结alloc_box在堆上初始化指针类型的值,并且需要手动管理内存。

alloc_box和alloc_stack对比

alloc_boxalloc_stack最大的区别在于值的生命周期。举例,如果在闭包之外有一个变量声明,在闭包内使用了该变量。变量的值是可以被修改的,所以需要使用alloc_box来引用变量。

对于var声明的变量,因为可以多次修改它的值,甚至在作用域外也可以修改。所以使用alloc_box管理引用计数。

优化:Alloc box to stack

在SILGen阶段,会对闭包内使用变量的情况,通过alloc_box进行管理。

在SIL guaranteed transformations阶段,即生成正式SIL的阶段,会对于在闭包内没有进行值修改的变量内存分配进行优化,将alloc_box替换为alloc_stack。这个功能是在AllocBoxToStack组件内实现的。内部实现是将堆区不必要的初始化移动到栈区。

参考资料