[英]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 r64
比div 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.