2017-02-20 21 views
3

У меня есть следующий фрагмент кода:Ускорение цикла

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 нас на те же данные (проверяется ее реализации и использования FFI называть его от ржавчины):

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

Я думаю, это должно возможно ускорить реализацию Rust для выполнения так же быстро, как и версия C++.

Программа Rust была построена с использованием cargo --release, программа C++ была создана без каких-либо флагов оптимизации.

Любые подсказки?

+1

Возможно, вам стоит взглянуть на использование небезопасного кода с указателями (по существу, на код C++) вместо использования текущего решения Iterator, которое намного безопаснее (предотвращает перерасход указателей и последующие segfaults) и более интуитивно понятное, но добавляет дополнительные накладные расходы , – EvilTak

+1

Вы знаете о 'std :: mem :: swap'? Кроме того, вы пытались использовать ['get_unchecked'] (https://doc.rust-lang.org/std/primitive.slice.html#method.get_unchecked), чтобы избежать индексов (в случае, если проверки границ не устранены)? Вы проверили, что потраченное время действительно в этом цикле? –

+1

Я не могу воспроизвести время, которое вы испытываете. На моей машине код Rust выполняется примерно через 30 мкс, как это делает код на C++. (Редактирование: я решил написать ответ об этом) –

ответ

7

Я не могу воспроизвести тайминги, которые вы получаете. Вероятно, у вас есть ошибка в том, как вы измеряете (или у меня есть). На моей машине обе версии работают точно в одно и то же время.

В этом ответе я сначала сравню вывод сборки как с версией C++, так и с Rust. Впоследствии я опишу, как воспроизводить мои таймингов.


сравнение Сборка

Я сгенерировал код сборки с удивительной Compiler проводника (Rust code, C++ Code). Я скомпилировал код C++ с активированными оптимизациями (-O3), чтобы сделать его честной (оптимизация компилятора на C++ не повлияла на измеренные тайминги). Вот в результате сборки (Rust слева, 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++ это на самом деле производит намного меньше сборки (без оптимизации 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); 
} 

повсюду предотвращает оптимизацию компилятора там, где он не предполагается. Я выполнил его с (ночной компилятором):

LIBRARY_PATH=.:$LIBRARY_PATH cargo run --release 

Давая мне (i7-6700HQ) значения, как это:

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

Времена колеблются много (намного больше, чем разница между двумя версиями). Я не совсем уверен, почему дополнительная сборка, созданная Rust, не приводит к более медленному выполнению.

+0

Человек, удивительный ответ! Я побежал код (Intel Xeon E3-1575M v5), и получил: ржавчины: Продолжительность {секунд: 0, NANOS: 21839} каст: Длительность {ИКС: 0, Nanos: 6576} (с некоторой изменчивостью) –

+0

Изменение порядка выполнения, поэтому код cpp идет первым, кажется, слегка меняет результаты. В среднем C-версия примерно в два раза быстрее. –

+0

Никоим образом не было той разницы, которую я получил вначале. Который, я ненавижу признаться, был, вероятно, потому, что я случайно использовал неправильный двоичный файл (неоптимизированный). Большое спасибо вам за подробное расследование! Что-то, что я действительно понял, изучая Русть, это очень приятный тон в сообществе Rust. Благодаря! –