この記事は、 セゾンテクノロジー Advent Calendar 2025 の記事です。
シリーズ2では HULFT・DataSpider の開発メンバーによる投稿をお届けします🕊️
12日目はHULFT10 for Container Servicesの開発者が担当します🙌
はじめに
皆さん、ファイルのseekとreadってどちらが速いと思いますか?
一般的に実際にデータを読まないseekの方が早そうですよね。
ですが、readよりseekの方が遅い。そんなケースがRustには存在します。
何が起きたか
以下のコードをご覧ください。seekとread、どちらが速いと思いますか?
use std::fs::File;
use std::io::prelude::*;
use std::io::{BufReader, SeekFrom};
const CHUNK: usize = 5;
fn read_chunk(reader: &mut BufReader<File>, buf: &mut [u8]) -> std::io::Result<bool> {
match reader.read(buf) {
Ok(0) => return Ok(false), // EOF
Ok(_) => Ok(true),
Err(e) => return Err(e),
}
}
// read版
pub fn bufreader_read(filename: &str) -> std::io::Result<()> {
let file = File::open(filename).unwrap();
let mut reader = BufReader::new(file);
let mut buf = [0u8; CHUNK];
// readしてからreadを繰り返す
while read_chunk(&mut reader, &mut buf)? {
read_chunk(&mut reader, &mut buf).unwrap();
}
Ok(())
}
// seek版
pub fn bufreader_seek(filename: &str) -> std::io::Result<()> {
let file = File::open(filename).unwrap();
let mut reader = BufReader::new(file);
let mut buf = [0u8; CHUNK];
// readしてからseekを繰り返す
while read_chunk(&mut reader, &mut buf)? {
reader.seek(SeekFrom::Current(CHUNK as i64)).unwrap();
}
Ok(())
}
普通に考えたらseekの方が早そうですよね。
では、性能を比較してみましょう。
先ほどのコードで1MBのファイル読み込みをしてみます。
性能比較はRustのクレートであるcriterion.rsで計測します。
| 計測対象の関数 | 平均実行時間 |
|---|---|
| bufreader_read | 1.0266 ms |
| bufreader_seek | 255.10 ms |
圧倒的にseekが遅いですね。255倍も遅いです。不思議です。
原因
原因は以下のBufReader::seekの公式ドキュメントに書いてありました。
https://doc.rust-lang.org/std/io/struct.BufReader.html#method.seek
原文
Seeking always discards the internal buffer, even if the seek position would otherwise fall within it.
日本語訳(機械翻訳)
シークは、シーク位置が内部バッファ内に含まれる場合でも、常に内部バッファを破棄します。
ざっくり解釈すると、シークするたびに内部バッファ全部捨てて、データを読み直してるらしいです。そりゃ遅いですよね![]()
改善策
改善策も以下のBufReader::seekの公式ドキュメントに書いてありました。
https://doc.rust-lang.org/std/io/struct.BufReader.html#method.seek
原文
To seek without discarding the internal buffer, use BufReader::seek_relative.
日本語訳(機械翻訳)
内部バッファを破棄せずにシークするには、BufReader::seek_relative を使用します。
BufReader::seek_relativeを使えばいいらしいです。
というわけで実際に使ってみて性能を比較してみます。コードはこちら。
// seek_relative版
pub fn bufreader_seek_relative(filename: &str) -> std::io::Result<()> {
let file = File::open(filename)?;
let mut reader = BufReader::new(file);
let mut buf = [0u8; CHUNK];
// readしてからseek_relativeを繰り返す
while read_chunk(&mut reader, &mut buf)? {
reader.seek_relative(CHUNK as i64).unwrap();
}
Ok(())
}
結果はこうなりました。
| 計測対象の関数 | 平均実行時間 |
|---|---|
| bufreader_read | 1.0266 ms |
| bufreader_seek | 255.10 ms |
| bufreader_seek_relative | 720.18 µs |
seek_relative版が一番速いことが分かります。マイクロ秒単位なのでかなり高速です。
最後に
割と直感的ではない仕様だと思うのでまんまとハマりました。
私はこの処理を製品コードに埋め込んで、テストのタイミングで「何か一部の処理が異常に遅くないか?」となりこの問題に気づけました。リリース前に検知できて良かったです。
皆さんもRustのBufReaderでreadやseekを繰り返すような処理を行う場合はご注意ください。
余談
今回はRustのBufReaderが内部バッファを破棄してしまうことが原因で遅くなりましたが、他にも同じような実装してる言語ってあるんでしょうか。気になったので今度調べてみようと思います。