我如何写 rust 代码编译成汇编,类似于由GCC从C产生?

mkshixfv  于 2022-12-19  发布在  其他
关注(0)|答案(2)|浏览(184)

我有这两个源文件:

const ARR_LEN: usize = 128 * 1024;

pub fn plain_mod_test(x: &[u64; ARR_LEN], m: u64, result: &mut [u64; ARR_LEN]) {
    for i in 0..ARR_LEN {
        result[i] = x[i] % m;
    }
}

以及

#include <stdint.h>

#define ARR_LEN (128 * 1024)

void plain_mod_test(uint64_t *x, uint64_t m, uint64_t *result) {
    for (int i = 0; i < ARR_LEN; ++ i) {
        result[i] = x[i] % m;
    }
}

我的C代码是一个很好的近似 rust 代码?
当我在www.example.com x86_64 gcc12.2 -O3上编译C代码时godbolt.org,我得到了一个合理的结果:

plain_mod_test:
        mov     r8, rdx
        xor     ecx, ecx
.L2:
        mov     rax, QWORD PTR [rdi+rcx]
        xor     edx, edx
        div     rsi
        mov     QWORD PTR [r8+rcx], rdx
        add     rcx, 8
        cmp     rcx, 1048576
        jne     .L2
        ret

但是当我对rustc 1.66 -C opt-level=3执行同样的操作时,我得到了以下冗长的输出:

example::plain_mod_test:
        push    rax
        test    rsi, rsi
        je      .LBB0_10
        mov     r8, rdx
        xor     ecx, ecx
        jmp     .LBB0_2
.LBB0_7:
        xor     edx, edx
        div     rsi
        mov     qword ptr [r8 + 8*rcx + 8], rdx
        mov     rcx, r9
        cmp     r9, 131072
        je      .LBB0_9
.LBB0_2:
        mov     rax, qword ptr [rdi + 8*rcx]
        mov     rdx, rax
        or      rdx, rsi
        shr     rdx, 32
        je      .LBB0_3
        xor     edx, edx
        div     rsi
        jmp     .LBB0_5
.LBB0_3:
        xor     edx, edx
        div     esi
.LBB0_5:
        mov     qword ptr [r8 + 8*rcx], rdx
        mov     rax, qword ptr [rdi + 8*rcx + 8]
        lea     r9, [rcx + 2]
        mov     rdx, rax
        or      rdx, rsi
        shr     rdx, 32
        jne     .LBB0_7
        xor     edx, edx
        div     esi
        mov     qword ptr [r8 + 8*rcx + 8], rdx
        mov     rcx, r9
        cmp     r9, 131072
        jne     .LBB0_2
.LBB0_9:
        pop     rax
        ret
.LBB0_10:
        lea     rdi, [rip + str.0]
        lea     rdx, [rip + .L__unnamed_1]
        mov     esi, 57
        call    qword ptr [rip + core::panicking::panic@GOTPCREL]
        ud2

如何编写Rust代码,使其编译为类似于gcc for C所生成的汇编程序?
更新:当我用clang 12.0.0 -O3编译C代码时,我得到的输出看起来更像Rust程序集,而不是GCC/C程序集。
这看起来像是GCC与Clang的问题,而不是C与Rust的差异。

plain_mod_test:                         # @plain_mod_test
        mov     r8, rdx
        xor     ecx, ecx
        jmp     .LBB0_1
.LBB0_6:                                #   in Loop: Header=BB0_1 Depth=1
        xor     edx, edx
        div     rsi
        mov     qword ptr [r8 + 8*rcx + 8], rdx
        add     rcx, 2
        cmp     rcx, 131072
        je      .LBB0_8
.LBB0_1:                                # =>This Inner Loop Header: Depth=1
        mov     rax, qword ptr [rdi + 8*rcx]
        mov     rdx, rax
        or      rdx, rsi
        shr     rdx, 32
        je      .LBB0_2
        xor     edx, edx
        div     rsi
        jmp     .LBB0_4
.LBB0_2:                                #   in Loop: Header=BB0_1 Depth=1
        xor     edx, edx
        div     esi
.LBB0_4:                                #   in Loop: Header=BB0_1 Depth=1
        mov     qword ptr [r8 + 8*rcx], rdx
        mov     rax, qword ptr [rdi + 8*rcx + 8]
        mov     rdx, rax
        or      rdx, rsi
        shr     rdx, 32
        jne     .LBB0_6
        xor     edx, edx
        div     esi
        mov     qword ptr [r8 + 8*rcx + 8], rdx
        add     rcx, 2
        cmp     rcx, 131072
        jne     .LBB0_1
.LBB0_8:
        ret
k7fdbhmy

k7fdbhmy1#

不要把苹果比作橙子蟹。
汇编输出之间的大部分差异是由于循环展开,rustc使用的LLVM代码生成器比GCC的代码生成器更积极地展开循环,并解决了CPU性能缺陷as explained in Peter Cordes’ answer

mov     r8, rdx
        xor     ecx, ecx
        jmp     .LBB0_1
.LBB0_6:
        xor     edx, edx
        div     rsi
        mov     qword ptr [r8 + 8*rcx + 8], rdx
        add     rcx, 2
        cmp     rcx, 131072
        je      .LBB0_8
.LBB0_1:
        mov     rax, qword ptr [rdi + 8*rcx]
        mov     rdx, rax
        or      rdx, rsi
        shr     rdx, 32
        je      .LBB0_2
        xor     edx, edx
        div     rsi
        jmp     .LBB0_4
.LBB0_2:
        xor     edx, edx
        div     esi
.LBB0_4:
        mov     qword ptr [r8 + 8*rcx], rdx
        mov     rax, qword ptr [rdi + 8*rcx + 8]
        mov     rdx, rax
        or      rdx, rsi
        shr     rdx, 32
        jne     .LBB0_6
        xor     edx, edx
        div     esi
        mov     qword ptr [r8 + 8*rcx + 8], rdx
        add     rcx, 2
        cmp     rcx, 131072
        jne     .LBB0_1
.LBB0_8:
        ret

这和Rust的版本差不多。
将Clang与-Os一起使用会使汇编更接近GCC:

mov     r8, rdx
        xor     ecx, ecx
.LBB0_1:
        mov     rax, qword ptr [rdi + 8*rcx]
        xor     edx, edx
        div     rsi
        mov     qword ptr [r8 + 8*rcx], rdx
        inc     rcx
        cmp     rcx, 131072
        jne     .LBB0_1
        ret

-C opt-level=s与rustc的关系也是如此:

push    rax
        test    rsi, rsi
        je      .LBB6_4
        mov     r8, rdx
        xor     ecx, ecx
.LBB6_2:
        mov     rax, qword ptr [rdi + 8*rcx]
        xor     edx, edx
        div     rsi
        mov     qword ptr [r8 + 8*rcx], rdx
        lea     rax, [rcx + 1]
        mov     rcx, rax
        cmp     rax, 131072
        jne     .LBB6_2
        pop     rax
        ret
.LBB6_4:
        lea     rdi, [rip + str.0]
        lea     rdx, [rip + .L__unnamed_1]
        mov     esi, 57
        call    qword ptr [rip + core::panicking::panic@GOTPCREL]
        ud2

当然,仍然需要检查m是否为零,这会导致一个异常分支,你可以通过缩小参数的类型来排除零来消除这个分支:

const ARR_LEN: usize = 128 * 1024;

pub fn plain_mod_test(x: &[u64; ARR_LEN], m: std::num::NonZeroU64, result: &mut [u64; ARR_LEN]) {
    for i in 0..ARR_LEN {
        result[i] = x[i] % m
    }
}

现在函数将发出与Clang相同的程序集。

nszi6y05

nszi6y052#

rustc使用LLVM后端优化器,因此与clang进行比较。LLVM默认展开小循环。
最近的LLVM也在冰湖之前针对英特尔CPU进行了调整,其中div r64div r32慢得多,慢得多,值得对其进行分支。

检查uint64_t是否实际适合uint32_t,并对div使用32位操作数大小。shr/je执行if ((dividend|divisor)>>32 == 0) use 32-bit以检查两个操作数的高半部分是否全为零。如果检查m的高半部分一次,并生成两个版本的循环,测试会更简单。2但是这段代码无论如何都会成为除法器吞吐量的瓶颈。

这种投机取巧的div r32代码生成最终会过时,因为Ice Lake的整数除法器足够宽,不需要为64位处理更多的微操作,所以性能只取决于实际值,而不管它上面是否有额外的32位零。
但是英特尔销售了很多基于Skylake的CPU(包括Cascade Lake服务器和Comet Lake的客户端CPU),虽然这些CPU仍然在广泛使用,LLVM -mtune=generic可能应该继续这样做。
有关详细信息:

相关问题