前言
本文翻译自 Swift's mysterious Builtin module
翻译的不对的地方还请多多包涵指正,谢谢~
神秘的 Swift 内置模块
... 好吧,自从 swift 开源后也许不那么神秘。但不管怎样,如果你在playground 按住 Command 并单击Int类型,你也许已经注意到类似这种代码:
/// A 64-bit signed integer value
/// type.
public struct Int : SignedIntegerType, Comparable, Equatable {
public var value: Builtin.Int64
...
}
或者如果你已经阅读过 Swift 的 stdlib 库,那大概率注意到了有很多 Builtin.* 类的函数,诸如:
Builtin.Int1Builtin.RawPointerBuiltin.NativeObjectBuiltin.allocRaw(size._builtinWordValue, Builtin.alignof(Memory.self)))
因此,神秘的 Builtin 到底是什么呢?
Clang, Swift Compiler, SIL, IR, LLVM
为理解Builtin真正的作用,让我们快速简要地看看 Objective-C 和 Swift 编译器是如何工作的。
Objective-C
(隐藏了很多细节,但用于解释本篇文章完全ok)
Objective-C 代码进入 Clang 前端处理后会产出叫 LLVM 中间表示语言(Intermediate Representation = IR),IR 随后会被送入 LLVM,经过处理后二进制机器码就出来了。
LLVM 中间表示语言是一种类似高级的汇编语言,它是独立于架构的(如i368,ARM 等)。为了给使用 LLVM 的新语言创造编译器,我们只需要实现一个前端,它能够将新语言的代码转换成 LLVM 中间表示语言(IR),并将 IR 传递给 LLVM 生成给任何它所支持平台的汇编或者二进制代码。
Swift
Swift 首先生成 Swift 中间表示语言 SIL(Swift Intermediate Representation),它能够被转换成 LLVM IR 中间表示语言,接着 LLVM IR 被 LLVM 编辑器编译。
如你所见 SIL 是 LLVM IR 的快速化封装,它被创建出来是有很多原因的,比如:
- 保证变量在使用前被初始化;
- 检测不可达(未使用)的代码;
- 在代码发送给
LLVM前进行优化;
你可以看这个 Youtube 视频找到更多 SIL 存在的原因及它做的事情。
这里的主要内容就是 LLVM IR。对于像以下简单的 Swift 程序:
let a = 5
let b = 6
let c = a + b
转换成 LLVM IR 后如下:(可通过 swiftc -emit-ir addswift.swift 命令生成)
...
//^ 将 5 数值存入 a 变量
store i64 5, i64* getelementptr inbounds (%Si* @_Tv8addswift1aSi, i32 0, i32 0), align 8
//^ 将 6 数值存入 b 变量
store i64 6, i64* getelementptr inbounds (%Si* @_Tv8addswift1bSi, i32 0, i32 0), align 8
//^ 将 a 加载到虚拟寄存器 %5 内
%5 = load i64* getelementptr inbounds (%Si* @_Tv8addswift1aSi, i32 0, i32 0), align 8
//^ 将 b 加载到虚拟寄存器 %6 内
%6 = load i64* getelementptr inbounds (%Si* @_Tv8addswift1bSi, i32 0, i32 0), align 8
//^ 调用 llvm 有符号可溢出相加方法(返回值是两者之和及一个是否溢出标识)
%7 = call { i64, i1 } @llvm.sadd.with.overflow.i64(i64 %5, i64 %6)
//^ 提取第一个值放入 %8
%8 = extractvalue { i64, i1 } %7, 0
//^ 提取第二个值放入 %9
%9 = extractvalue { i64, i1 } %7, 1
//^ 如果溢出跳转trap分支(label 11),否则跳到 label10(类似汇编的 goto)
br i1 %9, label %11, label %10
; <label>:10 ; preds = %once_done
//^ 将结果存入变量 c
store i64 %8, i64* getelementptr inbounds (%Si* @_Tv8addswift1cSi, i32 0, i32 0), align 8
ret i32 0
...
在上述代码中,可以通过 //^ 符号找到我对于 LLVM IR 代码注释。
尽管上述代码看起来像一坨垃圾,但你只要知道这两件事:
- 在
LLVM里有个数据类型叫做i64,它是64位整数; - 在
LLVM里有个函数叫做llvm.sadd.with.overflow.i64,它能将两个i64整数相加并返回两个值,一个是和,另一个是1bit的溢出标识(如果相加失败);
可以解释 Bulitin 了
ok,回到 Swift,我们知道 Swift 的 Int 类型实际上是 Swift struct 类型,而且 + 操作符实际是个全局函数,是作为 Int 对于 lhs 和 rhs 的重载。Int 语句不是语言的一部分,从某种意义上来说被语言直接理解的符号是诸如这 struct, class, if, guard,它们才是语言的一部分。
Int 和 + 是 Swift stdlib 库的一部分,意味着它们也就不是原生构造,那就可以说这样消耗很大 or Swift 很慢?并不是。
这就是 Builtin 发挥作用的地方。Builtin 将 LLVM IR 类型直接暴露给 stdlib,因而没有运行时查找的消耗,也能够让 Int 作为 struct 来做类似的事情:extension Int { func times(otherInt: Int) -> Int { return self * otherInt } }; 5.times(6)
Swift struct Int 类型只包含了一个类型为 Builtin.Int64 名叫 value 存储属性,因为我们可使用 unsafeBitCast 对它来回转换,而且 stdlib 也提供了将 Builtin.Int64 转换为 Swift struct Int 的 init 初始化的重载方法。
类似的,UnsafePointer 及相关类是对 Builtin 直接内存访问方法的封装。例如:alloc 函数的定义是这样:
public static func alloc(num: Int) -> UnsafeMutablePointer {
let size = strideof(Memory.self) * num
return UnsafeMutablePointer(
Builtin.allocRaw(size._builtinWordValue, Builtin.alignof(Memory.self)))
}
现在咱知道了 Swift Int 不会引起性能问题,但 + 操作符呢。它还是一个函数,定义如下:
@_transparent
public func + (lhs: Int, rhs: Int) -> Int {
let (result, error) = Builtin.sadd_with_overflow_Int64(
lhs._value, rhs._value, true._value)
// return overflowChecked((Int(result), Bool(error)))
Builtin.condfail(error)
return Int(result)
}
- @_transparent 表示函数是以内联方式被调用;
Builtin.sadd_with_overflow_Int64对应我们之前在LLVM IR看到的会返回元组(Builtin.Int64类型的结果,Builtin.Int1类型的错误)的llvm.sadd.with.overflow.i64方法;- 结果通过
Int(result)方法转换回SwiftstructInt型,并且返回;
因此,这些都是内联调用的话,意味着将会生成非常好的能生成又快又好的二进制 LLVM IR 代码 ^_^
我可以玩玩 Builtin 吗
因为显而易见的原因,Swift 内的 Builtin 只在 stdlib 库及特殊 Swift 程序内可见。我们可以通过swiftc 的 -parse-stdlib 标识试试 Builtin。
例:
import Swift //Import swift stdlib
let result = Builtin.sadd_with_overflow_Int64(5.value, 6.value, true._getBuiltinLogicValue())
print(Int(result.0))
let result2 = Builtin.sadd_with_overflow_Int64(unsafeBitCast(5, Builtin.Int64), unsafeBitCast(6, Builtin.Int64), true._getBuiltinLogicValue())
print(unsafeBitCast(result2.0, Int.self))
swiftc -parse-stdlib add.swift && ./add
翻译完毕~
个人总结
本文主要解释了 Builtin 存在的原因:
- 加快编译速度。
Swift很多struct值类型,最终内部都封装了IILV IR基础类型,不需要过多转换; - 提高运行性能。由于不需要做过多转换,直接使用的
IILV IR的函数,相当于使用很多类似底层函数在开发,性能更高;
其次,文章给我们对比 Objective-C 和 Swift 语言大致的编译过程。LLVM 后端流程一样:LLVM 通过将 LLVM IR 转换成二进制代码。那么两者语言区别点在于 LLVM IR 代码的生成。 Objective-C 通过 Clang,而 Swift 通过自身的编译器先生成 SIL,再通过 IRGen 生成 LLVM IR,Swift 在这个过程可以做很多优化(类型校验,初始化检验,不可达代码优化等)。
小结语
不像 Objective-C 部分代码只能靠猜,Swift 开源给了程序员更多的可探索性,开发信心及趣味性~ 大家一起学起来吧^_^
(PS:后面 swiftc -parse-stdlib add.swift && ./add 一直报错,知道的老铁可以告知下哟,ths~)