1. なぜ入出力はこんなに遅いのか?
入出力(I/O)の「遅さ」はよくある疑問です。僕らのコードは「考える」よりもデータを「待つ」時間のほうが長くなることが多い。なぜ「倉庫」への往復が「キッチン」での作業より遅いのか、見ていきましょう。
ハードウェアの物理的制約 (Hardware Limitations)
ハードディスク(HDD)。 機械部分:プラッタが回転し、ヘッドが動く。シーク時間や回転待ちが必要で、高いレイテンシが発生します。
ソリッドステートドライブ(SSD)。 HDDより速いし機械部分はないけど、書き込みやセルのウェアレベリングで操作が瞬時には終わりません。
ネットワーク。 帯域とレイテンシ、ルータや経路に依存します。ギガビット回線でもリモートサーバーへの往復はミリ秒単位で、CPUのナノ秒とは比べ物になりません。
OSのオーバーヘッド (OS Overhead)
- アクセス権のチェック。 プロセスがファイルを読み書きできるか確認されます。
- ファイルデータの探索。 ファイルシステムがフラグメントを辿ってデータを見つけます。
- バッファリングとキャッシュ。 OSは効率のためにバッファやキャッシュを管理します。
- コンテキストスイッチ。 プロセスがI/O待ちの間にCPUが切り替わり、それ自体がコストになります。
大きな隔たり:CPU速度 vs I/O速度
- CPU操作: 0.2 – 0.5 ナノ秒
- RAMからの読み取り: 10 – 100 ナノ秒
- SSDからの読み取り: 50 – 100 マイクロ秒
- HDDからの読み取り: 5 – 10 ミリ秒
- ネットワークリクエスト: 10 – 100 ミリ秒以上
差は桁違いです。もし文字ごとに「配達員」を呼んでいたら(I/O)、どれだけタイピングが速くても遅く感じるでしょう。データはブロック単位でまとめて取ってくるほうが遥かに効率的です。
2. ファイルの中で実際に何が起きているか?
ファイル操作時のコマンドチェーンはこうなります:
flowchart TD
A[あなたのC#コード] --> B[.NET FileStream]
B --> C[OS: Windows / Linux / Mac]
C --> D[ファイルシステム: NTFS, ext4, APFS]
D --> E[デバイスドライバ]
E --> F[物理ストレージ: HDD / SSD]
- あなたのコードが例えば File.ReadAllText(path) を呼びます。
- .NET は内部で FileStream、バッファ、システムコールを使います。
- OS がキャッシュやキューを管理します。
- ファイルシステム がファイルのブロックを見つけます。
- ドライバ がデバイスとやりとりします。
- ストレージ が物理操作を実行します。
各レイヤーがオーバーヘッドを追加します。ボトルネックは多くの場合、物理的なストレージ側にあります。
3. 例:実際の遅いコード
アンチパターン:ReadByte()で1バイトずつ読む。
// ❌ バイト単位でファイルを非効率に読む
using FileStream fs = new FileStream("bigfile.txt", FileMode.Open);
int currentByte;
while ((currentByte = fs.ReadByte()) != -1)
{
// バイトで何かをする
}
なぜダメか?各ReadByte()呼び出しはストリームへの別々のアクセスです。大きなファイルだと呼び出しが何百万回にもなり、システムは有用な仕事よりオーバーヘッドに時間を使います。
正しいやり方はブロックで読むこと:
// ✅ 大きなブロックで効率的にファイルを読む
byte[] buffer = new byte[4096]; // 4KB — 標準的なバッファサイズ
int bytesRead;
using FileStream fs = new FileStream("bigfile.txt", FileMode.Open);
while ((bytesRead = fs.Read(buffer, 0, buffer.Length)) > 0)
{
// 受け取ったデータブロックを処理する
}
大きなチャンクで読むことでOSやディスクがキャッシュやキューを効率的に使えるようになり、実行時間が何倍も短くなります。
4. 実際のアプリケーションへの影響
ユーザーインターフェース(UI)。 ブロッキングなI/Oはウィンドウを「フリーズ」させます。操作はバックグラウンド/非同期に移し、メインスレッドをブロックしないようにするのが重要です。
ウェブサーバーとDB。 サーバーは常に読み書きします。遅いディスクやネットワークはサービス全体を遅くします。バッファリング、接続プール、非同期I/Oがスループットの鍵です。
ビッグデータ。 ギガバイト・テラバイト単位だと小さな非効率がスケールして大問題に。ブロックサイズ、シーケンシャルアクセス、ストリーム処理が解決策になります。
ゲーム。 レベルやリソースの長いロードはI/Oが原因です。アセットの適切なパッケージングと大きなチャンクでの読み取りでロード時間を短縮できます。
5. 初心者にありがちなミス
よくあるミスは、大きなファイルを行ごとやバイトごとに読み込むこと(ReadByteなど)やバッファを小さくしすぎること(例えば 256 バイト)。システムコールの回数が増え、パフォーマンスが落ちます。
逆の問題もあります:巨大なファイルを丸ごと File.ReadAllBytes で読み込もうとして OutOfMemoryException を起こす、というパターン。現実的な「黄金律」は適切なブロックサイズ(多くの場合 4–8 KB 以上、負荷プロファイルによる)とストリーム処理を使うことです。
GO TO FULL VERSION