asm

对Rust生成汇编的分析 以及对Rust编译器的分析

本篇会讨论 如何写出高性能的Rust代码

本篇分为几个章节

  • 良好的优化习惯: 讲述了良好的习惯来让生成的汇编更快

  • 汇编观察: 对生成的汇编的优化进行观察与思考

工具链

cargo-asm

将函数生成汇编

首先我们需要安装cargo-asm工具

然后我们可以对函数生成汇编

cargo asm crate::function --rust
cargo asm crate::Type::method --rust

尽量使用#[inline(never)] 否则一些函数会被编译器内联而使rust-asm看不到汇编

良好的优化习惯

尽量使用开区间Range

前闭后开是计算机领域最好的表示法

为什么编号应该从零开始?

rust闭区间不会被llvm优化

Rust编译器会对开区间Range有优化 这是由于C语言不流行闭区间RangeInclusive

出现这种情况的原因是,包含范围需要进行特殊检查,以处理类似这样的情况1..=i32::MAX,这种情况无法用排除范围来表示。

使用for循环时,Rust 会反复.next()调用迭代器。每次调用时,它都必须检查当前迭代是否是上一次迭代的特殊情况。而使用.fold()for 循环.for_each(),迭代器可以遍历所有迭代1..n,然后单独执行n最后一次迭代,这更容易被 LLVM 优化。

代码与汇编

这里贴出一个前n项和的代码

pub fn sum_to_n(n: i32) -> i32 {
    let mut s = 0;
    for i in 1..=n{
	s += i;
	
    }
    s
}
pub fn sum_to_n(n: i32) -> i32 {
    let mut s = 0;
    for i in 1..n+1{
	s += i;
    }
    s
}

两段代码对应的x86汇编是

// 判断edi寄存器是否为0
 test    edi, edi 
// 为0则跳转到.LBB0_1
 jle     .LBB0_1
 // 将ecx的值复制为1
 mov     ecx, 1
 // eax=0
 xor     eax, eax
.LBB0_3:
// edx=0
 xor     edx, edx
 // 比较ecx - edi与0 写入标志位
 cmp     ecx, edi
 // 将标志位结果写入sil
 setl    sil
 add     eax, ecx
 cmp     ecx, edi
 jge     .LBB0_5
 mov     dl, sil
 add     ecx, edx
 cmp     ecx, edi
 jle     .LBB0_3
.LBB0_5:
 ret
.LBB0_1:
 xor     eax, eax
 ret
 xor     eax, eax
 test    edi, edi
 jle     .LBB1_4
 cmp     edi, 1
 je      .LBB1_3
 lea     eax, [rdi, -, 2]
 lea     ecx, [rdi, -, 3]
 imul    rcx, rax
 shr     rcx
 lea     eax, [rcx, +, 2*rdi]
 add     eax, -3
.LBB1_3:
 add     eax, edi
.LBB1_4:
 ret

benchmark

在实验下

Benchmarking sum/rangeinclusive: Collecting 100 samples in estimated 5.0001 sum/rangeinclusive      time:   [18.604 ns 18.618 ns 18.637 ns]
Found 12 outliers among 100 measurements (12.00%)
  5 (5.00%) high mild
  7 (7.00%) high severe
sum/range               time:   [1.5075 ns 1.5077 ns 1.5079 ns]
Found 4 outliers among 100 measurements (4.00%)
  4 (4.00%) high mild
sum/formula             time:   [1.0059 ns 1.0071 ns 1.0092 ns]
Found 16 outliers among 100 measurements (16.00%)
  3 (3.00%) high mild
  13 (13.00%) high severe

我们观察到

  • 直接使用 \(n(n+1)/2\)的formula函数均值为1.007ns

  • 使用闭区间的rangeinclusive函数的均值为18.618ns

  • 使用开区间的range函数均值为1.5007ns

汇编观察

整数除法

我们贴出最简单的1+...+n的通项的代码

#[inline(never)]
pub fn formula(n: i32)  -> i32 {
    n * (n+1)/2 
}

它生成的汇编为

 lea     ecx, [rdi, +, 1]
 imul    ecx, edi
 mov     eax, ecx
 shr     eax, 31
 add     eax, ecx
 sar     eax
 ret