原文:Swift's mysterious Builtin module
当你在Playground中cmd+click某个量,比如Int
的时候,你可能会看到下面的代码:
/// A 64-bit signed integer value type
public struct Int : SignedIntegerType, Comparable, Equatable, {
public var value: Builtin.Int64
...
}
或者当你阅读Swift标准库源代码的时候,你可能会看到许多形如Builtin.*
的方法,就像下面这些:
Builtin.Int1
Builtin.RawPointer
Builtin.NativeObject
Builtin.allocRaw(size._builtinWordValue,Builtin.alignof(Memory.self)))
那些反复出现的Builtin
可能会让你感到困惑:那TM是什么玩意?
Clang, Swift Compiler, SIL, IR, LLVM
要弄明白Builtin
是什么,先要明白Objective-C和Swift编译器是怎样工作的。
上图显示了Objective-C代码的编译过程:Objective-C代码经过Clang处理后,生成LLVM Intermediate Representation (IR),再经过LLVM处理,最后生成机器码。
可以把LLVM IR想象成某种高级的汇编语言,这种汇编语言和CPU架构(如i386或ARM)无关。任何一个编译器,只要能生成LLVM IR,就可以用LLVM生成和特定的CPU架构兼容的机器码。
明白了这点,我们再来看Swift编译器是如何工作的:
Swift代码首先被编译成SIL (Swift Intermediate Represention),然后再被编译成LLVM IR进入LLVM编译器,最后生成机器码。而SIL无非就是LLVM IR的一层Swift外壳(swifty wrapper),我们有很多理由需要SIL:比如确保变量在使用之前被初始化、检测不可执行的代码(unreachable code),优化代码等。如果你想知道SIL具体干了些啥,可以去看看这个视频。
我们再来看看LLVM IR,对于下面的代码:
let a = 5
let b = 6
let c = a + b
我们可以用这个命令
swiftc -emit-ir addswift.swift
将这三条语句编译成LLVM IR代码(以//^
开头的语句为注释):
...
//^ store 5 in a
store i64 5, i64* getelementptr inbounds (%Si* @_Tv8addswift1aSi, i32 0, i32 0), align 8
//^ store 6 in b
store i64 6, i64* getelementptr inbounds (%Si* @_Tv8addswift1bSi, i32 0, i32 0), align 8
//^ load a to virtual register %5
%5 = load i64* getelementptr inbounds (%Si* @_Tv8addswift1aSi, i32 0, i32 0), align 8
//^ load b to virtual register %6
%6 = load i64* getelementptr inbounds (%Si* @_Tv8addswift1bSi, i32 0, i32 0), align 8
//^ call llvm's signed addition with overflow on %5 and %6
//^ returns two values: sum and a flag if overflowed
%7 = call { i64, i1 } @llvm.sadd.with.overflow.i64(i64 %5, i64 %6)
//^ extract first value to %8
%8 = extractvalue { i64, i1 } %7, 0
//^ extract second value to %9
%9 = extractvalue { i64, i1 } %7, 1
//^ if overflowed jump to trap otherwise jump to label 10
br i1 %9, label %11, label %10
; <label>:10; preds = %once_done
//^ store result in c
store i64 %8, i64* getelementptr inbounds (%Si* @_Tv8addswift1cSi, i32 0, i32 0), align 8
ret i32 0
我知道你觉得上面的代码就像一坨屎,不过还是请注意:
-
i64
是定义在LLVM中的一个类型,代表64位整数。 -
llvm.sadd.with.overflow.i64
是个方法,这个方法将两个i64
整数相加并返回两个值:一个表示相加的和,另一个标识操作是否成功。
Builtin模块
回到Swift,我们知道在Swift中,Int
实际上一个struct
,而+
是一个针对Int
重载的全局方法(global function)。严格说来,Int
和+
不是Swift语言的一部分,它们是Swift标准库的一部分。既然不是原生态,是不是就意味着操作Int
或+
的时候会有额外的负担,导致Swift跑得慢?当然不是,因为我们有Builtin
。
Builtin
将LLVM IR的类型和方法直接暴露给Swift标准库,所以我们在操作Int
和+
的时候,没有额外的运行时负担。
以Int
为例,Int
在标准库中是一个struct
,定义了一个属性value
,类型是Builtin.Int64
。我们可以用unsafeBitCast
将value
属性在Int
和Builtin.Int64
之间相互转换。Int
还重载了init
方法,使得我们可以从Builtin.Int64
直接构造一个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
表示这个函数应该是个内联的(inline)。 -
Builtin.sadd_with_overflow_Int64
就是llvm.sadd.with.overflow.i64
。 - 相加的和被转换成
Int
类型,然后返回。
因为这是一个inline
方法,我们有理由相信编译器会内联展开方法调用并生成高效的LLVM IR代码。
我可以使用Builtin吗?
Builtin
模块只有在标准库内部才可以访问,一般的Swift程序是没有办法调用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))
只需要指定-parse-stdlib
,上面的代码就可以编译。
swiftc -parse-stdlib add.swift && ./add