##環境
toolchain: nightly-i686-pc-windows-msvc
rustc 1.34.0-nightly (e1c6d0057 2019-02-22)
##目的
Rustで01011110 01101011 01001011 00001101
みたいなバイト列を5E6B4B0D
みたいな16進数表記の文字列に変換するということを考えてみます。
##実装
###format! マクロ
これが一番お手軽です。format! のパラメタについてはここを参照してください。
let data: [u8; 4] = [0b01011110, 0b01101011, 0b01001011, 0b00001101];
let result = data.iter().map(|n| format!("{:02X}", n)).collect::<String>();
println!("{}", result); //5E6B4B0D
でもこれって都度format!()
で変換の処理があるし、遅いんじゃないか?と思ったのでベンチマーク取ってみます。
#[bench]
fn bench_to_hex_format(b: &mut Bencher) {
let data: [u8; 4] = [0b01011110, 0b01101011, 0b01001011, 0b00001101];
b.iter(|| {
data.iter().map(|n| format!("{:02X}", n)).collect::<String>()
});
}
私の PC ではtest bench_to_hex_format ... bench: 3,796 ns/iter (+/- 598)
でした。ここからもうちょっと速くする方法を考えてみよう。
###ルックアップテーブル
変換処理をあらかじめ実行しておこうというアイディアですね。
#[bench]
fn bench_to_hex_lut(b: &mut Bencher) {
let data: [u8; 4] = [0b01011110, 0b01101011, 0b01001011, 0b00001101];
let lut: Vec<String> = (0u8..=255)
.map(|n| format!("{:02X}", n)).collect();
b.iter(|| {
data.iter()
.map(|&n| lut.get(n as usize).unwrap().to_owned())
.collect::<String>()
});
}
結果はtest bench_to_hex_lut ... bench: 2,404 ns/iter (+/- 458)
です。最初の format! マクロバージョンより 1.6 倍くらいは速くなりました。
いや、まだいけるでしょ!
###Unsafe Rust
前述のルックアップテーブルとポインタ操作でゴリゴリやっていきます。
#[bench]
fn bench_to_hex_unsafe(b: &mut Bencher) {
let data: [u8; 4] = [0b01011110, 0b01101011, 0b01001011, 0b00001101];
let lut: Vec<u8> = (0u8..=255)
.map(|n| format!("{:02X}", n).into_bytes())
.flatten()
.collect();
b.iter(|| {
let mut result: [u8; 8] = [0u8; 8];
data.iter().enumerate().for_each(|(i, &n)| {
unsafe {
ptr::copy_nonoverlapping(
lut.as_ptr().offset(n as isize * 2),
result.as_mut_ptr().offset(i as isize * 2),
2
);
}
});
unsafe {
str::from_utf8_unchecked(
slice::from_raw_parts(result.as_ptr(), 8)
).to_owned()
}
});
}
結果はtest bench_to_hex_unsafe ... bench: 273 ns/iter (+/- 18)
でした。最初の format! マクロバージョンより 14 倍くらいは速くなりました。やったぜ!
###追記
上記 Unsafe Rust のコードでは公平を期す?ためにto_owned()
でString
に変換していますが、&str
のままで良いのならさらに速くなります。ただ、to_owned()
を消すと何も処理しないように最適化されて、ベンチマークの結果が 2ns とか異常な結果になってしまうので、test::black_box()
で包んで最適化されないようにしてやる必要があります。
#[bench]
fn bench_to_hex_unsafe_str(b: &mut Bencher) {
// 省略
b.iter(|| {
// 省略
test::black_box(unsafe {
str::from_utf8_unchecked(
slice::from_raw_parts(result.as_ptr(), 8)
)
})
});
}
結果はtest bench_to_hex_unsafe_str ... bench: 89 ns/iter (+/- 6)
でした。format! マクロバージョンの 42 倍。Awesome!!
##まとめ
test bench_to_hex_format ....... bench: 3,796 ns/iter (+/- 598)
test bench_to_hex_lut .......... bench: 2,404 ns/iter (+/- 458)
test bench_to_hex_unsafe ....... bench: 273 ns/iter (+/- 18)
test bench_to_hex_unsafe_str ... bench: 89 ns/iter (+/- 6)