前言
本文翻译自 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.Int1
Builtin.RawPointer
Builtin.NativeObject
Builtin.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)
方法转换回Swift
struct
Int
型,并且返回;
因此,这些都是内联调用的话,意味着将会生成非常好的能生成又快又好的二进制 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~)