由于 LLVM 中的错误,此问题跟踪了https://github.com/rust-lang/rust/pull/54639 中引入的-Zmutable-alias=no
默认值的撤消。 抄送@nagisa
(似曾相识? )
我仍在努力找出根本问题。 有趣的票是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
对于这个文件,优化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]
在单次迭代中不存在别名,但通常a
和b
可能存在别名。
在 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 这会在-O0
和1 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。
发生的事情是:
for (int i = 0; i < 3; i++) {
mat[idxs[i]] = mat[i%2]; mat[i%2] = 7;
}
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]]) */
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 的代码不再错误编译。
我认为是否包含它并不重要,因为 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 ,有任何更新吗?
现实地说,考虑到它的大小,估计的时间表和这个补丁被合并的总体机会是多少?
@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.x
和s.y
可以别名,所以需要两次内存访问,而不是s.y
的一次访问。 这真的很不幸,当您考虑必须在典型程序中交错通过&mut
进行成员读/写的次数时。
编辑:根据一些测试,这似乎不会影响所有此类读/写,这并不奇怪,因为如果这样做,它可能会降低性能。 尽管如此,使用-Z mutable-noalias
修复了上面示例中的双重内存访问,因此某些情况可能会被破坏。
@PaulGrandperrin在https://reviews.llvm.org/D69542 上有一个基于llvm@9fb46a452d4e5666828c95610ceac8dcd9e4ce16 的补丁的新版本
最有用的评论
我将其简化为一个简单的 C 测试用例(在 -O3 和 -O0 处编译并比较输出):
请注意,如果删除
restrict
,即noalias
的 C 等效项,行为是正确的。 然而即便如此,assert(a != b)
通过了,证明不会因为用restrict
调用它而发生 UB。发生的事情是:
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
元数据,例如:通常,如果一个函数被多次内联,则每个副本都会为 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。