簡體   English   中英

我如何編寫編譯成類似於 GCC 從 C 生成的程序集的 Rust 代碼?

[英]How do I write Rust code which compiles to assembly which resembles that produced by GCC from C?

我有這兩個源文件:

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 代碼?

當我在 godbolt.org x86_64 gcc12.2 -O3上編譯 C 代碼時,我明白了:

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 為 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

不要將蘋果與橙色螃蟹進行比較。

匯編輸出之間的大部分差異是由於循環展開,rustc 使用的 LLVM 代碼生成器比 GCC 更積極地執行循環展開,並且解決了 CPU 性能缺陷,如 Peter Cordes 的回答中所述 當您使用 Clang 15 編譯相同的 C 代碼時,它會輸出:

        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 發出相同的程序集。

rustc使用 LLVM 后端優化器,因此與clang進行比較。 LLVM 默認展開小循環。

最近的 LLVM 還在 Ice Lake 之前針對 Intel 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它上面的零位。 AMD 已經有一段時間了。

但英特爾出售了很多基於 Skylake 重新設計的 CPU(包括 Cascade Lake 服務器和客戶端 CPU,直至 Comet Lake)。 雖然這些仍在廣泛使用,但 LLVM -mtune=generic可能應該繼續這樣做。

更多細節:

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM