Warum sind einige C-Programme in einer großen Quelldatei geschrieben?

88

Das SysInternals- Tool "FileMon" aus der Vergangenheit verfügt beispielsweise über einen Kernelmodustreiber, dessen Quellcode sich vollständig in einer Datei mit 4.000 Zeilen befindet. Dasselbe gilt für das erste Ping-Programm, das jemals geschrieben wurde (~ 2.000 LOC).

Kleie
quelle

Antworten:

143

Die Verwendung mehrerer Dateien erfordert immer zusätzlichen Verwaltungsaufwand. Man muss ein Build-Skript und / oder ein Makefile mit getrennten Kompilierungs- und Verknüpfungsstufen einrichten, sicherstellen, dass die Abhängigkeiten zwischen den verschiedenen Dateien korrekt verwaltet werden, ein "zip" -Skript schreiben, um den Quellcode einfacher per E-Mail oder Download zu verteilen und so weiter auf. Moderne IDEs sind heutzutage in der Regel sehr belastend, aber ich bin mir ziemlich sicher, dass zu dem Zeitpunkt, als das erste Ping-Programm geschrieben wurde, keine solche IDE verfügbar war. Und für Dateien , die kleiner als ~ 4000 LOC, ohne eine solche IDE , die gut für Sie mehrere Dateien verwaltet, weg von der Handel zwischen der Kopf erwähnt und die Vorteile von mehreren Dateien mit möglicherweise damit die Menschen eine Entscheidung für die einzelne Datei Ansatz machen.

Doc Brown
quelle
9
"Und für Dateien, die so klein sind wie ~ 4000 LOC ..." Ich arbeite gerade als JS-Entwickler. Wenn ich eine Datei mit nur 400 Codezeilen habe, werde ich nervös, wie groß sie geworden ist! (Aber wir haben Dutzende von Dateien in unserem Projekt.)
Kevin
36
@ Kevin: ein Haar auf meinem Kopf ist zu wenig, ein Haar in meiner Suppe ist zu viel ;-) AFAIK in JS Mehrere Dateien verursachen nicht so viel Verwaltungsaufwand wie in "C ohne eine moderne IDE".
Doc Brown
4
@ Kevin JS ist ein ziemlich anderes Tier. JS wird jedes Mal an einen Endbenutzer übertragen, wenn ein Benutzer eine Website lädt und diese noch nicht von seinem Browser zwischengespeichert hat. C muss den Code nur einmal übertragen, dann kompiliert ihn die Person am anderen Ende und er bleibt kompiliert (offensichtlich gibt es Ausnahmen, aber das ist der allgemein erwartete Anwendungsfall). Auch C-Zeug ist in der Regel Legacy-Code, ebenso wie ein Großteil der "4000 Zeilen ist normal" -Projekte, die in den Kommentaren beschrieben werden.
Pharap
5
@ Kevin Nun gehen Sie und sehen Sie, wie underscore.js (1700 loc, eine Datei) und eine Vielzahl anderer Bibliotheken, die verteilt werden, geschrieben werden. In Bezug auf Modularisierung und Bereitstellung ist Javascript tatsächlich fast so schlecht wie C.
Voo
2
@Pharap Ich denke, er wollte so etwas wie Webpack verwenden, bevor er den Code bereitstellte . Mit Webpack können Sie mehrere Dateien bearbeiten und dann zu einem Bundle zusammenfassen.
Brian McCutchon
81

Weil C keine gute Modularisierung kann. Es wird chaotisch (Header-Dateien und #includes, externe Funktionen, Verbindungsfehler usw.) und je mehr Module Sie einbinden, desto kniffliger wird es.

Modernere Sprachen haben zum Teil bessere Modularisierungsfähigkeiten, weil sie aus den Fehlern von C gelernt haben und es einfacher machen, Ihre Codebasis in kleinere, einfachere Einheiten aufzuteilen. Mit C kann es jedoch von Vorteil sein, all diese Probleme zu vermeiden oder zu minimieren, auch wenn dies bedeutet, dass zu viel Code in einer einzigen Datei zusammengefasst wird.

Mason Wheeler
quelle
38
Ich halte es für unfair, den C-Ansatz als "Fehler" zu bezeichnen. Sie waren zu dem Zeitpunkt, als sie getroffen wurden, absolut vernünftige und vernünftige Entscheidungen.
Jack Aidley
14
Nichts davon ist besonders kompliziert. Es kann sein gemacht durch schlechten Codierstil komplizierte, aber es ist nicht schwer zu verstehen oder zu implementieren, und nichts davon könnte als „Fehler“ eingestuft werden. Der wahre Grund dafür ist laut Snowman die Tatsache, dass die Optimierung über mehrere Quelldateien in der Vergangenheit nicht so gut war und dass der FileMon-Treiber eine hohe Leistung erfordert. Entgegen der Meinung des OP handelt es sich bei diesen Dateien auch nicht um besonders große Dateien.
Graham
8
@ Abraham Jede Datei, die größer als 1000 Codezeilen ist, sollte als Codegeruch behandelt werden.
Mason Wheeler
11
@JackAidley seine nicht unfair überhaupt mit etwas, wäre ein Fehler ist nicht gegenseitig exklusiv mit sagen , es ist eine vernünftige Entscheidung zu der Zeit ist. Fehler sind angesichts unvollständiger Informationen und begrenzter Zeit unvermeidlich und sollten gelernt werden, wenn sie nicht schändlich versteckt oder neu klassifiziert werden, um das Gesicht zu schützen.
Jared Smith
8
Jeder, der behauptet, der Ansatz von C sei kein Fehler, kann nicht nachvollziehen, wie eine scheinbar zehnzeilige C-Datei tatsächlich zehntausendzeilige Dateien mit allen Headern sein kann. #Include: d. Dies bedeutet, dass jede einzelne Datei in Ihrem Projekt mindestens zehntausend Zeilen umfasst, unabhängig davon, wie viele Zeilen "wc -l" enthält. Eine bessere Unterstützung der Modularität würde die Analyse- und Kompilierungszeiten leicht in einen winzigen Bruchteil reduzieren.
JUHIST
37

Abgesehen von den historischen Gründen gibt es einen Grund, dies in moderner leistungssensitiver Software zu verwenden. Befindet sich der gesamte Code in einer Kompilierungseinheit, kann der Compiler programmübergreifende Optimierungen durchführen. Bei separaten Kompilierungseinheiten kann der Compiler das gesamte Programm nicht auf bestimmte Weise optimieren (z. B. Inlining bestimmter Codes).

Der Linker kann zwar einige Optimierungen zusätzlich zu den Funktionen des Compilers durchführen, jedoch nicht alle. Zum Beispiel: Moderne Linker sind wirklich gut darin, nicht referenzierte Funktionen auch über mehrere Objektdateien hinweg zu eliminieren. Sie sind möglicherweise in der Lage, einige andere Optimierungen durchzuführen, haben jedoch nichts mit dem zu tun, was ein Compiler in einer Funktion tun kann.

Ein bekanntes Beispiel für ein Single-Source-Code-Modul ist SQLite. Weitere Informationen finden Sie auf der Seite SQLite Amalgamation .

1. Zusammenfassung

Über 100 separate Quelldateien werden zu einer einzigen großen C-Code-Datei mit dem Namen "sqlite3.c" und dem Namen "the amalgamation" verknüpft. Die Zusammenführung enthält alles, was eine Anwendung zum Einbetten von SQLite benötigt. Die Zusammenschlussdatei ist mehr als 180.000 Zeilen lang und über 6 Megabyte groß.

Wenn Sie den gesamten Code für SQLite in einer großen Datei zusammenfassen, lässt sich SQLite einfacher bereitstellen - es gibt nur eine Datei, die Sie nachverfolgen müssen. Und da sich der gesamte Code in einer einzigen Übersetzungseinheit befindet, können Compiler die Optimierung zwischen den Prozeduren verbessern, was zu einem um 5 bis 10% schnelleren Maschinencode führt.


quelle
15
Beachten Sie jedoch, dass moderne C-Compiler mehrere Quelldateien programmübergreifend optimieren können (allerdings nicht, wenn Sie sie zuerst in einzelne Objektdateien kompilieren).
Davislor
10
@Davislor Sehen Sie sich das typische Build-Skript an: Compiler werden das nicht realistisch machen.
4
Es ist bedeutend einfacher, ein Build-Skript in zu ändern, $(CC) $(CFLAGS) $(LDFLAGS) -o $(TARGET) $(CFILES)als alles in eine einzige Soudce-Datei zu verschieben. Sie können sogar die Kompilierung des gesamten Programms als alternatives Ziel für das herkömmliche Erstellungsskript ausführen, bei dem das Neukompilieren von Quelldateien, die sich nicht geändert haben, übersprungen wird, ähnlich wie beim Deaktivieren der Profilerstellung und des Debuggens für das Produktionsziel. Sie haben diese Option nicht, wenn sich alles in einem großen Haufen befindet. Es ist nicht das, was die Leute gewohnt sind, aber es ist nicht umständlich.
Davislor
9
@Davislor-Ganzprogrammoptimierung / Link-Time-Optimierung (LTO) funktioniert auch, wenn Sie den Code in einzelne Objektdateien "kompilieren" (je nachdem, was "kompilieren" für Sie bedeutet). Beispielsweise fügt der LTO von GCC seine analysierte Codedarstellung zur Kompilierungszeit zu den einzelnen Objektdateien hinzu und verwendet diese zur Verbindungszeit anstelle des (ebenfalls vorhandenen) Objektcodes, um das gesamte Programm neu zu kompilieren und zu erstellen. Dies funktioniert also mit Build-Setups, die zuerst in einzelne Objektdateien kompiliert werden, obwohl der durch die Erstkompilierung generierte Maschinencode ignoriert wird.
Träumer
8
JsonCpp macht das heutzutage auch. Der Schlüssel ist, dass die Dateien während der Entwicklung nicht so sind.
Leichtigkeitsrennen im Orbit
15

Zusätzlich zu dem von dem anderen Befragten erwähnten Einfachheitsfaktor werden viele C-Programme von einer Person geschrieben.

Wenn Sie ein Team von Einzelpersonen haben, ist es wünschenswert, die Anwendung auf mehrere Quelldateien aufzuteilen, um unbegründete Konflikte bei Codeänderungen zu vermeiden. Vor allem, wenn sowohl fortgeschrittene als auch sehr junge Programmierer an dem Projekt arbeiten.

Wenn eine Person alleine arbeitet, ist das kein Problem.

Persönlich verwende ich mehrere Dateien basierend auf der Funktion als eine gewohnheitsmäßige Sache. Aber das bin nur ich.

Ron Ruble
quelle
4
@OskarSkog Aber Sie werden niemals eine Datei gleichzeitig mit Ihrem zukünftigen Ich ändern.
Loren Pechtel
2

Weil C89 keine inlineFunktionen hatte. Was bedeutete, dass das Aufteilen Ihrer Datei in Funktionen den Overhead verursachte, Werte auf den Stapel zu schieben und herumzuspringen. Dies führte zu einem erheblichen Mehraufwand bei der Implementierung des Codes in einer großen switch-Anweisung (Ereignisschleife). Es ist jedoch immer viel schwieriger, eine Ereignisschleife effizient (oder sogar korrekt) zu implementieren als eine modularere Lösung. Bei großen Projekten würden sich die Leute also immer noch gegen eine Modularisierung entscheiden. Aber als sie das Design im Voraus durchdacht hatten und den Status in einer switch-Anweisung steuern konnten, entschieden sie sich dafür.

Heutzutage muss man auch in C nicht auf Leistung verzichten, um zu modularisieren, da sogar in C Funktionen integriert werden können.

Dmitry Rubanovich
quelle
2
C-Funktionen könnten in 89 genauso inline sein wie heutzutage, Inline sollte so gut wie nie verwendet werden - der Compiler weiß es in fast allen Situationen besser als Sie. Und die meisten dieser 4k-LOC-Dateien sind keine einzige gigantische Funktion - das ist ein schrecklicher Codierungsstil, der auch keinen merklichen Leistungsvorteil hat.
Voo
@Voo, ich weiß nicht warum du den Codierungsstil erwähnst. Ich habe es nicht befürwortet. In der Tat habe ich erwähnt, dass es in den meisten Fällen eine weniger effiziente Lösung aufgrund einer fehlerhaften Implementierung garantiert. Ich erwähnte auch, dass es eine schlechte Idee ist, weil es nicht skalierbar ist (für größere Projekte). Das heißt, in sehr engen Schleifen (was bei hardwarenahem Netzwerkcode der Fall ist) wird das unnötige Pushen und Poppen von Werten auf dem Stack (beim Aufrufen von Funktionen) die Kosten des laufenden Programms erhöhen. Dies war keine großartige Lösung. Aber es war das beste, das es zu dieser Zeit gab.
Dmitry Rubanovich
2
Hinweis: Das Inline- Keyword hat nur wenig mit der Inlining-Optimierung zu tun. Es ist kein besonderer Hinweis für den Compiler, diese Optimierung durchzuführen, sondern es hat mit dem Verknüpfen mit doppelten Symbolen zu tun.
Hyde
@Dmitry Der Punkt ist, dass die Behauptung, dass es kein inlineSchlüsselwort in C89-Compilern gibt, das nicht inline funktionieren könnte, weshalb Sie alles in einer riesigen Funktion schreiben mussten, falsch ist. Sie sollten es so gut wie nie inlineals Leistungsoptimierung verwenden - der Compiler weiß es im Allgemeinen sowieso besser als Sie (und kann das Schlüsselwort genauso gut ignorieren).
Voo
@Voo: Ein Programmierer und ein Compiler wissen im Allgemeinen einige Dinge, die der andere nicht weiß. Das inlineSchlüsselwort hat eine Linker-bezogene Semantik, die wichtiger ist als die Frage, ob Inline-Optimierungen durchgeführt werden sollen oder nicht. Einige Implementierungen enthalten jedoch andere Anweisungen zur Steuerung des Inlinings, und solche Dinge können manchmal sehr wichtig sein. In einigen Fällen sieht eine Funktion möglicherweise so aus, als wäre sie zu groß, als dass sie ausgekleidet werden könnte. Durch ständiges Falzen können Größe und Ausführungszeit jedoch auf fast nichts reduziert werden. Ein Compiler, der keinen starken Anstoß erhält, um das
Inlining anzuregen
1

Dies gilt als Beispiel für die Evolution, von der ich überrascht bin, dass sie noch nicht erwähnt wurde.

In den dunklen Tagen des Programmierens kann das Kompilieren einer einzelnen DATEI einige Minuten dauern. Wenn ein Programm modularisiert würde, wäre die Einbeziehung der erforderlichen Header-Dateien (keine vorkompilierten Header-Optionen) eine wesentliche zusätzliche Ursache für die Verlangsamung. Zusätzlich kann es sein, dass der Compiler einige Informationen auf der Festplatte selbst speichern muss, möglicherweise ohne den Vorteil einer automatischen Auslagerungsdatei.

Die Gewohnheiten, zu denen diese Umweltfaktoren führten, wurden in die laufenden Entwicklungspraktiken übernommen und haben sich im Laufe der Zeit nur langsam angepasst.

Zu diesem Zeitpunkt wäre der Gewinn durch die Verwendung einer einzelnen Datei ähnlich wie bei der Verwendung von SSDs anstelle von HDDs.

itj
quelle