一部のCプログラムが1つの巨大なソースファイルで記述されているのはなぜですか?


88

たとえば、過去のSysInternalsツール「FileMon」には、ソースコードが完全に1つの4,000行のファイルにあるカーネルモードドライバーがあります。これは、これまでに作成された最初のpingプログラム(〜2,000 LOC)でも同じです。

回答:


143

複数のファイルを使用するには、常に追加の管理オーバーヘッドが必要です。コンパイルとリンクの段階を分けてビルドスクリプトやメイクファイルを設定し、異なるファイル間の依存関係が正しく管理されていることを確認し、電子メールまたはダウンロードでソースコードを簡単に配布できるように「zip」スクリプトを作成します。オン。今日の現代のIDEは通常、その負担の多くを取りますが、最初のpingプログラムが作成された時点では、そのようなIDEは利用できませんでした。また、〜4000 LOCの小さなファイルの場合、複数のファイルを適切に管理するIDEがなければ、前述のオーバーヘッドと複数のファイルを使用する利点のトレードオフにより、単一ファイルのアプローチを決定できる場合があります。


9
「そして、〜4000 LOCほど小さいファイルの場合...」私は今、JS開発者として働いています。ファイルのコードがたった400行しかない場合、どれだけ大きくなるか不安になります!(しかし、私たちのプロジェクトには何十ものファイルがあります。)
ケビン

36
@Kevin:私の頭の1本の髪が少なすぎ、私のスープの1本の髪が多すぎます;-) JSの複数ファイルのAFAIKは、「最新のIDEのないC」のようにそれほど管理オーバーヘッドを引き起こしません。
ドックブラウン

4
しかし、@ Kevin JSはかなり異なる獣です。JSは、ユーザーがWebサイトを読み込むたびにエンドユーザーに送信され、ブラウザでキャッシュされていません。Cはコードを一度送信するだけでよく、その後、相手の人がそれをコンパイルし、コンパイルされたままになります(明らかに例外がありますが、それは一般的に期待されるユースケースです)。また、コメントで説明している「4000行は正常」プロジェクトの多くがそうであるように、Cのものはレガシーコードである傾向があります。
ファラプ

5
@Kevinでは、underscore.js(1700 loc、1つのファイル)と、配布されている他の無数のライブラリがどのように作成されているかを確認してください。Javascriptは、モジュール化とデプロイに関して、実際にはCとほとんど同じくらい悪いです。
Voo

2
@Pharap彼は、コードをデプロイする前にWebpackのようなものを使用するつもりだったと思います。Webpackを使用すると、複数のファイルで作業し、それらを1つのバンドルにコンパイルできます。
ブライアンマックラッチン

81

Cはモジュール化が苦手だからです。乱雑になり(ヘッダーファイルと#includes、extern関数、リンク時エラーなど)、持ち込むモジュールが増えるほど、複雑になります。

より現代的な言語は、Cの間違いから学んだため、モジュール化機能が優れており、コードベースをより小さくシンプルなユニットに簡単に分解できるためです。しかし、Cを使用すると、それ以外の場合は大量のコードと見なされるものを1つのファイルにまとめることを意味する場合でも、そのような問題をすべて回避または最小限に抑えることができます。


38
Cのアプローチを「間違い」として説明するのは不公平だと思います。それらは作成された時点で完全に賢明で合理的な決定でした。
ジャックエイドリー

14
モジュール化されたものはどれも特に複雑ではありません。コーディングスタイルが悪いと複雑になる可能性があります、理解や実装が難しくなく、「間違い」として分類されるものもありません。Snowmanの答えによると、本当の理由は、過去に複数のソースファイルに対する最適化がそれほど良くなく、FileMonドライバーには高いパフォーマンスが必要だからです。また、OPの意見に反して、これらは特に大きなファイルではありません。
グラハム

8
@Grahamコードの1000行を超えるファイルは、コードのにおいとして処理する必要があります。
メイソンウィーラー

11
@JackAidleyはまったく不公平はありません。何かが間違いであるということは、それが当時合理的な決定であったと言うことと相互排他的ではありません。不完全な情報と限られた時間を考えると、間違いは避けられません。
ジャレッド・スミス

8
Cのアプローチが間違いではないと主張する人は誰でも、一見10ライナーのCファイルが実際にすべてのヘッダー#include:dを備えた1万ライナーのファイルになりうることを理解できません。これは、「wc -l」で指定された行数がいくらであっても、プロジェクト内のすべてのファイルが少なくとも1万行であることを意味します。モジュール性のより良いサポートは、解析時間とコンパイル時間をごくわずかに簡単に削減します。
ジュスト

37

歴史的な理由とは別に、最新のパフォーマンス重視のソフトウェアでこれを使用する理由が1つあります。すべてのコードが1つのコンパイル単位にある場合、コンパイラはプログラム全体の最適化を実行できます。個別のコンパイル単位では、コンパイラは特定の方法(特定のコードのインライン化など)でプログラム全体を最適化できません。

リンカは、コンパイラができることだけでなく、確かにいくつかの最適化を実行できますが、すべてではありません。たとえば、最新のリンカは、複数のオブジェクトファイル間でさえ、参照されていない関数を除外するのに非常に優れています。他の最適化を実行できる場合もありますが、関数内でコンパイラーができることとは異なります。

単一ソースコードモジュールのよく知られた例の1つはSQLiteです。詳細については、SQLite Amalgamationページをご覧ください。

1。エグゼクティブサマリー

100を超える個別のソースファイルが連結され、「sqlite3.c」という名前の「統合」と呼ばれるCコードの単一の大きなファイルになります。統合には、アプリケーションがSQLiteを埋め込むために必要なすべてが含まれています。統合ファイルの長さは180,000行を超え、サイズは6メガバイトを超えます。

SQLiteのすべてのコードを1つの大きなファイルにまとめると、SQLiteの展開が簡単になります。追跡するファイルは1つだけです。また、すべてのコードが単一の翻訳単位にあるため、コンパイラーはプロシージャー間の最適化を改善して、マシンコードを5%〜10%高速化できます。


15
ただし、最新のCコンパイラは、複数のソースファイルをプログラム全体で最適化できることに注意してください(ただし、最初に個々のオブジェクトファイルにコンパイルする場合は異なります)。
デイビスラー

10
@Davislor典型的なビルドスクリプトを見てください:コンパイラーは現実的にそうするつもりはありません。

4
$(CC) $(CFLAGS) $(LDFLAGS) -o $(TARGET) $(CFILES)すべてを単一のsoudceファイルに移動するよりも、ビルドスクリプトを変更する方がはるかに簡単です。プロダクションターゲットのプロファイリングとデバッグをオフにする方法と同様に、変更されていないソースファイルの再コンパイルをスキップする従来のビルドスクリプトの代替ターゲットとして、プログラム全体のコンパイルを行うこともできます。すべてが1つの大きなヒープソースにある場合、そのオプションはありません。人々が慣れているものではありませんが、面倒なことは何もありません。
デイビスラー

9
@Davislorのプログラム全体の最適化/リンク時最適化(LTO)は、コードを個々のオブジェクトファイルに「コンパイル」するときにも機能します(「コンパイル」の意味によって異なります)。たとえば、GCCのLTOは、コンパイル時に個々のオブジェクトファイルに解析されたコード表現を追加し、リンク時に(存在する)オブジェクトコードの代わりにそのファイルを使用して、プログラム全体を再コンパイルおよびビルドします。したがって、最初のコンパイルで生成されたマシンコードは無視されますが、これは最初に個々のオブジェクトファイルにコンパイルするビルドセットアップで機能します。
ドリーマー

8
JsonCppも最近ではこれを行っています。重要なのは、開発中にファイルがこのようになっていないことです。
軌道での軽量レース

15

他の回答者が言及した単純さの要因に加えて、多くのCプログラムは1人の個人によって書かれています。

個人のチームがある場合、アプリケーションを複数のソースファイルに分割して、コード変更の不必要な競合を回避することが望ましくなります。特に、プロジェクトに取り組んでいる上級プログラマーと非常に若いプログラマーの両方がいる場合。

一人が一人で作業しているとき、それは問題ではありません。

個人的に、私は習慣的なものとして機能に基づいて複数のファイルを使用しています。しかし、それは私だけです。


4
@OskarSkogただし、将来の自分と同時にファイルを変更することはありません。
ローレンペクテル

2

C89にはinline機能がなかったからです。つまり、ファイルを関数に分割すると、スタックに値をプッシュしてジャンプするオーバーヘッドが発生します。これにより、1つの大きなswitchステートメント(イベントループ)でコードを実装する場合に比べてかなりのオーバーヘッドが追加されました。しかし、イベントループは、モジュール化されたソリューションよりも、効率的に(または正確に)実装するのが常にはるかに困難です。そのため、大規模なプロジェクトの場合、人々はモジュール化を拒否します。しかし、設計を事前に考え、1つのswitchステートメントで状態を制御できる場合、彼らはそれを選択しました。

今日では、Cでも関数をインライン化できるため、モジュール化するためにパフォーマンスを犠牲にする必要はありません。


2
C関数は89と同じくらいインラインになりました。インラインはほとんど使用されるべきではありません-コンパイラはほとんどすべての状況であなたよりもよく知っています。そして、それらの4k LOCファイルのほとんどは1つの巨大な機能ではありません-それは目立ったパフォーマンスの利点も持たない恐ろしいコーディングスタイルです。
Voo

@Voo、コーディングスタイルに言及する理由がわかりません。私はそれを主張していませんでした。実際、私はほとんどの場合、実装の失敗により効率の低いソリューションを保証すると述べました。また、(より大きなプロジェクトに)スケールしないため、それは悪い考えだと言いました。とはいえ、非常にタイトなループ(ハードウェアに近いネットワークコードで発生すること)では、(関数を呼び出すときに)スタック上で値を不必要にプッシュおよびポップすると、実行中のプログラムのコストが増加します。これは素晴らしい解決策ではありませんでした。しかし、それは当時入手可能な最高のものでした。
ドミトリールバノビッチ

2
必須の注意:インラインキーワードは、インライン最適化とはほとんど関係ありません。コンパイラがその最適化を行うための特別なヒントではなく、重複するシンボルとのリンクに関係しています。
ハイド

@Dmitryポイントはinline、C89コンパイラにキーワードがなかったためインライン化できなかったため、1つの巨大な関数ですべてを記述しなければならなかったと主張するのは誤りです。inlineパフォーマンスの最適化として使用することはほとんどないはずです-コンパイラは一般にあなたよりもよく知っているでしょう(そしてキーワードを無視することもできます)。
Voo

@Voo:プログラマーとコンパイラーは一般に、お互いが知らないことを知っています。inlineキーワードは、インライン最適化を実行するか否かの問題よりも重要であるリンカ関連のセマンティクスを持っていますが、いくつかの実装は、インライニングを制御すると、そのような事は時には非常に重要になることが他のディレクティブを持っています。場合によっては、関数は大きすぎてインライン化する価値がないように見えるかもしれませんが、定数の折りたたみはサイズと実行時間をほとんどゼロに減らすかもしれません。
インライン

1

これは進化の例としてカウントされますが、まだ言及されていないことに驚いています。

プログラミングの暗い日には、単一のFILEのコンパイルに数分かかることがありました。プログラムがモジュール化されている場合、必要なヘッダーファイルをインクルードすると(プリコンパイル済みヘッダーオプションはありません)、速度低下の大きな原因になります。さらに、コンパイラは、おそらく自動スワップファイルの利点なしに、ディスク自体に情報を保持することを選択/必要とする場合があります。

これらの環境要因が進行中の開発プラクティスに引き継がれる習慣は、徐々にゆっくりと順応してきました。

当時、単一のファイルを使用することで得られる利益は、HDDの代わりにSSDを使用することで得られる利益に似ています。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.