この記事は セゾンテクノロジー Advent Calendar 2024 8日目の記事です。
シリーズ2は HULFT10 のエンジニアによる投稿をお届けします。
お仕事でRustを書いてて気になったことを少しだけ調べて見たので書いてみます👀
動的バッファ
I/Oを伴うプログラムを書くと、なんだかんだバッファが必要になると思います。
普段は固定長バッファを用意しといて使い回せば良いのですが、大きいサイズのバッファが必要になったりでヒープにバッファを取りたい(スタックに取りたくない)ケースがあると思います。
Rustでの動的バッファ
Rustのバッファは(多分)他言語と同じくバイト列&[u8]
で、これをヒープに取る場合はVec<u8>
を作るのが主流かな~と勝手に思ってます。
じゃあ実際どうやって作るのか調べてみたところ、それっぽいのが2つありました。
Vec::with_capacity
&Vec::set_len
1つ目がVec::with_capacity
とVec::set_len
を使って作る方法
let len = 128;
let mut buf: Vec<u8> = Vec::with_capacity(len);
// lenはまだ0のままなので、u8スライスも長さ0
assert_eq!(buf.len(), 0);
assert_eq!(buf.as_slice().len(), 0);
// set_lenで長さを伸ばしてあげる必要がある
unsafe {
buf.set_len(len);
}
assert_eq!(buf.len(), len);
assert_eq!(buf.as_slice().len(), len);
Vec::set_len
を使う都合、unsafe
ブロックを合わせて使う必要があります。
大したことはしてないんですがそれでもunsafe、なんか嫌ですね(個人的感想)
vec!
もう一つ、vec!
マクロでも作れます。
let len = 128;
let mut buf = vec![0u8; len];
assert_eq!(buf.len(), len);
assert_eq!(buf.as_slice().len(), len);
この場合、unsafe
が不要です😀
ただ、初期化用の要素を指定してその値でクリアしてることからオーバーヘッドがありそうなのが気になります。
ベンチマーク
ということでネタ作りも兼ねてベンチ取ってみました。
測定にはcriterion.rsを使ってみます。初なので使い方間違ってたらごめんなさい。
use criterion::{criterion_group, criterion_main, Criterion};
const LEN: usize = 10485760;
fn bench_buffer(c: &mut Criterion) {
let mut group = c.benchmark_group("buffer");
group.bench_function("vec_with_capacity", |b| {
b.iter(|| {
let mut buf: Vec<u8> = Vec::with_capacity(LEN);
unsafe {
buf.set_len(LEN);
}
})
});
group.bench_function("vec_macro", |b| {
b.iter(|| {
let mut _buf = vec![0u8; LEN];
})
});
}
criterion_group!(benches, bench_buffer);
criterion_main!(benches);
結果
buffer/vec_with_capacity
time: [117.22 ps 118.46 ps 119.88 ps]
Found 7 outliers among 100 measurements (7.00%)
4 (4.00%) high mild
3 (3.00%) high severe
buffer/vec_macro time: [0.0000 ps 0.0000 ps 0.0000 ps]
Found 12 outliers among 100 measurements (12.00%)
4 (4.00%) high mild
8 (8.00%) high severe
マクロを使ったほうが早い…?というか一瞬で終わってる……
ちょっと意外
再測定
何度か再測定しても同じ結果だったので、1クリアするケースを追加して比較してみます。
use criterion::{criterion_group, criterion_main, Criterion};
const LEN: usize = 10485760;
fn bench_buffer(c: &mut Criterion) {
let mut group = c.benchmark_group("buffer");
group.bench_function("vec_with_capacity", |b| {
b.iter(|| {
let mut buf: Vec<u8> = Vec::with_capacity(LEN);
unsafe {
buf.set_len(LEN);
}
})
});
group.bench_function("vec_macro", |b| {
b.iter(|| {
let mut _buf = vec![0u8; LEN];
})
});
// 1クリアするケース
group.bench_function("vec_macro_one", |b| {
b.iter(|| {
let mut _buf = vec![1u8; LEN];
})
});
}
criterion_group!(benches, bench_buffer);
criterion_main!(benches);
buffer/vec_with_capacity
time: [115.12 ps 116.00 ps 117.17 ps]
Found 15 outliers among 100 measurements (15.00%)
9 (9.00%) high mild
6 (6.00%) high severe
buffer/vec_macro time: [0.0000 ps 0.0000 ps 0.0000 ps]
Found 12 outliers among 100 measurements (12.00%)
4 (4.00%) high mild
8 (8.00%) high severe
buffer/vec_macro_one time: [115.61 ps 116.78 ps 118.23 ps]
Found 11 outliers among 100 measurements (11.00%)
1 (1.00%) high mild
10 (10.00%) high severe
1クリアだと遅くなりました。0クリアだとよしなに最適化してくれてるんでしょうか?
まとめ
動的バッファ作成にはvec!
で0クリアが良いのかもしれない…
けどpsレベルの差なので何でも良い気がする。
余談
Vec
のままだとpushとかの操作でlenが変えられちゃうのが若干気になるので、Boxにしておいたほうがいいかも。私は面倒でやってないですが。
let len = 128;
let mut buf: Box<[u8]> = vec![0u8; len].into_boxed_slice();
assert_eq!(buf.len(), len);
assert_eq!(buf.as_ref().len(), len);