加速循环

时间:2017-02-20 06:51:32

标签: optimization rust

我有以下代码:

for chunk in imagebuf.chunks_mut(4) {
    let temp = chunk[0];
    chunk[0] = chunk[2];
    chunk[2] = temp;
}

对于40000 u8的数组,我的机器上需要大约2.5毫秒,使用cargo build --release编译。

以下C ++代码对于完全相同的数据大约需要100 us(通过实现它并使用FFI从rust中调用它来验证):

for(;imagebuf!=endbuf;imagebuf+=4) {
    char c=imagebuf[0];
    imagebuf[0]=imagebuf[2];
    imagebuf[2]=c;
}

我认为应该可以加速Rust实现,以便像C ++版本一样快速执行。

Rust程序是使用cargo --release构建的,C ++程序是在没有任何优化标志的情况下构建的。

任何提示?

1 个答案:

答案 0 :(得分:8)

我无法重现你得到的时间。您测量(或我有)的方式可能有误。在我的机器上,两个版本都在同一时间运行。

在这个答案中,我将首先比较C ++和Rust版本的汇编输出。之后我将描述如何重现我的时间。

装配比较

我用令人惊奇的Compiler Explorer(Rust codeC++ Code)生成了汇编代码。我编译了C ++代码并激活了优化(-O3),以使其成为公平的游戏(但C ++编译器优化对测量的时序没有影响)。这是生成的程序集(Rust left,C ++ right):

example::foo_rust:                    |  foo_cpp(char*, char*):
    test    rsi, rsi                  |      cmp     rdi, rsi
    je      .LBB0_5                   |      je      .L3
    mov     r8d, 4                    |  
.LBB0_2:                              |  .L5:
    cmp     rsi, 4                    |  
    mov     rdx, rsi                  |  
    cmova   rdx, r8                   |  
    test    rdi, rdi                  |  
    je      .LBB0_5                   |  
    cmp     rdx, 3                    |  
    jb      .LBB0_6                   |  
    movzx   ecx, byte ptr [rdi]       |      movzx   edx, BYTE PTR [rdi]
    movzx   eax, byte ptr [rdi + 2]   |      movzx   eax, BYTE PTR [rdi+2]
                                      |      add     rdi, 4
    mov     byte ptr [rdi], al        |      mov     BYTE PTR [rdi-2], al
    mov     byte ptr [rdi + 2], cl    |      mov     BYTE PTR [rdi-4], dl
    lea     rdi, [rdi + rdx]          |  
    sub     rsi, rdx                  |      cmp     rsi, rdi
    jne     .LBB0_2                   |      jne     .L5 
.LBB0_5:                              |  .L3:
                                      |      xor     eax, eax
    ret                               |      ret
.LBB0_6:                              |  
    push    rbp                       +-----------------+  
    mov     rbp, rsp                                    |  
    lea     rdi, [rip + panic_bounds_check_loc.3]       |  
    mov     esi, 2                                      |  
    call    core::panicking::panic_bounds_check@PLT     |

你可以立即看到C ++确实产生了更少的汇编(没有优化C ++生成的指令几乎和Rust一样多)。我不确定Rust生成的所有附加指令,但至少有一半是用于绑定检查。但据我所知,这种绑定检查不是通过[]实际访问,而是每次循环迭代一次。这只是因为切片的长度不能被4整除。但我猜Rust组合可能会更好(即使是绑定检查)。

如评论中所述,您可以使用get_unchecked()get_unchecked_mut()删除绑定的检查。但请注意,这不会影响我测量的性能!

最后:你应该在这里使用[&]::swap(i, j)

for chunk in imagebuf.chunks_mut(4) {
    chunk.swap(0, 2);
}

这再次没有显着影响表现。但它是更短更好的代码。

测量

我使用了这个C ++代码(在foocpp.cpp中):

extern "C" void foo_cpp(char *imagebuf, char *endbuf);

void foo_cpp(char* imagebuf, char* endbuf) {
    for(;imagebuf!=endbuf;imagebuf+=4) {
        char c=imagebuf[0];
        imagebuf[0]=imagebuf[2];
        imagebuf[2]=c;
    }
}

我编译它:

gcc -c -O3 foocpp.cpp && ar rvs libfoocpp.a foocpp.o

然后我用这个Rust代码测量一切:

#![feature(test)]

extern crate libc;
extern crate test;

use test::black_box;
use std::time::Instant;

#[link(name = "foocpp")]
extern {
    fn foo_cpp(start: *mut libc::c_char, end: *const libc::c_char);
}

pub fn foo_rust(imagebuf: &mut [u8]) {
    for chunk in imagebuf.chunks_mut(4) {
        let temp = chunk[0];
        chunk[0] = chunk[2];
        chunk[2] = temp;
    }
}

fn main() {
    let mut buf = [0u8; 40_000];

    let before = Instant::now();

    foo_rust(black_box(&mut buf));
    black_box(buf);

    println!("rust: {:?}", Instant::now() - before);

    // ----------------------------------

    let mut buf = [0u8 as libc::c_char; 40_000];

    let before = Instant::now();

    let ptr = buf.as_mut_ptr();
    let end = unsafe { ptr.offset(buf.len() as isize) };
    unsafe { foo_cpp(black_box(ptr), black_box(end)); }
    black_box(buf);

    println!("cpp:  {:?}", Instant::now() - before);
}

整个地方black_box()阻止编译器优化它不应该的地方。我用(夜间编译器)执行它:

LIBRARY_PATH=.:$LIBRARY_PATH cargo run --release

给我(i7-6700HQ)这样的值:

rust: Duration { secs: 0, nanos: 30583 }
cpp:  Duration { secs: 0, nanos: 30810 }

时间波动很多(超过两个版本之间的差异)。我不确定为什么Rust生成的附加程序集不会导致执行速度变慢。