CodeGym /コース /C# SELF /データのバッファリングの原則

データのバッファリングの原則

C# SELF
レベル 41 , レッスン 1
使用可能

1. はじめに

あなたが鉛筆で手紙を書いていると想像して。消しゴムがすごく小さくて、1単語分しか消せないとする。消さないと続けられない。もっと一度にたくさん消せたらいいよね?バッファリングはだいたいその「消しゴムの束」みたいなもの:一度に大きな塊のデータを扱えて、ちまちま処理しなくて済む。

プログラミングでのバッファリングは、ディスクに読み書きされる前にデータを一時的にメモリ(「バッファ」)にためておくこと。洗濯籠に例えると、靴下を1週間ためてまとめて洗うようなもの。一つずつ洗うより時間(とリソース)が節約できる。

I/O操作

ハードディスク、SSD、フラッシュメモリへのアクセスはCPUにとって最も遅い操作の一つ。RAMはおよそ千倍速い!だから毎回 WriteRead を呼んで即座にディスクに行ってたら、プログラムは512MBのRAMの古いノートのWindows XPみたいに遅くなるよ。

バッファリング は物理的なディスクアクセス回数を減らしてパフォーマンスを上げるためにあるんだ。

2. 入出力でのバッファリングの仕組み

バッファ は単に一時的にデータを置くためのメモリ領域。仕組みはこんな感じ:

ファイル書き込み時:

  • あなたのコードがいくつかの Write() を呼ぶ。
  • データはまずバッファに積まれる。
  • バッファが満杯になったり操作を終える必要があるとき、一気に大きな塊でディスクに書き込まれる。

ファイル読み取り時:

  • 少しデータを読みたいと要求する。
  • システムはファイルから一度に大きな塊を読み、バッファに置く。
  • 次に呼ぶときにはデータが既にバッファにあるのでディスクアクセスは不要。

結果として:

  • ディスクへのアクセス回数が減る。
  • 読み書きが速くなる。

3. .NETでのバッファリング: どこで使われているか

.NETでは多くのI/Oストリームがデフォルトでバッファリングを使っているよ:

  • StreamWriter / StreamReader
  • FileStream
  • BufferedStream
  • さらには Console.Out も!

ただし バッファサイズや使い方はカスタマイズできる(し、しばしばすべき)ことに注意してね。

なぜ重要なのか?

大量のデータ(ログファイル、データベース、マルチメディア処理)を読み書きするとき、適切なバッファリングはプログラムを何倍も速くできる。バッファリングが無ければ、良いCPUでもデータ待ちで「あくび」をすることになる、まるで雨の中の猫みたいに。

4. バッファリング無しの単純な例

まず、もし1バイトずつ書いていたらどうなるか(こんなことはやらないで!):

string path = "slowfile.txt";
using (FileStream fs = new FileStream(path, FileMode.Create))
{
    for (int i = 0; i < 100000; i++)
    {
        fs.WriteByte((byte)'A'); // 1バイトずつ書いてる!
    }
}
Console.WriteLine("完了! (でもめっちゃ遅い)");

この例では100000回の実際のディスクアクセスが発生する!SSDでも「なんでそんなことするの?」って言うよ。

どのくらいのバッファサイズを選ぶか?

ケースバイケース:

  • .NETのデフォルトは 4KB や 8KB が多い。
  • 大きなファイル(100MB以上)なら 16KB、64KB、あるいは1MB でも良いことが多い。
  • 大きすぎるバッファもダメ:メモリの無駄遣いで、効果が頭打ちになることがある。

黄金律: 推測せずに計測(profiling)してみよう!バッファ増加で10倍速くなることもあれば、ほとんど変わらないこともある。

5. バッファリング: I/Oを速くする

ファイルにおける「バッファリング」はまとめ買いの親戚みたいなもの。一個ずつバナナ運ぶんじゃなくて箱ごと運ぶんだ。

.NETのほとんどのI/Oストリームはデフォルトでバッファリングを使うけど、例外もある:自分で FileStream のパラメータを細かく制御している場合や、極端に小さいバッファやバッファなしの環境だと問題が出る。

バッファリングはどうやってI/Oを速くするの?

大きなブロックで読み書きすると、OSは操作を最適化して複数の操作をまとめたり、ディスクへのアクセス数を減らしたり、次のデータを先読み(prefetch)したりできる。

図示: ファイル読み取り — バッファ無しとバッファあり

方式 アクセス回数 時間(概算)
1バイトずつ読み 10 000 000 10分
4096バイトずつ読み 2 500 5秒

数値は概算だけど、桁が違うってことは伝わるよね!

6. FileStream と .NETでのバッファリング

クラス FileStream はファイル操作の最も低レベルなツールで、最大の制御を与えてくれるが注意も必要。コンストラクタでバッファサイズを指定できる:

// FileMode.Open: 既存ファイルを開く
// FileAccess.Read: 読み取り
// FileShare.Read: 他からの読み取りを許可
// bufferSize: バッファサイズ(バイト)
var fs = new FileStream("bigfile.txt", FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 8192)

    // ファイル操作が速くなる

デフォルトで FileStream は 4096 バイトのバッファを使うことが多いけど、ファイルが大きければ 16KB、64KB、1MB など大きめにしても良い。

アドバイス: あまりに大きなバッファは避ける

巨大なバッファを使うとRAMを大量に消費して速度向上が見られないことがある — 現代のOSはブロックをキャッシュするのも得意だからね。一般的には 4KB から 128KB が多くの用途で適切。

どんな時にパフォーマンス問題が特に顕著になる?

  • 多数の小さいファイルをコピーするとき(例: 写真など)。
  • 大きなファイルを小さなチャンクで読み書きするとき(1バイトずつ、あるいはバッファ無しで1行ずつ)。
  • 同時に多数のファイルを開くとき(例: スクリプトが全ログを検索する場合)。
  • ネットワーク共有ドライブを使うとき(遅延 + ネットワーク負荷)。
  • 大量処理(アーカイブ作成、バックアップ、インポート/エクスポート)。

7. 古いやり方と速いやり方でファイルをコピーする

実際に速度に影響するアプローチを比べてみよう。

とても遅い例:

// ❌ ダメ — 1バイトずつ読んで書いてる
using FileStream source = new FileStream("source.bin", FileMode.Open);
using FileStream dest = new FileStream("dest.bin", FileMode.Create);

int b;
while ((b = source.ReadByte()) != -1)
{
    dest.WriteByte((byte)b);
}

かなり速い例:

// ✅ 良い — 大きなブロックで読み書き
byte[] buffer = new byte[16 * 1024]; // 16KB
int bytesRead;

using FileStream source = new FileStream("source.bin", FileMode.Open);
using FileStream dest = new FileStream("dest.bin", FileMode.Create);

while ((bytesRead = source.Read(buffer, 0, buffer.Length)) > 0)
{
    dest.Write(buffer, 0, bytesRead);
}

超速(かつ簡単):

// 🚀 File.Copy — 内部で最適化されたバッファリングを使ってる
File.Copy("source.bin", "dest.bin");

なぜブロック処理を理解する必要があるかって?たまに単純にコピーするだけじゃなくて、読みながらフィルタしたり暗号化したり合計を計算したりする必要が出るからだよ。

実行時間の比較

見せるための実験データ(一例):

方法 ファイルサイズ 1GB 時間(概算)
1バイトずつ 1GB 約30分
4KBブロック 1GB 約20秒
組み込みの File.Copy 1GB 約5秒

重要なファイルやシステムSSDでこのテストをやるなよ — ディスクとあなたの神経が「休戦協定」を結ぶかもね。

8. 役に立つ細かい点

「遅さ」の他の原因は?

ディスクの物理特性や不適切なブロックサイズ以外にも遅くなる理由がある:

  • ファイルを何度も開閉する(1回だけ開いて処理して閉じる方がいい)。
  • I/Oをメインスレッドで実行するとUIが固まる(Windows Forms/WPF/MAUIなど)。
  • メモリ不足:OSがページをRAMとディスクの間でスワップし始めると二重に遅くなる。
  • アンチウイルス、Windowsの検索インデックス、バックグラウンドプロセス — ときどきファイルに絡んで見えない遅延を生む。

実務での適用

実プロジェクトでは: ログ処理、メディア処理、ドキュメント処理、クラウドストレージ、レポート集約、バックアップなどを作るなら、必ず「どうやって速いI/Oを実現するか」に直面する。バッファリング、大きなブロック、そして File.Copy のような既製ツールの活用はファイル処理の基本だよ。

面接で: 「なぜ1バイトずつファイルを読むのはアンチパターンか?」とか「大量ファイルのコピーを速くするにはどうする?」と聞かれることがある。バッファリングの経験と知識は自信を持って答えるのに役立つ。

実運用で: 何でも速かったのに、SSDからネットワークドライブに移したら急に遅くなった、あるいはOS更新後に問題が出た、ということはよくある。I/Oの仕組みが分かれば原因特定と最適化が早くできるよ。

I/Oを速くするための実用的なヒント

  • 常にバッファリングされたI/Oを使う(BufferedStreamFileStream のバッファ設定)。
  • 大きなブロックで読み書きする(4KB以上)。
  • ファイルの開閉を最小限に — 一回開いて処理してから閉じる。
  • 可能なら非同期メソッドを使う(ReadAsync, WriteAsync) — I/O自体を速くはしないけど、アプリが待たなくて済む。
  • 非常に大きなファイルを扱うなら Memory<T>, Span<T> を検討する。
  • 組み込み機能を信頼する: File.Copy, File.Move などは内部で最速のシステム呼び出しを使うことが多い。

.NETクラスでのバッファリング

誰がどのようにバッファリングしているかの小さな表:

クラス デフォルトでバッファするか バッファを設定できるか
FileStream
はい はい(コンストラクタで指定)
StreamWriter
はい はい(コンストラクタ経由)
StreamReader
はい はい
BufferedStream
いいえ(ラッパーとして動作) はい
BinaryWriter/Reader
はい いいえ

.NETで完全にバッファ無しで動くものはほとんどない — 非効率すぎるからね。

いつ手動でバッファをフラッシュする必要がある?

時々データがバッファに残っていて、今すぐディスクに書き込みたい場合がある。たとえばログを書いていて突然クラッシュしたとき。そんなときは .Flush() を呼ぶんだ:

using var fs = new FileStream("log.txt", FileMode.Append);
using var writer = new StreamWriter(fs);
writer.WriteLine("何か重要なこと");
writer.Flush(); // 今すぐバッファをディスクに落とす

Flush は「よし、全部片付けてディスクに入れよう!」って叫ぶようなもの。未書き込みのデータが本当に書き出されるよ。

9. 実践的な質問: よくあるミスと注意点

初心者がよくがっかりするのは「ファイルに書いたはずなのに空っぽ!」ってやつ。原因はデータがまだバッファに残っているから。プログラムが強くバッファリングしていて、すぐにはディスクに書かれない。これを避けるには Flush() を呼ぶか、ストリームを閉じて(Dispose())バッファを解放すること。

別の問題は巨大なバッファを割り当てすぎてシステムメモリが足りなくなり、アプリがもたつくこと。バッファは大きければ良いわけじゃない — 使いすぎに注意。

2
タスク
C# SELF, レベル 41, レッスン 1
ロック未解除
大きなブロックでファイルをコピーしよう
大きなブロックでファイルをコピーしよう
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION