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

创建于 2018-10-06  ·  33评论  ·  资料来源: rust-lang/rust

由于 LLVM 中的错误,此问题跟踪了https://github.com/rust-lang/rust/pull/54639 中引入的-Zmutable-alias=no默认值的撤消。 抄送@nagisa

似曾相识?

A-LLVM A-codegen C-tracking-issue I-slow T-compiler

最有用的评论

我将其简化为一个简单的 C 测试用例(在 -O3 和 -O0 处编译并比较输出):

#include <stdlib.h>
#include <stdio.h>
#include <assert.h>

__attribute__((always_inline))
static inline void copy(int *restrict a, int *restrict b) {
    assert(a != b);
    *b = *a;
    *a = 7;
}

__attribute__((noinline))
void floppy(int mat[static 2], size_t idxs[static 3]) {
    for (int i = 0; i < 3; i++) {
        copy(&mat[i%2], &mat[idxs[i]]);
    }
}

int main() {
    int mat[3] = {10, 20};
    size_t idxs[3] = {1, 0, 1};
    floppy(mat, idxs);
    printf("%d %d\n", mat[0], mat[1]);
}

请注意,如果删除restrict ,即noalias的 C 等效项,行为是正确的。 然而即便如此, assert(a != b)通过了,证明不会因为用restrict调用它而发生 UB。

发生的事情是:

  1. copy() 被内联,导致类似:
for (int i = 0; i < 3; i++) {
    mat[idxs[i]] = mat[i%2]; mat[i%2] = 7;
}
  1. LLVM 展开循环:
mat[idxs[0]] = mat[0]; mat[0] = 7; /* from copy(&mat[0%2], &mat[idxs[0]]) */
mat[idxs[1]] = mat[1]; mat[1] = 7; /* from copy(&mat[1%2], &mat[idxs[1]]) */
mat[idxs[2]] = mat[0]; mat[0] = 7; /* from copy(&mat[2%2], &mat[idxs[2]]) */
  1. LLVM 认为mat[0]不能与mat[idxs[1]]mat[1]别名,因此它不能在mat[0] = 7;mat[idxs[2]] = mat[0];之间更改,因此它是安全的全局值编号将后者优化为mat[idxs[2]] = 7;

但是mat[0]mat[idxs[1]] mat[0]别名,因为idxs[1] == 0 。 我们没有承诺它不会,因为在&mat[idxs[1]]传递给copy的第二次迭代中,另一个参数是&mat[1] 。 那么为什么 LLVM 认为它不能呢?

嗯,这与copy的内联方式有关。 noalias函数属性在加载和存储指令上转换为!alias.scope!noalias元数据,例如:

  %8 = load i32, i32* %0, align 4, !tbaa !8, !alias.scope !10, !noalias !13
  store i32 %8, i32* %7, align 4, !tbaa !8, !alias.scope !13, !noalias !10
  store i32 7, i32* %0, align 4, !tbaa !8, !alias.scope !10, !noalias !13

通常,如果一个函数被多次内联,则每个副本都会为 alias.scope 和 noalias 获取自己唯一的 ID,这表明每次调用都代表了它自己的“不等式”关系 * 标记为noalias ( restrict在 C 级别),每个调用可能有不同的值。

但是,在这种情况下,首先将函数内联到循环中,然后在展开循环时复制内联代码——并且这种复制不会更改 ID。 正因为如此,LLVM认为没有a的可以别名与任何b的,这是假的,因为a从第一和第三电话别名b来自第二次调用(都指向&mat[0] )。

令人惊讶的是,GCC 也会错误编译它,输出不同。 (-O0 处的 clang 和 GCC 都输出7 10 ;-O3 处的 clang 输出7 7 ;-O3 处的 GCC 输出10 7 。)呃,我真的希望我没有毕竟搞砸了并添加了UB,但我不知道如何......

* 比这稍微复杂一点,但在这种情况下,由于copy不使用任何指针算术并写入两个指针,因此不等式a != b是必要的并且足以调用不成为UB。

所有33条评论

我仍在努力找出根本问题。 有趣的票是https://github.com/rust-lang/rust/issues/54462。

使用@nagisa的最小复制:


没有不安全代码的最小化测试用例(确保使用 1 个代码生成单元进行编译!):

fn linidx(row: usize, col: usize) -> usize {
    row * 1 + col * 3
}

fn swappy() -> [f32; 12] {
    let mut mat = [1.0f32, 5.0, 9.0, 2.0, 6.0, 10.0, 3.0, 7.0, 11.0, 4.0, 8.0, 12.0];

    for i in 0..2 {
        for j in i+1..3 {
            if mat[linidx(j, 3)] > mat[linidx(i, 3)] {
                    for k in 0..4 {
                            let (x, rest) = mat.split_at_mut(linidx(i, k) + 1);
                            let a = x.last_mut().unwrap();
                            let b = rest.get_mut(linidx(j, k) - linidx(i, k) - 1).unwrap();
                            ::std::mem::swap(a, b);
                    }
            }
        }
    }

    mat
}

fn main() {
    let mat = swappy();
    assert_eq!([9.0, 5.0, 1.0, 10.0, 6.0, 2.0, 11.0, 7.0, 3.0, 12.0, 8.0, 4.0], mat);
}

我能够平分 LLVM 的优化通道以找到导致错误的优化通道。

运行此命令会产生一个有效的可执行文件(用您保存复制品的文件名替换bug.rs )。

rustc -Z no-parallel-llvm -C codegen-units=1 -O -Z mutable-noalias=yes -C llvm-args=-opt-bisect-limit=2260 bug.rs

运行此命令会导致可执行文件损坏(`assert_eq` 失败):

rustc -Z no-parallel-llvm -C codegen-units=1 -O -Z mutable-noalias=yes -C llvm-args=-opt-bisect-limit=2261 bug.rs

LLVM 平分输出

对于这个文件,优化2261对应于Global Value Numbering on function (_ZN3bug6swappy17hdcc51d0e284ea38bE)

平分 LLVM 修订版(使用 llvmlab bisect)将其缩小到 r305936-r305938,大概是 r305938:

[BasicAA] 使用 MayAlias 而不是 PartialAlias 进行后备。

请注意,这是 2017 年 6 月以来的一个相当古老的变化。

编辑:查看提交描述,似乎该错误在此之前就存在,但被 BasicAA 屏蔽,防止以后的别名传递运行,这是提交修复的内容。 这种情况涉及检查一对getelementptr指令之间的别名,其中编译器知道它们具有相同的基地址但不知道偏移量。

Edit2:此外,将-enable-scoped-noalias=false作为 LLVM 选项传递可防止错误编译。 (这并不奇怪,因为这完全禁用了 noalias 处理,但以防万一它有帮助......)

从 GVN 之前的 IR 来看,我觉得这里的根本原因可能是循环展开,这取决于我对 LLVM 别名注释如何工作的理解是否正确。

考虑一个类似的代码

int *a, *b;
for (int i = 0; i < 4; i++) {
    a[i & 1] = b[i & 1];
}

其中a[i & 1]b[i & 1]在单次迭代中不存在别名,但通常ab可能存在别名。

在 LLVM IR 中,这将类似于:

define void @test(i32* %addr1, i32* %addr2) {
start:
    br label %body

body:
    %i = phi i32 [ 0, %start ], [ %i2, %body ]
    %j = and i32 %i, 1
    %addr1i = getelementptr inbounds i32, i32* %addr1, i32 %j
    %addr2i = getelementptr inbounds i32, i32* %addr2, i32 %j

    %x = load i32, i32* %addr1i, !alias.scope !2
    store i32 %x, i32* %addr2i, !noalias !2

    %i2 = add i32 %i, 1
    %cmp = icmp slt i32 %i2, 4
    br i1 %cmp, label %body, label %end

end:
    ret void
}

!0 = !{!0}
!1 = !{!1, !0}
!2 = !{!1}

如果我们通过-loop-unroll运行它,我们会得到:

define void @test(i32* %addr1, i32* %addr2) {
start:
  br label %body

body:                                             ; preds = %start
  %x = load i32, i32* %addr1, !alias.scope !0
  store i32 %x, i32* %addr2, !noalias !0
  %addr1i.1 = getelementptr inbounds i32, i32* %addr1, i32 1
  %addr2i.1 = getelementptr inbounds i32, i32* %addr2, i32 1
  %x.1 = load i32, i32* %addr1i.1, !alias.scope !0
  store i32 %x.1, i32* %addr2i.1, !noalias !0
  %x.2 = load i32, i32* %addr1, !alias.scope !0
  store i32 %x.2, i32* %addr2, !noalias !0
  %addr1i.3 = getelementptr inbounds i32, i32* %addr1, i32 1
  %addr2i.3 = getelementptr inbounds i32, i32* %addr2, i32 1
  %x.3 = load i32, i32* %addr1i.3, !alias.scope !0
  store i32 %x.3, i32* %addr2i.3, !noalias !0
  ret void
}

!0 = !{!1}
!1 = distinct !{!1, !2}
!2 = distinct !{!2}

请注意循环的所有四个副本如何在同一别名域上使用别名元数据。 它不是在单个迭代中成为 noalias,而是整个函数中的 noalias。

最后, -scoped-noalias -gvn给了我们:

define void @test(i32* %addr1, i32* %addr2) {
start:
  %x = load i32, i32* %addr1, !alias.scope !0
  store i32 %x, i32* %addr2, !noalias !0
  %addr1i.1 = getelementptr inbounds i32, i32* %addr1, i32 1
  %addr2i.1 = getelementptr inbounds i32, i32* %addr2, i32 1
  %x.1 = load i32, i32* %addr1i.1, !alias.scope !0
  store i32 %x.1, i32* %addr2i.1, !noalias !0
  store i32 %x, i32* %addr2, !noalias !0
  store i32 %x.1, i32* %addr2i.1, !noalias !0
  ret void
}

!0 = !{!1}
!1 = distinct !{!1, !2}
!2 = distinct !{!2}

如果a = b + 1 ,这将导致不正确的结果。

可以使用以下代码从 C 重现此问题:

#include "stdio.h"

void copy(int * restrict to, int * restrict from) {
    *to = *from;
}

void test(int *a, int *b) {
    for (int i = 0; i < 4; i++) {
        copy(&b[i & 1], &a[i & 1]);
    }
}

int main() {
    int ary[] = {0, 1, 2};
    test(&ary[1], &ary[0]);
    printf("%d %d %d\n", ary[0], ary[1], ary[2]);
    return 1;
}

使用 Clang 6.0 这会在-O01 2 2-O3打印2 2 2 -O3 。 我不确定这段代码在 C 中的restrict语义下是否合法,但我认为它在 LLVM 更严格的noalias语义下应该是合法的。

我将其简化为一个简单的 C 测试用例(在 -O3 和 -O0 处编译并比较输出):

#include <stdlib.h>
#include <stdio.h>
#include <assert.h>

__attribute__((always_inline))
static inline void copy(int *restrict a, int *restrict b) {
    assert(a != b);
    *b = *a;
    *a = 7;
}

__attribute__((noinline))
void floppy(int mat[static 2], size_t idxs[static 3]) {
    for (int i = 0; i < 3; i++) {
        copy(&mat[i%2], &mat[idxs[i]]);
    }
}

int main() {
    int mat[3] = {10, 20};
    size_t idxs[3] = {1, 0, 1};
    floppy(mat, idxs);
    printf("%d %d\n", mat[0], mat[1]);
}

请注意,如果删除restrict ,即noalias的 C 等效项,行为是正确的。 然而即便如此, assert(a != b)通过了,证明不会因为用restrict调用它而发生 UB。

发生的事情是:

  1. copy() 被内联,导致类似:
for (int i = 0; i < 3; i++) {
    mat[idxs[i]] = mat[i%2]; mat[i%2] = 7;
}
  1. LLVM 展开循环:
mat[idxs[0]] = mat[0]; mat[0] = 7; /* from copy(&mat[0%2], &mat[idxs[0]]) */
mat[idxs[1]] = mat[1]; mat[1] = 7; /* from copy(&mat[1%2], &mat[idxs[1]]) */
mat[idxs[2]] = mat[0]; mat[0] = 7; /* from copy(&mat[2%2], &mat[idxs[2]]) */
  1. LLVM 认为mat[0]不能与mat[idxs[1]]mat[1]别名,因此它不能在mat[0] = 7;mat[idxs[2]] = mat[0];之间更改,因此它是安全的全局值编号将后者优化为mat[idxs[2]] = 7;

但是mat[0]mat[idxs[1]] mat[0]别名,因为idxs[1] == 0 。 我们没有承诺它不会,因为在&mat[idxs[1]]传递给copy的第二次迭代中,另一个参数是&mat[1] 。 那么为什么 LLVM 认为它不能呢?

嗯,这与copy的内联方式有关。 noalias函数属性在加载和存储指令上转换为!alias.scope!noalias元数据,例如:

  %8 = load i32, i32* %0, align 4, !tbaa !8, !alias.scope !10, !noalias !13
  store i32 %8, i32* %7, align 4, !tbaa !8, !alias.scope !13, !noalias !10
  store i32 7, i32* %0, align 4, !tbaa !8, !alias.scope !10, !noalias !13

通常,如果一个函数被多次内联,则每个副本都会为 alias.scope 和 noalias 获取自己唯一的 ID,这表明每次调用都代表了它自己的“不等式”关系 * 标记为noalias ( restrict在 C 级别),每个调用可能有不同的值。

但是,在这种情况下,首先将函数内联到循环中,然后在展开循环时复制内联代码——并且这种复制不会更改 ID。 正因为如此,LLVM认为没有a的可以别名与任何b的,这是假的,因为a从第一和第三电话别名b来自第二次调用(都指向&mat[0] )。

令人惊讶的是,GCC 也会错误编译它,输出不同。 (-O0 处的 clang 和 GCC 都输出7 10 ;-O3 处的 clang 输出7 7 ;-O3 处的 GCC 输出10 7 。)呃,我真的希望我没有毕竟搞砸了并添加了UB,但我不知道如何......

* 比这稍微复杂一点,但在这种情况下,由于copy不使用任何指针算术并写入两个指针,因此不等式a != b是必要的并且足以调用不成为UB。

嘿,看起来我和@nikic一起寻找相同的解释。 他们的测试用例稍微好一点:)

这是一个非常好的时机 ^^ 我们同时使用几乎相同的简化测试用例得出了相同的结论:)

要解决此问题,可能还需要在 LoopUnrollPass 中完成类似于https://github.com/llvm-mirror/llvm/blob/54d4881c352796b18bfe7314662a294754e3a752/lib/Transforms/Utils/InlineFunction.cpp#L801 的内容。

我已在https://bugs.llvm.org/show_bug.cgi?id=39282提交了针对此问题的 LLVM 错误报告

而且 - 只是为了完整性而提到这一点 - 我向 GCC 提交了一份错误报告,因为它也错误编译了我的 C 测试用例: https ://gcc.gnu.org/bugzilla/show_bug.cgi?id=87609

分类:如果我没看错,LLVM 修复已被接受(https://reviews.llvm.org/D9375)。 我不确定这对于实际合并到 LLVM 意味着什么,或者何时发生; 对 LLVM 的修订过程更了解的人应该检查问题现在是否已修复(以及适用于哪些版本)。

它没有合并,审查过程有点奇怪,确定它的人不再审查补丁。

有人呼吁使用“完全限制”补丁集

我愿意尝试这样做。
我可以访问中等强大的服务器(48 个 HT 内核,128G 内存),我想我可以设法使用补丁正确构建所有内容。
一旦我有了一个可用的工具链,你会推荐尝试哪些板条箱?

我在llvm 的上游主节点上完成了所有 Rust 特定提交的合并,然后应用了补丁
这是结果分支: https :
我现在将尝试编译工具链并尝试

首先确保您恢复: https :

一个好的尝试是这样的:

pub fn adds(a: &mut i32, b: &mut i32) {
    *a += *b;
    *a += *b;
}

并确认它编译为:

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

并不是

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

接下来,确保来自https://github.com/rust-lang/rust/issues/54462#issue -362850708 的代码不再错误编译。

@jrmuizel请注意,# 54639确实的最小复制器作为新的编译器测试。 也许完全恢复不合适?

我认为是否包含它并不重要,因为 AFAIK 它只是关于更改一些默认标志(可以用-Zmutable-noalias=yes覆盖),我可以手动编译提到的测试文件。

只是让您知道,我仍在尝试构建 LLVM,但截至目前,无论是否应用 Rust 特定补丁,我都会遇到编译错误(即:只是上游的 llvm master + 补丁也失败了):

In file included from /usr/include/c++/8/cmath:45,
                 from /opt/rust/src/llvm-project/llvm/include/llvm-c/DataTypes.h:28,
                 from /opt/rust/src/llvm-project/llvm/include/llvm/Support/DataTypes.h:16,
                 from /opt/rust/src/llvm-project/llvm/include/llvm/ADT/Hashing.h:47,
                 from /opt/rust/src/llvm-project/llvm/include/llvm/ADT/ArrayRef.h:12,
                 from /opt/rust/src/llvm-project/llvm/include/llvm/Transforms/Utils/NoAliasUtils.h:16,
                 from /opt/rust/src/llvm-project/llvm/lib/Transforms/Utils/NoAliasUtils.cpp:13:
/opt/rust/src/llvm-project/llvm/lib/Transforms/Utils/NoAliasUtils.cpp: In function ‘void llvm::cloneNoAliasScopes(llvm::ArrayRef<llvm::MetadataAsValue*>, llvm::DenseMap<llvm::MDN
ode*, llvm::MDNode*>&, llvm::DenseMap<llvm::MetadataAsValue*, llvm::MetadataAsValue*>&, llvm::StringRef, llvm::LLVMContext&)’:
/opt/rust/src/llvm-project/llvm/lib/Transforms/Utils/NoAliasUtils.cpp:174:30: error: no matching function for call to ‘llvm::AliasScopeNode::AliasScopeNode(double)’
         llvm::AliasScopeNode SNAN(MD);
                              ^~~~
In file included from /opt/rust/src/llvm-project/llvm/include/llvm/IR/TrackingMDRef.h:16,
                 from /opt/rust/src/llvm-project/llvm/include/llvm/IR/DebugLoc.h:17,
                 from /opt/rust/src/llvm-project/llvm/include/llvm/IR/Instruction.h:21,
                 from /opt/rust/src/llvm-project/llvm/include/llvm/IR/BasicBlock.h:22,
                 from /opt/rust/src/llvm-project/llvm/include/llvm/IR/Instructions.h:27,
                 from /opt/rust/src/llvm-project/llvm/include/llvm/Transforms/Utils/NoAliasUtils.h:22,
                 from /opt/rust/src/llvm-project/llvm/lib/Transforms/Utils/NoAliasUtils.cpp:13:
/opt/rust/src/llvm-project/llvm/include/llvm/IR/Metadata.h:1446:12: note: candidate: ‘llvm::AliasScopeNode::AliasScopeNode(const llvm::MDNode*)’
   explicit AliasScopeNode(const MDNode *N) : Node(N) {}
            ^~~~~~~~~~~~~~
/opt/rust/src/llvm-project/llvm/include/llvm/IR/Metadata.h:1446:12: note:   no known conversion for argument 1 from ‘double’ to ‘const llvm::MDNode*’
/opt/rust/src/llvm-project/llvm/include/llvm/IR/Metadata.h:1445:3: note: candidate: ‘constexpr llvm::AliasScopeNode::AliasScopeNode()’
   AliasScopeNode() = default;
   ^~~~~~~~~~~~~~
/opt/rust/src/llvm-project/llvm/include/llvm/IR/Metadata.h:1445:3: note:   candidate expects 0 arguments, 1 provided
/opt/rust/src/llvm-project/llvm/include/llvm/IR/Metadata.h:1441:7: note: candidate: ‘constexpr llvm::AliasScopeNode::AliasScopeNode(const llvm::AliasScopeNode&)’
 class AliasScopeNode {
       ^~~~~~~~~~~~~~
/opt/rust/src/llvm-project/llvm/include/llvm/IR/Metadata.h:1441:7: note:   no known conversion for argument 1 from ‘double’ to ‘const llvm::AliasScopeNode&’
/opt/rust/src/llvm-project/llvm/include/llvm/IR/Metadata.h:1441:7: note: candidate: ‘constexpr llvm::AliasScopeNode::AliasScopeNode(llvm::AliasScopeNode&&)’
/opt/rust/src/llvm-project/llvm/include/llvm/IR/Metadata.h:1441:7: note:   no known conversion for argument 1 from ‘double’ to ‘llvm::AliasScopeNode&&’
/opt/rust/src/llvm-project/llvm/lib/Transforms/Utils/NoAliasUtils.cpp:177:31: error: request for member ‘getName’ in ‘__builtin_nans(((const char*)""))’, which is of non-class ty
pe ‘double’
         auto ScopeName = SNAN.getName();
                               ^~~~~~~
/opt/rust/src/llvm-project/llvm/lib/Transforms/Utils/NoAliasUtils.cpp:187:39: error: request for member ‘getDomain’ in ‘__builtin_nans(((const char*)""))’, which is of non-class
type ‘double’
             const_cast<MDNode *>(SNAN.getDomain()), Name);
                                       ^~~~~~~~~
[ 75%] Building CXX object lib/Target/Hexagon/CMakeFiles/LLVMHexagonCodeGen.dir/RDFCopy.cpp.o
make[2]: *** [lib/Transforms/Utils/CMakeFiles/LLVMTransformUtils.dir/build.make:635: lib/Transforms/Utils/CMakeFiles/LLVMTransformUtils.dir/NoAliasUtils.cpp.o] Error 1
make[2]: *** Waiting for unfinished jobs....

也许 LLVM 主机已经与补丁不同步(考虑到补丁有多大以及 LLVM 主机移动的速度有多快,这可能会发生)并且您需要针对 LLVM 主机的旧版本进行构建? 就此问题向补丁作者发出 ping 通知绝对是值得的。

这正是我正在做的事情,试图找到一个使用补丁构建的旧版本:-)

我现在确定问题不在于 llvm/master,而是来自补丁。
来自 llvm/master 的仍然与补丁兼容的最旧提交是https://github.com/llvm/llvm-project/commit/5b99c189b3bfc0faa157f7ca39652c0bb8c315a7但即使如此,补丁也无法编译。
我现在太累了,懒得去尝试理解 C++,我明天再试试。
同时,有人可以联系补丁的作者寻求帮助吗?

我认为如果不修补rustllvm ,您将无法轻松地将主 LLVM 与 Rust 一起使用(在您实际构建它之后)。 AFAIK 它现在仅支持版本 6-9。

@mati865我首先尝试在 rust 的 llvm-9 fork 上应用补丁,但这也不是微不足道的......

该补丁显然是基于 llvm/ llvm-project@82d3ba87d06f9e2abc6e27d8799587d433c56630 之上的。 如果您在此基础上申请,它是否适合您?

@jrmuizel谢谢,我会试试的!
与此同时,我已经能够成功地调整rustllvm以使用 llvm master 进行构建。

Ping @PaulGrandperrin ,有任何更新吗?

https://reviews.llvm.org/D68484

现实地说,考虑到它的大小,估计的时间表和这个补丁被合并的总体机会是多少?

@MSxDOS您想问一下 LLVM 开发人员。 总的来说,我认为补丁的大小比所有者希望看到它合并的愿望更重要,所以要问的问题是 LLVM 希望看到它登陆的程度。

这是我看到的最新状态: https :

@leeoniya ,短期内没有使用起重机升降机进行选择构建的计划。 Cranelift 没有大量优化工作来实现这一目标。

我惊讶地发现编译器在假设没有此选项的情况下可能存在别名方面是多么保守。 例如:

fn baz(s: &mut S) {
    if s.y < 10 {
        s.x = foo();
    }

    if s.y < 5 {
        s.x = foo();
    }
}

因为它通过&mut访问结构成员,它假设s.xs.y可以别名,所以需要两次内存访问,而不是s.y的一次访问。 这真的很不幸,当您考虑必须在典型程序中交错通过&mut进行成员读/写的次数时。

编辑:根据一些测试,这似乎不会影响所有此类读/写,这并不奇怪,因为如果这样做,它可能会降低性能。 尽管如此,使用-Z mutable-noalias修复了上面示例中的双重内存访问,因此某些情况可能会被破坏。

此页面是否有帮助?
0 / 5 - 0 等级