今日の内容
- write
- write_all
- writeとwrite_allの違い
- BufWriterの役割: 頻繁なシステムコールを回避する
はじめに
前回は、借用と参照について学びました。
今回はwrite
とwrite_all
の違いを細かく説明します。
ファイル操作を行う際、Rustにはいくつかの便利なメソッドが用意されています。その中で、writeとwrite_allの違いを知っているでしょうか?初心者の方が陥りがちなのは、「とりあえずwriteを使っておけばいいだろう」と考えることです。しかし、実際には write_allを使うことが推奨されるケースが多い のです。この記事では、その理由を詳しく解説し、さらに効率的なファイル操作のためのBufWriterについても触れていきます。
writeとwrite_allの違い
writeとは?
writeは渡されたデータの一部または全部をファイルに書き込むメソッドです。
ただし、 一度の呼び出しで必ずしも全てのデータが書き込まれるわけではありません。
これは、低レベルなファイル操作(システムコール)に依存しており、環境やシステムの状態によって書き込みが途中で止まる場合があるためです。
例: writeで部分的な書き込みが発生するケース
use std::fs::File;
use std::io::Write;
fn main() {
let mut file = File::create("ex.txt").unwrap();
// 10バイトのデータを書き込む
let bytes_written = file.write(b"HelloWorld").unwrap();
println!("書き込まれたバイト数: {}", bytes_written);
}
このコードでは、OSの制約や負荷により、writeがすべてのデータを書き込む前に停止し、戻り値として 実際に書き込まれたバイト数 を返す場合があります。例えば、10バイト中5バイトしか書き込めなかった場合、残りの5バイトを手動で再度書き込む必要があります。
write_allとは
一方、write_allは、 渡されたデータ全体を必ず書き込むことを保証するメソッド です。
内部でwriteを繰り返し呼び出し、すべてのデータが書き込まれるまで処理を継続します。 そのため、残りのデータを書き込む処理を手動で行う必要がありません。
例: write_allの使用
use std::fs::File;
use std::io::Write;
fn main() {
let mut file = File::create("ex.txt").unwrap();
// 全てのデータが確実に書き込まれる
file.write_all(b"HelloWorld").unwrap();
}
上記の例では、「書き込み漏れ」を気にする必要はありません。write_allがデータの完全性を保証してくれるからです。
なぜwrite_allを使うべきなのか
write_allを使う最大の理由は、 データの完全性が保証されるから です。
writeを使う場合、書き込み漏れの可能性を考慮して、エラーチェックや残りのデータを再試行する処理を自前で実装する必要があります。しかし、これはコードを複雑にし、バグの温床になることがあります。
開発効率の向上
- writeを使う場合: 書き込み結果を確認し、未書き込みのデータを追跡して再試行するロジックを実装する必要がある
- write_allを使う場合: データの完全性が保証されるため、コードがシンプルになる
BufWriterの役割
頻繁なシステムコールを回避する
ここでさらに効率的なファイル操作を実現するための機能、 BufWriter について説明します。
システムコールのコスト
ファイル操作では、writeやwrite_allが呼ばれるたびにOSに対して システムコール が発生します。システムコールとは、プログラムがOSに処理を依頼する仕組みですが、これには一定のオーバーヘッド(遅延)が伴います。
例えば、以下のように小さなデータを繰り返し書き込む場合を考えます:
use std::fs::File;
use std::io::Write;
fn main() {
let mut file = File::create("ex.txt").unwrap();
// 全てのデータが確実に書き込まれる
for _ in 0..1000 {
file.write_all(b"a").unwrap();
}
}
上記のコードでは、1000回のシステムコールが発生しています。これにより、処理速度が大幅に低下する可能性があります。
BufWriterの仕組み
BufWriterは、書き込むデータを 一時的にメモリ上のバッファ(キャッシュ)に蓄え、一度にまとめてディスクに書き込む 仕組みを提供します。これにより、システムコールの回数が減少して、処理効率が上がります。
BufWriterの使用例
use std::fs::File;
use std::io::{BufWriter, Write};
fn main() {
let file = File::create("ex.txt").unwrap();
let mut writer = BufWriter::new(file);
for _ in 0..1000 {
// バッファに蓄積されるだけ
writer.write_all(b"a");
}
// バッファに溜まったデータをまとめて書き込む
writer.flush().unwrap();
}
- write_allが呼ばれても、実際にディスクへの書き込みは発生せず、バッファにデータが蓄えられるだけ
- flushが呼び出されたとき、バッファに蓄えられたデータが一度に書き込まれ、システムコールの回数が大幅に減少する
Column
実際にプログラム全体の実行時間を表示させる
以下は、BufWriterを使う場合と、使わない場合のプログラムにおける実行速度を比較するサンプルプログラムです。1万個の素数をファイルに書き込んでいます。
use std::fs;
use std::io::Write;
use std::time;
static N:usize = 10000;
fn main() {
let mut primes: [i32; N + 1] = [0; N + 1]; // N + 1個の配列
let fp = fs::File::create("prime.txt");
if let Ok(mut file) = fp {
let primes = evaluate_primes(&mut primes);
let now = time::Instant::now(); // ファイルへの書き込み開始時間を取得
file.write_all(b"Prime number\n").unwrap();
for i in primes {
let byte = format!("{}\n", i);
file.write_all(byte.as_bytes()).unwrap();
}
file.flush().unwrap();
println!("{:?}", now.elapsed()); // 経過時間を表示
} else {
eprintln!("Failed to create file")
}
}
// 要素番目が素数であるベクタを返す
fn evaluate_primes (a: &mut [i32; N+1]) -> Vec<i32> {
// 素数候補
for i in 2..=N { a[i] = 1; }
// iを2から開始し、√Nまでループ
let limit = (N as f32).sqrt() as usize;
for i in 2..=limit {
if a[i] == 1 {
for j in (2*i..=N).step_by(i) {
a[j] = 0;
}
}
}
let mut primes: Vec<i32> = Vec::new();
for i in 0..=N {
if a[i] != 0 { primes.push(i as i32); }
}
primes
}
このプログラムの実行速度は2.5ms程度でした。
use std::fs;
use std::io::{self, Write};
use std::time;
static N:usize = 10000;
fn main() {
let mut primes: [i32; N + 1] = [0; N + 1]; // N + 1個の配列
let fp = fs::File::create("prime.txt");
if let Ok(file) = fp {
let mut writer = io::BufWriter::new(file);
let primes = evaluate_primes(&mut primes);
let now = time::Instant::now(); // ファイルへの書き込み開始時間を取得
writer.write_all(b"Prime number\n").unwrap();
for i in primes {
let byte = format!("{}\n", i);
writer.write_all(byte.as_bytes()).unwrap();
}
writer.flush().unwrap();
println!("{:?}", now.elapsed()); // 経過時間を表示
} else {
eprintln!("Failed to create file")
}
}
// 要素番目が素数であるベクタを返す
fn evaluate_primes (a: &mut [i32; N+1]) -> Vec<i32> {
// 素数候補
for i in 2..=N { a[i] = 1; }
// iを2から開始し、√Nまでループ
let limit = (N as f32).sqrt() as usize;
for i in 2..=limit {
if a[i] == 1 {
for j in (2*i..=N).step_by(i) {
a[j] = 0;
}
}
}
let mut primes: Vec<i32> = Vec::new();
for i in 0..=N {
if a[i] != 0 { primes.push(i as i32); }
}
primes
}
このプログラムの実行速度は170µs(= 0.170ms)程度でした。
おわりに
今回は、writeとwrite_allの違いと、BufWriterの仕組みについて説明しました。
ご精読ありがとうございました。