LoginSignup
12
5

More than 5 years have passed since last update.

Rust でバイト列を16進数な文字列に変換する(Hex dump in Rust)

Last updated at Posted at 2019-02-22

環境

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)

12
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
5