神秘的 Swift 内置模块

13,826 阅读6分钟

前言

本文翻译自 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

oc.png

(隐藏了很多细节,但用于解释本篇文章完全ok)

Objective-C 代码进入 Clang 前端处理后会产出叫 LLVM 中间表示语言(Intermediate Representation = IR),IR 随后会被送入 LLVM,经过处理后二进制机器码就出来了。

LLVM 中间表示语言是一种类似高级的汇编语言,它是独立于架构的(如i368,ARM 等)。为了给使用 LLVM 的新语言创造编译器,我们只需要实现一个前端,它能够将新语言的代码转换成 LLVM 中间表示语言(IR),并将 IR 传递给 LLVM 生成给任何它所支持平台的汇编或者二进制代码。

Swift

swift.png

Swift 首先生成 Swift 中间表示语言 SIL(Swift Intermediate Representation),它能够被转换成 LLVM IR 中间表示语言,接着 LLVM IR 被 LLVM 编辑器编译。

如你所见 SIL 是 LLVM IR 的快速化封装,它被创建出来是有很多原因的,比如:

  1. 保证变量在使用前被初始化;
  2. 检测不可达(未使用)的代码;
  3. 在代码发送给 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 代码注释。

尽管上述代码看起来像一坨垃圾,但你只要知道这两件事:

  1. 在 LLVM 里有个数据类型叫做 i64,它是64位整数;
  2. 在 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 存在的原因:

  1. 加快编译速度。Swift 很多 struct 值类型,最终内部都封装了 IILV IR 基础类型,不需要过多转换;
  2. 提高运行性能。由于不需要做过多转换,直接使用的 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~)