为什么Rust编译器不会优化假设两个可变引用不会别名的代码?

406
据我所知,引用/指针别名可以阻碍编译器生成优化代码的能力,因为它们必须确保在两个引用/指针确实是别名的情况下生成的二进制代码表现正确。例如,在以下C代码中,
void adds(int *a, int *b) {
    *a += *b;
    *a += *b;
}

使用clang版本6.0.0-1ubuntu2(tags/RELEASE_600/final)编译时,使用-O3标志会生成以下内容:

0000000000000000 <adds>:
   0:    8b 07                    mov    (%rdi),%eax  # load a into EAX
   2:    03 06                    add    (%rsi),%eax  # load-and-add b
   4:    89 07                    mov    %eax,(%rdi)  # store into a
   6:    03 06                    add    (%rsi),%eax  # load-and-add b again
   8:    89 07                    mov    %eax,(%rdi)  # store into a again
   a:    c3                       retq

在这段代码中,为了防止int *aint *b别名存储,代码将结果两次存储回(%rdi)

当我们使用restrict关键字明确告诉编译器这两个指针不会别名时:

void adds(int *restrict a, int *restrict b) {
    *a += *b;
    *a += *b;
}

那么Clang将发出更优化的版本,有效地执行*a+=2*(*b),如果(如restrict所承诺的那样)*b没有通过对*a的赋值进行修改,则等效:

0000000000000000 <adds>:
   0:    8b 06                    mov    (%rsi),%eax   # load b once
   2:    01 c0                    add    %eax,%eax     # double it
   4:    01 07                    add    %eax,(%rdi)   # *a += 2 * (*b)
   6:    c3                       retq

由于 Rust 确保(除了不安全的代码之外)两个可变引用不能别名,因此我认为编译器应该能够发出更优化的代码版本。

当我使用下面的代码进行测试,并使用 rustc 1.35.0 编译它时,使用 -C opt-level=3 --emit obj 参数:

#![crate_type = "staticlib"]
#[no_mangle]
fn adds(a: &mut i32, b: &mut i32) {
    *a += *b;
    *a += *b;
}

它会生成:

0000000000000000 <adds>:
   0:    8b 07                    mov    (%rdi),%eax
   2:    03 06                    add    (%rsi),%eax
   4:    89 07                    mov    %eax,(%rdi)
   6:    03 06                    add    (%rsi),%eax
   8:    89 07                    mov    %eax,(%rdi)
   a:    c3                       retq

这并没有利用到 ab 不能别名的保证。

这是因为当前Rust编译器仍在开发中,并未将别名分析纳入优化中吗?

还是因为即使在安全的Rust中,ab 仍有可能成为别名?


7
链接 https://godbolt.org/z/aEDINX,很奇怪。 - Stargateur
130
旁注:“由于Rust确保(除了在unsafe代码中)两个可变引用不能别名” - 值得一提的是,即使在unsafe代码中,别名可变引用也是不允许的,并且会导致未定义行为。您可以有别名原始指针,但是unsafe代码实际上不允许您忽略Rust的标准规则。这只是一个常见的误解,因此值得指出。 - Lukas Kalbertodt
12
我花了一些时间才理解这个例子的含义,因为我不擅长阅读汇编语言,所以如果有帮助的话:这归结于adds函数体中的两个+=操作是否可以被重新解释为*a = *a + *b + *b。如果指针不别名,它们可以被重新解释,你甚至可以在第二个汇编列表中看到相当于b* + *b的内容: 2: 01 c0 add %eax,%eax。但是如果它们别名,则无法这样做,因为当您第二次添加*b时,它将包含与第一次不同的值(即您在第一个汇编列表的第4行存储的值)。 - dlukes
2
@dlukes:是的。我已经注释了汇编代码,并添加了*a += 2 * (*b)的等效表达式,以供未来读者参考。 - Peter Cordes
1个回答

476
Rust最初启用了LLVM的noalias属性,但这导致了错误编译的代码。当所有支持的LLVM版本不再对代码进行错误编译时,将重新启用该属性
如果在编译选项中添加-Zmutable-noalias=yes,你将得到预期的汇编代码:
adds:
        mov     eax, dword ptr [rsi]
        add     eax, eax
        add     dword ptr [rdi], eax
        ret

简单来说,Rust 在很多地方都使用了类似于 C 中的 restrict 关键字,比普通的 C 程序更为普遍。这导致 LLVM 处理这些情况时出现问题。结果发现,C 和 C++ 程序员使用 &mut 的频率远高于使用 restrict
这种情况已经发生过多次:
- Rust 1.0 到 1.7 — 启用 noalias - Rust 1.8 到 1.27 — 禁用 noalias - Rust 1.28 到 1.29 — 启用 noalias - Rust 1.30 到 1.54 — 禁用 noalias - Rust 1.54 到 ??? — 根据编译器使用的 LLVM 版本有条件地启用 noalias 相关的 Rust 问题:

27
不足为奇。尽管LLVM声称支持多种语言,但它实际上是专为C++后端设计的,并且一直对不太像C++的东西容易出错。 - Mason Wheeler
66
如果您单击某些问题,您可以找到使用“restrict”的C代码示例,并且在Clang和GCC上都会编译错误。这不仅限于那些不够“C++” 的语言,除非您将C++本身也归为该组。 - Shepmaster
6
我认为LLVM并不是真正基于C或C++的规则设计的,而是基于LLVM的规则设计的。它做出了通常适用于C或C++代码的假设,但据我所知,其设计是基于一个静态数据依赖模型的,无法处理棘手的边缘情况。如果它悲观地假定了不能被证明的数据依赖关系,那就没问题了,但是它将会写存储器的操作视为无操作,并具有读取和写入的潜在但不可证明的数据依赖关系,这种处理方法则存在问题。 - supercat
11
@supercat,我已经多次阅读了您的评论,但是我承认我感到困惑-我不知道它们与这个问题或答案有什么关系。未定义行为在这里并没有起作用,这只是多个优化通道相互作用不良的情况。 - Shepmaster
11
@avl_sweden 再次强调,这只是一个 bug。循环展开优化步骤在执行时没有完全考虑到 noalias 指针。它基于输入指针创建新指针,错误地复制了 noalias 属性,即使新的指针存在别名情况。 - Shepmaster
显示剩余16条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接