为什么LLVM似乎忽略了Rust的assume内部函数?

15

LLVM似乎忽略core::intrinsics::assume(..)调用。它们最终会出现在字节码中,但不会改变生成的机器码。例如,考虑以下(无意义)代码:

pub fn one(xs: &mut Vec<i32>) {
    if let Some(x) = xs.pop() {
        xs.push(x);
    }
}

这将编译为大量的汇编代码:

example::one:
        push    rbp
        push    r15
        push    r14
        push    r12
        push    rbx
        mov     rbx, qword ptr [rdi + 16]
        test    rbx, rbx
        je      .LBB0_9
        mov     r14, rdi
        lea     rsi, [rbx - 1]
        mov     qword ptr [rdi + 16], rsi
        mov     rdi, qword ptr [rdi]
        mov     ebp, dword ptr [rdi + 4*rbx - 4]
        cmp     rsi, qword ptr [r14 + 8]
        jne     .LBB0_8
        lea     rax, [rsi + rsi]
        cmp     rax, rbx
        cmova   rbx, rax
        mov     ecx, 4
        xor     r15d, r15d
        mov     rax, rbx
        mul     rcx
        mov     r12, rax
        setno   al
        jo      .LBB0_11
        mov     r15b, al
        shl     r15, 2
        test    rsi, rsi
        je      .LBB0_4
        shl     rsi, 2
        mov     edx, 4
        mov     rcx, r12
        call    qword ptr [rip + __rust_realloc@GOTPCREL]
        mov     rdi, rax
        test    rax, rax
        je      .LBB0_10
.LBB0_7:
        mov     qword ptr [r14], rdi
        mov     qword ptr [r14 + 8], rbx
        mov     rsi, qword ptr [r14 + 16]
.LBB0_8:
        or      ebp, 1
        mov     dword ptr [rdi + 4*rsi], ebp
        add     qword ptr [r14 + 16], 1
.LBB0_9:
        pop     rbx
        pop     r12
        pop     r14
        pop     r15
        pop     rbp
        ret
.LBB0_4:
        mov     rdi, r12
        mov     rsi, r15
        call    qword ptr [rip + __rust_alloc@GOTPCREL]
        mov     rdi, rax
        test    rax, rax
        jne     .LBB0_7
.LBB0_10:
        mov     rdi, r12
        mov     rsi, r15
        call    qword ptr [rip + alloc::alloc::handle_alloc_error@GOTPCREL]
        ud2
.LBB0_11:
        call    qword ptr [rip + alloc::raw_vec::capacity_overflow@GOTPCREL]
        ud2

现在我们可以假设,在 pop() 后,xs 没有达到满载状态(仅限于夜间版):

#![feature(core_intrinsics)]

pub fn one(xs: &mut Vec<i32>) {
    if let Some(x) = xs.pop() {
        unsafe {
            core::intrinsics::assume(xs.len() < xs.capacity());
        }
        xs.push(x);
    }
}

然而,尽管LLVM字节码中出现了assume,但汇编代码并没有改变。但是,如果我们在非假设情况下使用core::hint::unreachable_unchecked()创建一个分歧路径,例如:

pub fn one(xs: &mut Vec<i32>) {
    if let Some(x) = xs.pop() {
        if xs.len() >= xs.capacity() {
            unsafe { core::hint::unreachable_unchecked() }
        }
        xs.push(x);
    }
}

我们得到以下结果:

example::one:
        mov     rax, qword ptr [rdi + 16]
        test    rax, rax
        je      .LBB0_2
        mov     qword ptr [rdi + 16], rax
.LBB0_2:
        ret

这基本上是一个空操作,但也不算太糟糕。当然,我们可以通过使用以下代码来保留该值:

pub fn one(xs: &mut Vec<i32>) {
    xs.last_mut().map(|_e| ());
}

编译后得到我们所预期的结果:

example::one:
        ret

LLVM为什么似乎忽略了assume内部函数?


3
并不能保证优化一定会发生。LLVM在本例中没有优化代码并不意味着它总是忽略这种假设。我知道你想知道为什么LLVM在换了一种说法后“理解”了这个假设,但我认为理解这种差异需要非常详细的LLVM优化流程知识,而答案并不会给我们适用于其他情况的很多见解。 - Sven Marnach
1
你认为使用 assume 的汇编代码应该与使用 unreachable_unchecked 的汇编代码匹配吗?你的问题实际上是“为什么它们不一样?” - Shepmaster
3
是的。根据文档,优化器应该能够使用这个假设来移除未到达的代码路径,但它显然没有这样做。 - llogiq
LLVM有一个选项,在每个传递过程中打印LLVM IR,通常称为“-print-after-all”。这对于这种情况非常有用。您可以使用该选项编译两个近似等效的版本,然后查找LLVM行为分歧的点。这本身并不能告诉您原因,但当我这样做时,相关传递的git日志告诉了我想要知道的内容。 - arnt
如果assumeunreachable_unchecked有不同的行为,那么用户需要选择“最佳”的使用方式。这很奇怪,因为可以通过使用unreachable_unchecked来实现assume。Rust文档确实警告说对于assume,“优化器将尝试在传递之间保留它(及其条件),这可能会干扰周围代码的优化并降低性能。”但是文档没有类似的警告unreachable_unchecked。我想知道这是否是与LLVM的真正区别。 - Slix
1个回答

5

由于rustc和LLVM的改进,现在这个编译器只需输出ret。之前LLVM无法对内部函数进行优化,因此忽略了它,但现在它有能力更好地优化。


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