
2026/03/02 4:25
C言語の「ファイルAPI」は、そのシンプルさ、移植性、制御性が高く評価されています。 | 機能 | 重要性 | |-----|--------| | **標準ライブラリのみ** | 外部依存関係なし―`<stdio.h>`はすべてのCコンパイラに付属しています。 | | **移植可能な動作** | `fopen`、`fread`、`fprintf`などがWindows、Linux、macOS、組み込みシステム等で同一に振る舞います。 | | **双方向I/Oモード** | テキストモードは改行変換を自動的に処理し、バイナリモードでは予期せぬ変換無しに生のバイトを扱えます。 | | **ストリーム抽象化** | ファイルストリーム(`FILE *`)は不透明オブジェクトであり、実装側がバッファリングやキャッシュ、OS固有最適化を行ってもユーザーコードには影響しません。 | | **バッファ付き/非バッファ付き** | `setvbuf`でバッファリング戦略を細かく制御でき、性能とレイテンシのバランスを取れます。 | | **エラーハンドリング** | 関数は明確な戻り値(`NULL`、`EOF`)を返し、`errno`を設定します。呼び出し側が適切に対処できます。 | | **アーキテクチャ横断の移植性** | APIはエンディアン、ワードサイズ、ファイル記述子の違いを抽象化しています。 | | **拡張性** | `freopen`、`tmpfile`、`fseek`など新機能が追加されても既存コードに影響しません。 | 簡潔に言えば、C言語のファイルAPIは「標準ライブラリが利用できる任意のプラットフォームで信頼性を保ちつつ、一貫した低レベルインターフェース」を提供します。このシンプルさ・移植性・制御性の組み合わせこそ、多くの人々が「システムプログラミング言語におけるファイルI/Oで最高」と考える理由です。
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
著者は、ファイルI/Oが常に手動でのパースとシリアライズを必要とするという一般的な信念、特に非常に大きなファイルを扱うメモリ制約付きシステムにおいて、その考え方に挑戦します。
- 多くの言語では、ファイルは副次的に扱われ、
、read()
などでアクセスされるか、重いシリアライズライブラリが使用されます。write() - C の
はプログラムにファイルデータを配列として表示させ、ページは必要時にロードされるため、テラバイト規模のファイルでも全体を RAM に保持することなく処理できます。OS がマップされたページをキャッシュし、必要に応じて除外するので、明示的なキャッシングロジックは不要です。mmap() - 他の言語では通常、チャンク単位で読み込み、パースし、処理して書き出す冗長な操作が必要になり、順序付き操作に制限があります。メモリマッピングが別途利用できる場合でも、それは
/read()
のラッパーに過ぎず、複雑なデータ型の手動パースを依然として要求します。write() - C はビルトインのバイナリ形式処理やエンディアンチェックがなく、開発者は自らパースしなければならないためです。Python の
は不安全ながらコードとデータを混在させるために人気があります。pickle - ファイルシステムは NoSQL データベースのように機能しますが、言語ラッパー(例:
)は最小限であり、開発者は SQLite のような追加データベースを層化することになります。リレーショナルデータベースはシリアライゼーションオーバーヘッドと別の SQL 言語を追加し、結果としてカスタムキー・バリューストアや複雑なインデックス構造が必要となります。readdir()
主なメッセージは、ファイルI/O が常に手動でパース/シリアライズを必要とするという仮定は誤りであり、OS レベルのメモリマッピングなどの機構が制約付きシステム上で大きなファイル処理を劇的に簡素化できるということです。
本文
2026年2月28日(プログラミング)(愚痴)
優れたプログラミング言語は数多く存在しますが、ファイルに関しては常に「後回し」扱いになりがちです。
通常は
read()・write() だけでなく、ある種のシリアライゼーションライブラリを呼び出すことになります。
C言語では、ファイルへのアクセスをメモリ上のデータへアクセスするのと全く同じ方法で行うことができます:
#include <sys/mman.h> #include <stdio.h> #include <stdint.h> #include <fcntl.h> #include <unistd.h> int main(void) { /* 1000 個の unsigned int を格納したファイルを作成/開く。初期値はすべてゼロ */ size_t len = 1000 * sizeof(uint32_t); int file = open("numbers.u32", O_RDWR | O_CREAT, 0600); ftruncate(file, len); /* ファイルをメモリにマップする。 */ uint32_t *numbers = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, file, 0); /* 処理例: */ printf("%d\n", numbers[42]); numbers[42]++; /* 後始末 */ munmap(numbers, len); close(file); }
メモリマッピングは「ファイルを一括でメモリへ読み込む」わけではありません。
ファイルが RAM に収まらない場合でも、必要に応じてデータがロードされるため、テラバイト級のファイルを開くのに何時間もかかることはありません。
すべてのデータ型で機能し、自動的にキャッシュされます。
システムが他のメモリ需要に応じてこのキャッシュをクリアするため、メモリ管理も楽です。
しかしほとんどの言語では
read() で小さなチャンクを読み込み、解析・処理・シリアライズし、最後に write() してディスクへ戻す必要があります。この手順は機能しますが冗長で、むやみに「連続アクセス」しかサポートできません。実際、コンピュータは何十年もテープを使っていないのです。
メモリマッピングが利用可能ならば、それはバイト配列に限定されるだけですが、依然として明示的なパース/シリアライズが必要です。
結局のところ、
read() と write() を呼び出すよりも優れた方法ではなく、単なる「便利な呼び方」に過ぎません。
多くの言語はカスタムアロケータやゲッタ関数などを既にサポートしているので、ファイルアクセスをより良くする手段を追加することは十分に可能だと考えられます…しかし(私が知る限り)バイナリ形式を指定し、それをそのまま利用できるのは C のみです。
C の実装もそれほど優秀ではありません。メモリマッピングにはページフォルトや TLB フラッシュなどのオーバーヘッドがありますし、エンディアン対応は一切行いませんが、「何もしない」より勝てるものは少ないでしょう。
もちろん解析と検証を行う必要がある場合もありますが、データがディスクから出るたびに必ずそれを行うべきではありません。
メモリ不足になることは非常に一般的で、すべてのデータを RAM に読み込むことが不可能です。
コードを複雑化せずにデータをオフロードできる機能はとても有用です。
Python の
pickle を見ればわかります:完全に非安全なシリアライゼーション形式です。ファイルを読み込むだけで、実際には数値が欲しかったとしてもコードが実行されてしまうことがあります…それでも広く使われ続けています。Python の「コードとデータの混在」モデルに合っているからです。
多くのファイルは信頼できるものではありません。
ファイル操作自体も同様に軽視されています。
ファイルシステムは本来 NoSQL データベースそのものですが、C の
readdir() などをラップしただけの API がほとんどです。
その結果、SQLite など別途データベースを乗せて使うケースが多いですが、リレーショナルデータベースはプログラムに完全にはフィットしません。
さらに SQL はファイルよりも悪化します。すべてのデータをシリアライズする必要に加え、アクセスするだけで別言語のコードを書かなければなりません!
多くのプログラマはキーバリューストアとして使い、自前のインデックスを実装し、奇妙な三重ネストのデータベースを作ります。
つまりタイトルに対する答えとして、私は「悪い仮定」が原因だと考えます。すなわち、ファイルから読み込まれるデータは別処理で解析が必要であり、ディスクへ書き出すデータも標準フォーマットにシリアライズして送るべきという前提です。
しかしメモリ制約のあるシステムではそれは真実ではありません。100 GB のファイルを扱う際には、ほぼすべてのシステムがメモリ制限に直面します。