神秘的 Swift 内置模块

13,205 阅读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-CSwift 编译器是如何工作的。

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 IRLLVM 编辑器编译。

如你所见 SILLLVM 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,我们知道 SwiftInt 类型实际上是 Swift struct 类型,而且 + 操作符实际是个全局函数,是作为 Int 对于 lhsrhs 的重载。Int 语句不是语言的一部分,从某种意义上来说被语言直接理解的符号是诸如这 structclassifguard,它们才是语言的一部分。

Int+Swift stdlib 库的一部分,意味着它们也就不是原生构造,那就可以说这样消耗很大 or Swift 很慢?并不是。

这就是 Builtin 发挥作用的地方。BuiltinLLVM 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 Intinit 初始化的重载方法。

类似的,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-CSwift 语言大致的编译过程。LLVM 后端流程一样:LLVM 通过将 LLVM IR 转换成二进制代码。那么两者语言区别点在于 LLVM IR 代码的生成。 Objective-C 通过 Clang,而 Swift 通过自身的编译器先生成 SIL,再通过 IRGen 生成 LLVM IRSwift 在这个过程可以做很多优化(类型校验,初始化检验,不可达代码优化等)。

小结语

不像 Objective-C 部分代码只能靠猜,Swift 开源给了程序员更多的可探索性,开发信心及趣味性~ 大家一起学起来吧^_^

(PS:后面 swiftc -parse-stdlib add.swift && ./add 一直报错,知道的老铁可以告知下哟,ths~)