2017-02-20 3 views
3

をスピードアップ:40000のu8の配列については、私は次のコード持っているループ

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

を、それがcargo build --releaseを使用してコンパイル、私のマシン上で約2.5ミリ秒かかります。

次のC++コードは(それを実装し、錆からそれを呼び出すためにFFIを用いて検証)まったく同じデータのための私達に約100を取る:私はそれを考えている

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

ことが可能であるべきですRustの実装を高速化してC++バージョンと同じ速さで実行できます。

Rustプログラムはcargo --releaseを使用して構築されていますが、C++プログラムは最適化フラグなしで構築されています。

ヒント

+1

現在の 'Iterator'ソリューションを使用する代わりに、ポインタで安全でないコード(本質的にはC++コード)を使用し、より安全で(ポインタのオーバーランや結果的なセグメンテーションを防ぐ)、より直感的ですがオーバーヘッドが増えます。 – EvilTak

+1

'std :: mem :: swap'について知っていますか?また、インデックスを避けるために['get_unchecked'](https://doc.rust-lang.org/std/primitive.slice.html#method.get_unchecked)を試してみましたか(境界チェックを省略しない場合)?本当にこのループに費やされた時間を確認しましたか? –

+1

体験した時間を再現することはできません。私のマシンでは、RustコードはC++コードとまったく同じように約30μsで実行されます。 (編集:私はこれについての答えを書くことにしました) –

答えて

7

取得しているタイミングを再現できません。あなたは、あなたが測定する方法(または私が持っている方法)におそらく間違いがあります。私のマシンでは、両方のバージョンがまったく同じ時間に実行されます。

この回答では、まずC++とRustの両方のアセンブリ出力を比較します。その後、私は私ののタイミングを再現する方法を説明します。


組立比較

私は素晴らしいコンパイラエクスプローラ(Rust codeC++ Code)とのアセンブリコードを生成しました。私は最適化を有効にしてC++コードをコンパイルしました(-O3)。これは公正なゲームです(C++コンパイラの最適化は測定タイミングに影響を与えませんでした)。あなたはすぐにC++が実際に(錆がないほど多くの命令を生成する最適化C++なし)はるかに少ないアセンブリを生成しないことがわかります

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::[email protected]  | 

:ここでは結果のアセンブリ(右錆が左、C++)はあります。私はRustが作成する追加の命令のすべてについてはわかりませんが、少なくとも半分はバインドチェックのためのものです。しかし、この境界チェックは、私が理解する限り、[]経由の実際のアクセスではなく、ループの繰り返しごとに1回だけです。これは、スライスの長さが4で割り切れない場合にのみ当てはまります。しかし、Rustアセンブリはより良い(バインドされたチェックであっても)ことができると思います。

コメントに記載されているとおり、get_unchecked()get_unchecked_mut()を使用すると、バインドされたチェックを削除できます。しかし、これは私の測定のパフォーマンスには影響しませんでした。

最後に[&]::swap(i, j)を使用してください。

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

これは、パフォーマンスに特に影響しませんでした。しかし、それはより短く、より良いコードです。

を測定


私は(foocpp.cppで)このC++コードを使用:その後、私はすべてを測定するために、この錆コードを使用

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

:私はそれをコンパイル

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; 
    } 
} 

を:

#![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); 
} 

は、コンパイラが想定されていない場所での最適化を妨げています。私は(毎晩コンパイラ)とそれを実行:

LIBRARY_PATH=.:$LIBRARY_PATH cargo run --release 

私を与えるこのような(i7-6700HQ)の値は:

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

回はを変動する多く(両方のバージョンの違いよりも方法の詳細)。私は、なぜRustによって生成された追加のアセンブリが遅い実行を引き起こさないのか正確にはわかりません。

+0

男、素晴らしい答え!私はあなたのコード(インテルXeonプロセッサE3-1575MのV5)を走った、となった: 錆:期間{秒:0、またはnanos:21839} CPP:期間{秒:0、またはnanos:6576} (いくつかの変動性を) –

+0

実行順序を変更すると、最初にcppコードが実行されるため、結果がわずかに変更されるようです。 Cバージョンは、平均で約2倍の速さであるようです。 –

+0

私は最初に得た違いの近くに道はありません。おそらく、間違ったバイナリ(最適化されていないバイナリ)を誤って実行していたことが原因であると私は認めていません。 詳細な調査に感謝します。 Rustを学ぶときに私が本当に感謝したことは、Rustのコミュニティでとても楽しい音です。ありがとう! –

関連する問題