神秘的Builtin模块

原文: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编译器是怎样工作的。

神秘的Builtin模块_第1张图片

上图显示了Objective-C代码的编译过程:Objective-C代码经过Clang处理后,生成LLVM Intermediate Representation (IR),再经过LLVM处理,最后生成机器码。

可以把LLVM IR想象成某种高级的汇编语言,这种汇编语言和CPU架构(如i386或ARM)无关。任何一个编译器,只要能生成LLVM IR,就可以用LLVM生成和特定的CPU架构兼容的机器码。

明白了这点,我们再来看Swift编译器是如何工作的:

神秘的Builtin模块_第2张图片

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

; 

我知道你觉得上面的代码就像一坨屎,不过还是请注意:

  • 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。我们可以用unsafeBitCastvalue属性在IntBuiltin.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

你可能感兴趣的:(神秘的Builtin模块)