For example, the SysInternals tool "FileMon" from the past has a kernel-mode driver whose source code is entirely in one 4,000-line file. The same for the first ever ping program ever written (~2,000 LOC).
6 Answers
Using multiple files always requires additional administrative overhead. One has to setup a build script and/or makefile with separated compiling and linking stages, make sure the dependencies between the different files are managed correctly, write a "zip" script for easier distribution of the source code by email or download, and so on. Modern IDEs today typically take a lot of that burden, but I am pretty sure at the time when the first ping program was written, no such IDE was available. And for files that small as ~4000 LOC, without such an IDE which manages multiple files for you well, the trade off between the mentioned overhead and the benefits from using multiple files might let people make a decision for the single file approach.
-
12"And for files that small as ~4000 LOC..." I'm working as a JS dev right now. When I have a file just 400 lines of code long, I get nervous about how large it's become! (But we have dozens and dozens of files in our project.)– KevinCommented Mar 3, 2017 at 16:06
-
38@Kevin: one hair on my head is too few, one hair in my soup is too many ;-) AFAIK in JS multiple files do not cause that much administrative overhead as in "C without a modern IDE". Commented Mar 3, 2017 at 16:20
-
4@Kevin JS is a fairly different beast though. JS is transmitted to an end user every time a user loads a website and does not have it already cached by their browser. C only has to have the code transmitted once, then the person at the other end compiles it and it stays compiled (obviously there are exceptions, but that's the general expected use-case). Also C stuff tends to be legacy code, as are much of the '4000 lines is normal' projects people are describing in the comments.– PharapCommented Mar 4, 2017 at 1:04
-
6@Kevin Now go and see how underscore.js (1700 loc, one file) and a myriad of other libraries that are distributed are written. Javascript is actually almost as bad as C with regard to modularization and deployment.– VooCommented Mar 4, 2017 at 11:47
-
3@Pharap I think he meant using something like Webpack before deploying the code. With Webpack, you can work on multiple files and then compile them into one bundle. Commented Mar 5, 2017 at 3:20
Because C isn't good at modularization. It gets messy (header files and #includes, extern functions, link-time errors, etc) and the more modules you bring in, the trickier it gets.
More modern languages have better modularization capabilities in part because they learned from C's mistakes, and they make it easier to break down your codebase into smaller, simpler units. But with C, it can be beneficial to avoid or minimize all that trouble, even if it means lumping what would otherwise be considered too much code into a single file.
-
43I think it is unfair to describe the C approach as 'mistakes'; they were perfectly sensible and reasonable decisions at the time they were made. Commented Mar 3, 2017 at 14:43
-
15None of that modularisation stuff is particularly complicated. It can be made complicated by bad coding style, but it's not hard to understand or implement, and none of it could be classed as "mistakes". The real reason, as per Snowman's answer, is that optimisation over multiple source files was not so good in the past, and that FileMon driver requires high performance. Also, contrary to the OP's opinion, those aren't particularly large files.– GrahamCommented Mar 3, 2017 at 14:47
-
8@Graham Any file larger than 1000 lines of code should be treated as a code smell. Commented Mar 3, 2017 at 14:54
-
12@JackAidley its not unfair at all, having something be a mistake is not mutual exclusive with saying it was a reasonable decision at the time. Mistakes are inevitable given imperfect information and limited time and should be learned from not shamefully hidden or reclassified to save face. Commented Mar 3, 2017 at 15:49
-
10Anybody who claims that C's approach is not a mistake fails to understand how a seemingly ten-liner C file can actually be ten-thousand-liner file with all headers #include:d. This means every single file in your project is effectively at least ten thousand lines, no matter how much is the line count given by "wc -l". Better support for modularity would easily cut parsing and compilation times into a tiny fraction.– juhistCommented Mar 3, 2017 at 18:33
Aside from the historical reasons, there is one reason to use this in modern performance-sensitive software. When all of the code is in one compilation unit, the compiler is able to perform whole-program optimizations. With separate compilation units, the compiler cannot optimize the entire program in certain ways (e.g. inlining certain code).
The linker can certainly perform some optimizations in addition to what the compiler can do, but not all. For example: modern linkers are really good at eliding unreferenced functions, even across multiple object files. They may be able to perform some other optimizations, but nothing like what a compiler can do inside a function.
One well-known example of a single-source code module is SQLite. You can read more about it on The SQLite Amalgamation page.
1. Executive Summary
Over 100 separate source files are concatenated into a single large files of C-code named "sqlite3.c" and called "the amalgamation". The amalgamation contains everything an application needs to embed SQLite. The amalgamation file is more than 180,000 lines long and over 6 megabytes in size.
Combining all the code for SQLite into one big file makes SQLite easier to deploy — there is just one file to keep track of. And because all code is in a single translation unit, compilers can do better inter-procedure optimization resulting in machine code that is between 5% and 10% faster.
-
16But note that modern C compilers can do whole-program optimization of multiple source files (although not if you compile them into individual object files first).– DavislorCommented Mar 3, 2017 at 3:35
-
10@Davislor Look at the typical build script: compilers are not realistically going to do that.– user22815Commented Mar 3, 2017 at 4:11
-
4It’s significantly easier to change a build script to
$(CC) $(CFLAGS) $(LDFLAGS) -o $(TARGET) $(CFILES)
than to move everything to a single soudce file. You can even do the whole-program compilation as an alternative target to the traditional build script that skips recompiling source files that haven’t changed, similar to how people might turn off profiling and debugging for the production target. You do not have that option if everything is in one big heap o’source. It’s not what people are used to, but there’s nothing cumbersome about it.– DavislorCommented Mar 3, 2017 at 6:47 -
9@Davislor whole program optimization / link-time optimization (LTO) also works when you "compile" the code into individual object files (depending on what "compile" means to you). For example, GCC's LTO will add its parsed code representation to the individual object files at compile time, and at link time will use that one instead of the (also present) object code to re-compile and build the whole program. So this works with build setups that compile to individual object files first, though the machine code generated by the initial compilation is ignored.– DreamerCommented Mar 3, 2017 at 7:54
-
8JsonCpp does this nowadays too. The key is that the files are not this way during development. Commented Mar 3, 2017 at 11:01
In addition to the simplicity factor the other respondent mentioned, many C programs are written by one individual.
When you have a team of individuals, it becomes desirable to split the application across several source files to avoid gratuitous conflicts in code changes. Especially when there are both advanced and very junior programmers working on the project.
When one person is working by himself, that isn't an issue.
Personally, I use multiple files based on function as a habitual thing. But that's just me.
-
4@OskarSkog But you will never modify a file at the same time as your future self. Commented Mar 5, 2017 at 5:13
Because C89 didn't have inline
functions. Which meant that breaking up your file into functions caused the overhead of pushing values on stack and jumping around. This added quite a bit of an overhead over implementing the code in 1 large switch statement (event loop). But an event loop is always much more difficult to implement efficiently (or even correctly) than a more modularized solution. So for large-size projects, people would still opt out to modularize. But when they had the design thought-out in advance and could control the state in 1 switch statement, they opted for that.
Nowadays, even in C, one need not have to sacrifice performance to modularize because even in C functions can be inlined.
-
3C functions could be just as much inline in 89 as these days, inline is something that should be used almost never - the compiler knows better than you in almost all situations. And most of those 4k LOC files are not one gigantic function - that's a horrible coding style which won't have any noticeable performance benefit either.– VooCommented Mar 4, 2017 at 11:55
-
@Voo, I don't know why you mention the coding style. I wasn't advocating it. In fact, I mentioned that in most cases it guarantees a less efficient solution due to a botched implementation. I also mentioned that it's a bad idea because it doesn't scale (to larger projects). Having said that, in very tight loops (which is what happens in close-to-hardware networking code), needlessly pushing and popping values on/off stack (when calling functions) will add to the cost of the running program. This was not a great solution. But it was the best one available at the time. Commented Mar 4, 2017 at 12:29
-
3Obligatory note: inline keyword has only a little to do with inlining optimization. It is not a special hint for compiler to do that optimization, instead it has to do with linking with duplicate symbols.– hydeCommented Mar 5, 2017 at 16:01
-
@Dmitry The point is that claiming that because there was no
inline
keyword in C89 compilers couldn't inline which is why you had to write everything in one giant function is incorrect. You should pretty much never useinline
as a performance optimisations - the compiler will generally know better than you anyhow (and can just as well ignore the keyword).– VooCommented Mar 5, 2017 at 16:58 -
@Voo: A programmer and a compiler will generally each know some things the other doesn't. The
inline
keyword has linker-related semantics which are more important than the question of whether or not to perform in-line optimizations, but some implementations have other directives to control in-lining and such things can sometimes be very important. In some cases, a function may look like it's too large to be worth in-lining, but constant folding might reduce the size and execution time to almost nothing. A compiler that isn't given a strong nudge to encourage in-lining might not...– supercatCommented Mar 5, 2017 at 22:33
This counts as an example of evolution, which I am surprised has not been mentioned yet.
In the dark days of programming, compilation of a single FILE could take minutes. If a program was modularised, then inclusion of the necessary header files (no precompiled header options) would be a significant additional cause of slowdown. Additionally the compiler might choose/need to keep some information on disk itself, probably without the benefit of an automatic swap file.
The habits that these environmental factors led to carried over into ongoing development practices and have only slowly adapted over time.
At the time the gain from using a single file would be similar to that we get by the use of SSDs instead of HDDs.