ChatGPT解决这个技术问题 Extra ChatGPT

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

据我所知,引用/指针别名会阻碍编译器生成优化代码的能力,因为它们必须确保生成的二进制文件在两个引用/指针确实别名的情况下正确运行。例如,在下面的 C 代码中,

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

clang version 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

此处代码存储回 (%rdi) 两次,以防 int *aint *b 别名。

当我们明确告诉编译器这两个指针不能使用 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 仍有可能出现别名?

旁注:“由于 Rust 确保(在不安全的代码中除外)两个可变引用不能别名”——值得一提的是,即使在 unsafe 代码中,也不允许对可变引用进行别名并导致在未定义的行为中。您可以使用别名原始指针,但 unsafe 代码实际上不允许您忽略 Rust 标准规则。这只是一个常见的误解,因此值得指出。
我花了一段时间才弄清楚这个例子的含义,因为我不擅长阅读 asm,所以以防万一它对其他人有帮助:归结为 {2 主体中的两个 += 操作是否} 可以重新解释为 *a = *a + *b + *b。如果指针没有别名,它们可以,您甚至可以在第二个 asm 列表中看到相当于 b* + *b 的内容:2: 01 c0 add %eax,%eax。但是如果他们做别名,他们就不能,因为当您第二次添加 *b 时,它将包含与第一次不同的值(您存储在第一个 asm 的第 4: 行的值)清单)。
@dlukes:是的。我评论了 asm 并为未来的读者添加了 *a += 2 * (*b) 等效项。

S
Shepmaster

Rust 最初确实启用了 LLVM 的 noalias 属性,但是这个 caused miscompiled code。当所有受支持的 LLVM 版本不再错误编译代码时,it will be re-enabled

如果将 -Zmutable-noalias=yes 添加到编译器选项,您将获得预期的程序集:

adds:
        mov     eax, dword ptr [rsi]
        add     eax, eax
        add     dword ptr [rdi], eax
        ret

简而言之,Rust 放置了相当于 C 的 restrict 关键字everywhere,比任何普通的 C 程序都要普遍得多。这使 LLVM 的极端情况超出了它能够正确处理的范围。事实证明,C 和 C++ 程序员使用 restrict 的频率不如 Rust 中使用的 &mut 频繁。

这已经发生了多次。

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 到 ??? — noalias 根据编译器使用的 LLVM 版本有条件地启用

相关的 Rust 问题

当前情况 nalgebra 的 Matrix::swap_rows() 的代码生成不正确 #54462 一旦 LLVM 不再错误编译它们,默认情况下重新启用 noalias 注释 #54878 Enable mutable noalias for LLVM >= 12 #82834 Regression: Miscompilation due to bug in "mutable noalias " 逻辑 #84958

nalgebra 的 Matrix::swap_rows() 的代码生成不正确 #54462

一旦 LLVM 不再错误编译它们,默认情况下重新启用 noalias 注释 #54878

为 LLVM >= 12 启用可变 noalias #82834

回归:由于“可变 noalias”逻辑中的错误导致的错误编译 #84958

以前的案例解决 LLVM 优化器错误,不将 &mut 指针标记为 noalias #31545 一旦 LLVM 不再错误编译它们,将 &mut 指针标记为 noalias #31681

通过不将 &mut 指针标记为 noalias 来解决 LLVM 优化器错误 #31545

一旦 LLVM 不再错误编译它们,将 &mut 指针标记为 noalias #31681

其他使用 LLVM 的范围 noalias 元数据 #16515 错过优化:来自指针的引用不被视为 noalias #38941 noalias 不够 #53105 mutable noalias:永久重新启用,仅用于恐慌=中止或稳定标志? #45029

利用 LLVM 的范围 noalias 元数据 #16515

错过的优化:来自指针的引用不被视为 noalias #38941

noalias 不够 #53105

mutable noalias:永久重新启用,仅用于恐慌=中止或稳定标志? #45029


这并不奇怪。尽管它广泛地声称对多语言友好,但 LLVM 是专门设计为 C++ 后端的,并且它总是有一种强烈的倾向,它会扼杀那些看起来不够像 C++ 的东西。
@MasonWheeler 如果您点击某些问题,您可以找到使用 restrict 并在 Clang 和 GCC 上错误编译的 C 代码示例。它不仅限于“C++ 不够”的语言,除非you count C++ itself in that group
@MasonWheeler:我不认为 LLVM 真的是围绕 C 或 C++ 的规则设计的,而是围绕 LLVM 的规则设计的。它做出的假设通常适用于 C 或 C++ 代码,但据我所知,该设计基于静态数据依赖模型,该模型无法处理棘手的极端情况。如果它悲观地假设无法证明的数据依赖关系,那将是可以的,但它会将其视为无操作操作,这些操作将使用与它所持有的相同的位模式写入存储,并且具有潜在但不可证明的数据依赖关系读和写。
@supercat 我读过你的评论几次,但我承认我很困惑——我不知道他们与这个问题或答案有什么关系。未定义的行为在这里没有发挥作用,这“只是”多个优化通道彼此交互不佳的情况。
@avl_sweden 重申一下,它是 just a bug。循环展开优化步骤在执行时确实(是否?)没有完全考虑 noalias 指针。它基于输入指针创建了新指针,即使新指针做了别名,也会不正确地复制 noalias 属性。