|
| 1 | +# 内置函数、属性和元数据 |
| 2 | + |
| 3 | +在LLVM IR中,除了基础的数据表示、控制流之外,还有内置函数、属性和元数据等,能够影响二进制程序生成的功能。 |
| 4 | + |
| 5 | +## 内置函数 |
| 6 | + |
| 7 | +我们回顾一下,LLVM IR的作用实际上是将编译器前端与后端解耦合。编程语言的前端开发者,负责将输入的编程语言代码进行解析,生成LLVM IR;指令集架构的后端开发者,负责将输入的LLVM IR生成为目标架构的二进制指令。因此,LLVM IR提供了若干非常基础的指令,如`add`、`br`、`call`等。这样做的好处在于: |
| 8 | + |
| 9 | +* 对前端开发者而言,这些指令语义足够全,使用方法也和常见高级语言类似。 |
| 10 | +* 对后端开发者而言,这些指令相对数目比较少,提供的功能也相对较为独立,在大部分常见的指令集中都有类似的指令与其对应。 |
| 11 | + |
| 12 | +但是,这样的策略也有其弊端: |
| 13 | + |
| 14 | +* 对前端开发者而言,仍然有部分通用的语义无法被单个指令所涵盖 |
| 15 | +* 对后端开发者而言,对一些通用指令的优化无法针对LLVM IR指令来做 |
| 16 | + |
| 17 | +### `memcpy` |
| 18 | + |
| 19 | +以内存拷贝为例。熟悉AMD64或者AArch64的开发者一定知道,在这些支持向量操作的指令集架构中,大规模的内存拷贝往往是通过向量指令来实现的,Glibc中的`memcpy`就是这样实现的。 |
| 20 | + |
| 21 | +但是对于通用编程语言来说,标准库往往不喜欢直接调用libc中的函数,会产生一些不必要的依赖。并且,`memcpy`用向量操作来实现已经是一个非常通用的方案了,所以能不能复用一些逻辑呢? |
| 22 | + |
| 23 | +对于此类,LLVM IR指令过于基础,但是却非常广泛地使用同一套实现逻辑的情况,LLVM IR提供了「[内置函数](https://llvm.org/docs/LangRef.html#intrinsic-functions)」(Intrinsic Functions)功能来解决。 |
| 24 | + |
| 25 | +所谓内置函数,我们可以理解成一些可以像普通的LLVM IR函数一样调用的函数,但这些函数不需要开发者自己实现,LLVM的后端开发者提供了这些函数的实现。 |
| 26 | + |
| 27 | +例如,LLVM IR提供了[`llvm.memcpy`](https://llvm.org/docs/LangRef.html#llvm-memcpy-intrinsic)内置函数,以提供内存的拷贝操作。前端开发者只需要调用这个函数,就可以实现内存拷贝功能了。 |
| 28 | + |
| 29 | +我们熟知的Rust语言,在利用LLVM生成二进制程序时,就是使用的这个函数,可以参考其封装的[`LLVMRustBuildMemCpy`](https://github.com/rust-lang/rust/blob/90c541806f23a127002de5b4038be731ba1458ca/compiler/rustc_llvm/llvm-wrapper/RustWrapper.cpp#L1448-L1456)与调用者[`memcpy`](https://github.com/rust-lang/rust/blob/90c541806f23a127002de5b4038be731ba1458ca/compiler/rustc_codegen_llvm/src/builder.rs#L871-L896)。 |
| 30 | + |
| 31 | +### 静态分支预测 |
| 32 | + |
| 33 | +LLVM IR提供的内置函数有许多,这里,我们再以静态分支预测为例,介绍一个常见内置函数。 |
| 34 | + |
| 35 | +我们在阅读一些大规模项目源码时,例如Linux内核源码、QEMU源码等,往往会注意到大量使用的`likely`与`unlikely`,如: |
| 36 | + |
| 37 | +```c |
| 38 | +if (likely(x > 0)) { |
| 39 | + // Do something |
| 40 | +} |
| 41 | +``` |
| 42 | + |
| 43 | +这个`likely`是什么?它是干什么用的?事实上,`likely`与`unlikely`往往是通过宏定义实现的,它们的作用是静态分支预测。 |
| 44 | + |
| 45 | +我们知道,对于C语言等常见的编程语言的`if`语句,在生成二进制程序的时候,我们可以交换它的两个分支的位置。紧接着`cmp`等判断语句的分支,在执行时,不会发生跳转,而另一个分支则需要设置PC寄存器来跳转。这种跳转往往会造成一定程度的性能损耗,这些具体的我在「[在 Apple Silicon Mac 上入门汇编语言](https://github.com/Evian-Zhang/learn-assembly-on-Apple-Silicon-Mac)」中的[编译期分支预测](https://evian-zhang.github.io/learn-assembly-on-Apple-Silicon-Mac/11-跳转.html#编译期分支预测)一节中有详细阐述。总之,我们需要给编译器一些信息,来排布不同的分支布局。 |
| 46 | + |
| 47 | +对于Clang来说,这是通过[内置`expect`指令](https://llvm.org/docs/BranchWeightMetadata.html#built-in-expect-instructions)来实现的,也就是说: |
| 48 | + |
| 49 | +```c |
| 50 | +#define likely(x) __builtin_expect(!!(x), 1) |
| 51 | +#define unlikely(x) __builtin_expect(!!(x), 0) |
| 52 | +``` |
| 53 | + |
| 54 | +而`__builtin_expect`这个内置指令,就会翻译为LLVM IR中的[`llvm.expect`](https://llvm.org/docs/LangRef.html#llvm-expect-intrinsic)内置函数,从而实现了静态分支预测。 |
0 commit comments