你可以信任由编译器优化的代码吗?( 二 )

编译器会收到一个指向某段内存的指针 。由于该内存包含了一个复杂的结构(即:ptr、len、capacity triple),因此它很难推理出该结构的演变 。对此,编译器可以从内存中加载该结构,并使用一堆标量局部变量,来进行替换聚合:
fn permute(xs: &mut Vec<i32>) {local ptr: ptrlocal len: usizelocal cap: usizeload ptr xs.ptrload len xs.lenload cap xs.cap...store xs.ptr ptrstore xs.len lenstore xs.cap cap}如此,编译器再次获得了推理能力 。虽然与内联类似,但是SROA主要用于内存,而不是代码 。
3、不可能与可能使用编译器模型的主要优势在于:

  • 在每个函数的基础上进行优化
  • 可以进行内联函数的调用
  • 擅长发现局部变量之间的关系,并据此重新排列代码
  • 能够对内存进行有限的推理(即,决定何时适合load或store) 。
当然,我们需要描述哪些代码可以被可靠地优化,哪些代码无法被优化,从而解释零成本抽象(zero cost abstractions) 。针对启用内联的情况,编译器需要知道有哪个函数被实际调用了 。如果属于直接调用,编译器则会尝试着与之内联;如果是间接调用(即:通过函数指针或通过虚函数表进行调用),那么在一般情况下,编译器将无法与之内联 。对于间接调用而言,编译器有时也可以推断指针的值,并对调用进行去虚拟化(de-virtualize) 。不过,这往往依赖于在其他地方已实现了成功的优化 。
这也就是为什么在Rust中,每个函数都有一个唯一的、大小为零(zero-sized)的类型,而且并没有运行时(runtime)的表示 。它静态的方式保证了编译器始终可以内联到代码上,以使得抽象的成本为零,毕竟任何优化编译器都会将其“融化”为零 。
当然,更高级别的语言则可能会选择始终使用函数指针去表示函数 。实际上,在许多情况下,它们生成的代码就等效为可优化的 。不过,在源代码中是不会有任何迹象来表明这是一个可优化的情况(实际指针在编译时是可知的),还是真正的动态调用状况 。因此,使用Rust就保证了可优化和潜在可优化之间的区别,能够反映在源语言之中,请参见如下代码段:
// Compiler is guaranteed to be able to inline call to `f`.fn call1<F: Fn()>(f: F) {f()}// Compiler _might_ be able to inline call to `f`.fn call2(f: fn()) {f()}由上述代码可知,其第一条规则便是使大多数调用可以被静态解析,以允许内联 。而函数指针和动态调度则会防止内联 。此外,内存中的间接寻址也会给编译器带来如下麻烦:
struct Foo {bar: Bar,baz: Baz,}显然,上述Foo结构对编译器是完全透明的 。不过,让我们稍作如下修改:
struct Foo {bar: Box<Bar>,baz: Baz,}上述结果就不那么明确了 。也就是说,被Foo占用的内存一般不会转移到被Bar占用的内存处 。同样,在许多情况下,鉴于唯一性,编译器可以通过box进行“不保证”的推理 。
如下代码段展示了map是如何被签名和定义的 。
#[inline]fn map<B, F>(self, f: F) -> Map<Self, F>whereSelf: Sized,F: FnMut(Self::Item) -> B,{Map::new(self, f)}关于内存的另一个重点是,一般而言,编译器不能改变整体布局 。SROA可以将一些数据结构加载到一堆局部变量中,然后可以用“一对指针”代替“一个指针和一个索引”的表示 。不过,SROA最终将不得不具体化“一个指针和一个索引”,并将该表示存储回内存之中 。这是因为内存布局是在所有函数之间共享的,因此函数不能单方面地指定更优化的表示 。
总之,只要能够“看到”代码,编译器就能够更擅长推理代码 。因此,请确保大多数调用在编译时可以实现内联 。
四、SIMD让我们将前面讨论的、为编译器提供可优化代码的通用框架,应用到自动矢量化上 。下面是我们优化计算两个字节片之间最长公共前缀的函数 。
use std::iter::zip;// 650 millisecondsfn common_prefix(xs: &[u8], ys: &[u8]) -> usize {let mut result = 0;for (x, y) in zip(xs, ys) {if x != y { break; }result += 1}result}如果您已经有了自动矢量化的模型,或者已查看了汇编的输出,那么就会发现一次仅处理一个字节的函数,效率非常慢 。我们该如何解决这个问题呢?
SIMD既然可以同时处理多个值,那么我们就希望编译器能够同时比较一堆字节 。例如,我们通过如下代码段,先一次性处理16个字节,然后分别处理剩余部分,以便使得结构更加显式化:


推荐阅读