Почему некоторые программы на C написаны в одном огромном исходном файле?

88

Например, инструмент SysInternals "FileMon" из прошлого имеет драйвер режима ядра, исходный код которого полностью находится в одном файле из 4000 строк. То же самое для первой когда-либо написанной программы ping (~ 2000 LOC).

Отруби
источник

Ответы:

143

Использование нескольких файлов всегда требует дополнительных административных затрат. Нужно настроить скрипт сборки и / или make-файл с отдельными этапами компиляции и компоновки, убедиться, что зависимости между различными файлами корректно управляются, написать скрипт «zip» для более легкого распространения исходного кода по электронной почте или загрузке и т. Д. на. Современные IDE сегодня обычно берут на себя это бремя, но я уверен, что в то время, когда была написана первая программа ping, такой IDE не было. А для файлов размером ~ 4000 LOC без такой IDE, которая хорошо управляет несколькими файлами, компромисс между упомянутыми издержками и преимуществами использования нескольких файлов может позволить людям принять решение о подходе с одним файлом.

Док Браун
источник
9
«И для файлов размером ~ 4000 LOC ...» Я сейчас работаю разработчиком JS. Когда у меня есть файл длиной всего 400 строк кода, я нервничаю из-за его размера! (Но у нас есть десятки и десятки файлов в нашем проекте.)
Кевин
36
@Kevin: одного волоска на моей голове слишком мало, одного волоска в моем супе слишком много ;-) AFAIK в нескольких файлах JS не вызывает таких больших административных издержек, как в «C без современной IDE».
Док Браун
4
@Kevin JS - совсем другой зверь. JS передается конечному пользователю каждый раз, когда пользователь загружает веб-сайт и еще не кэшировал его в своем браузере. C должен только один раз передать код, затем человек на другом конце компилирует его, и он остается скомпилированным (очевидно, есть исключения, но это общий ожидаемый вариант использования). Кроме того, материал на C, как правило, является унаследованным кодом, как и большая часть проектов «4000 строк - это нормально», которые люди описывают в комментариях.
Pharap
5
@Kevin Теперь посмотрим, как пишется underscore.js (1700 loc, один файл) и множество других распространяемых библиотек. Javascript на самом деле почти так же плох, как C, в отношении модульности и развертывания.
Во
2
@Pharap Я думаю, он имел в виду использование чего-то вроде Webpack перед развертыванием кода. С помощью Webpack вы можете работать с несколькими файлами, а затем скомпилировать их в один пакет.
Брайан Маккатон
81

Потому что C не хорош в модульности. Он запутывается (файлы заголовков и #include, функции extern, ошибки во время компоновки и т. Д.), И чем больше модулей вы вводите, тем сложнее становится.

Более современные языки имеют лучшие возможности модульности отчасти потому, что они учились на ошибках С, и они облегчают разбивку вашей кодовой базы на более мелкие и простые модули. Но с C может быть полезно избежать или минимизировать все эти проблемы, даже если это означает объединение того, что в противном случае считалось бы слишком большим количеством кода в одном файле.

Мейсон Уилер
источник
38
Я думаю, что было бы несправедливо описывать подход C как «ошибки»; они были совершенно разумными и разумными решениями во время их принятия.
Джек Эйдли,
14
Ничто из этого модульного материала не является особенно сложным. Это может быть сделано осложняются плохой стиль кодирования, но это не трудно понять или осуществить, и ни один из них не может быть классифицирован как «ошибки». Реальная причина, согласно ответу Snowman, заключается в том, что в прошлом оптимизация по нескольким исходным файлам была не очень хорошей, а драйвер FileMon требует высокой производительности. Кроме того, вопреки мнению ОП, это не особо большие файлы.
Грэм
8
@Graham Любой файл размером более 1000 строк кода должен восприниматься как запах кода.
Мейсон Уилер
11
@JackAidley его не несправедливо вообще , имея что - то ошибка не взаимоисключающая с сказав , что это разумное решение , в то время. Ошибки неизбежны, учитывая несовершенную информацию и ограниченное время, и их следует извлекать из постыдно скрытого или реклассифицированного, чтобы спасти лицо.
Джаред Смит
8
Любой, кто заявляет, что подход C не является ошибкой, не понимает, как, по-видимому, файл C с десятью строками может фактически быть файлом с десятью тысячами строк со всеми заголовками #include: d. Это означает, что каждый отдельный файл в вашем проекте содержит не менее десяти тысяч строк, независимо от того, сколько строк указано в «wc -l». Лучшая поддержка модульности легко сократит время разбора и компиляции до крошечной доли.
юрист
37

Помимо исторических причин, есть одна причина использовать это в современном чувствительном к производительности программном обеспечении. Когда весь код находится в одном модуле компиляции, компилятор может выполнять оптимизацию всей программы. С отдельными модулями компиляции компилятор не может оптимизировать всю программу определенными способами (например, вставляя определенный код).

Компоновщик, безусловно, может выполнять некоторые оптимизации в дополнение к тому, что может делать компилятор, но не все. Например: современные компоновщики действительно хороши для исключения функций, на которые нет ссылок, даже для нескольких объектных файлов. Они могут быть в состоянии выполнить некоторые другие оптимизации, но ничего общего с тем, что компилятор может делать внутри функции.

Одним из хорошо известных примеров модуля с одним исходным кодом является SQLite. Вы можете прочитать больше об этом на странице объединения SQLite .

1. Резюме

Более 100 отдельных исходных файлов объединяются в один большой файл C-кода с именем «sqlite3.c» и называются «объединением». Объединение содержит все, что необходимо приложению для встраивания SQLite. Файл объединения имеет длину более 180 000 строк и размер более 6 мегабайт.

Объединение всего кода для SQLite в один большой файл упрощает развертывание SQLite - существует только один файл для отслеживания. А поскольку весь код находится в одном модуле перевода, компиляторы могут лучше оптимизировать межпроцедурную оптимизацию, в результате чего машинный код работает на 5-10% быстрее.


источник
15
Но обратите внимание, что современные компиляторы C могут выполнять целую программу оптимизации нескольких исходных файлов (хотя и не в том случае, если вы сначала скомпилируете их в отдельные объектные файлы).
Дэвислор
10
@Davislor Посмотрите на типичный скрипт сборки: компиляторы не собираются этого делать.
4
Значительно проще изменить сценарий сборки, $(CC) $(CFLAGS) $(LDFLAGS) -o $(TARGET) $(CFILES)чем перенести все в один файл соудс. Вы даже можете выполнить компиляцию всей программы в качестве альтернативы целевому сценарию, который пропускает перекомпиляцию исходных файлов, которые не изменились, подобно тому, как люди могут отключить профилирование и отладку для производственной цели. У вас нет такой возможности, если все находится в одной большой куче ресурсов. Это не то, к чему привыкли люди, но в этом нет ничего обременительного.
Дэвислор
9
@Davislor оптимизация всей программы / оптимизация во время компоновки (LTO) также работает, когда вы «компилируете» код в отдельные объектные файлы (в зависимости от того, что для вас означает «компилировать»). Например, LTO GCC добавит свое проанализированное представление кода к отдельным объектным файлам во время компиляции и во время компоновки будет использовать его вместо (также присутствующего) объектного кода для повторной компиляции и сборки всей программы. Так что это работает с настройками сборки, которые сначала компилируются в отдельные объектные файлы, хотя машинный код, сгенерированный при начальной компиляции, игнорируется.
Мечтатель
8
JsonCpp делает это и сейчас. Ключ в том, что файлы не так во время разработки.
Гонки
15

В дополнение к фактору простоты, упомянутому другим респондентом, многие программы на Си написаны одним человеком.

Когда у вас есть группа людей, становится желательным разделить приложение на несколько исходных файлов, чтобы избежать необоснованных конфликтов в изменениях кода. Особенно, когда над проектом работают как продвинутые, так и начинающие программисты.

Когда один человек работает сам по себе, это не проблема.

Лично я использую несколько файлов на основе функций как привычную вещь. Но это только я.

Рон Рубль
источник
4
@OskarSkog Но вы никогда не будете изменять файл в то же время, что и вы сами.
Лорен Печтел
2

Потому что у C89 не было inlineфункций. Это означало, что разбиение вашего файла на функции приводило к дополнительным затратам на перенос значений в стек и перепрыгивание. Это добавило много накладных расходов при реализации кода в 1 большом операторе switch (цикл обработки событий). Но цикл обработки событий всегда намного сложнее реализовать (или даже правильно), чем более модульное решение. Поэтому для крупных проектов люди все равно отказались бы от модульности. Но когда они заранее продумали дизайн и смогли контролировать состояние в 1 операторе switch, они сделали это.

В настоящее время, даже в C, не нужно жертвовать производительностью для модульности, потому что даже в C функции могут быть встроенными.

Дмитрий Рубанович
источник
2
C-функции могут быть такими же встроенными в 89, как и в наши дни, встроенные - это то, что должно использоваться почти никогда - компилятор знает лучше, чем вы, почти во всех ситуациях. И большинство из этих файлов 4k LOC не являются одной гигантской функцией - это ужасный стиль кодирования, который также не принесет заметного выигрыша в производительности.
Во
@ Воу, я не знаю, почему ты упоминаешь стиль кодирования. Я не защищал это. Фактически, я упомянул, что в большинстве случаев это гарантирует менее эффективное решение из-за неудачной реализации. Я также упомянул, что это плохая идея, потому что она не масштабируется (для более крупных проектов). Сказав это, в очень узких циклах (что и происходит в сетевом коде, близком к аппаратному), ненужное добавление и извлечение значений из стека вкл / выкл (при вызове функций) увеличит стоимость работающей программы. Это не было отличным решением. Но это был лучший из доступных в то время.
Дмитрий Рубанович
2
Обязательное примечание: встроенное ключевое слово имеет мало общего с встраиваемой оптимизацией. Это не особый совет для компилятора, чтобы выполнить эту оптимизацию, вместо этого это связано с связыванием с дублирующимися символами.
Hyde
@Dmitry Дело в том, что утверждение о том, что из-за отсутствия inlineключевого слова в компиляторах C89, не может быть встроенным, поэтому вам пришлось писать все в одной гигантской функции, что неверно. Вы не должны использовать inlineоптимизацию производительности - компилятор, как правило, будет знать лучше, чем вы (и может игнорировать ключевое слово).
Во
@ Voo: программист и компилятор, как правило, каждый знает то, что другие не знают. inlineКлючевое слово компоновщика связанного с семантикой , которые являются более важными , чем вопрос о том, следует ли выполнять в линии оптимизацию, но некоторые реализации имеют другие директивы для контроля качества в подкладке и такие вещи иногда могут быть очень важны. В некоторых случаях функция может выглядеть так, как будто она слишком велика, чтобы ее можно было встроить, но постоянное свертывание может уменьшить размер и время выполнения практически до нуля. Компилятор, которому не дают сильного толчка, чтобы поощрить
встраивание,
1

Это считается примером эволюции, которая, как я удивляюсь, еще не упоминалась.

В темные дни программирования компиляция одного ФАЙЛА могла занимать минуты. Если бы программа была модульной, то включение необходимых файлов заголовков (без предварительно скомпилированных параметров заголовков) было бы значительной дополнительной причиной замедления. Кроме того, компилятор может выбрать / нужно сохранить некоторую информацию на самом диске, возможно, без использования файла автоматической замены.

Привычки, которые эти факторы окружающей среды привели к продолжению практики развития и только постепенно адаптировались с течением времени.

В то время выигрыш от использования одного файла был бы аналогичен тому, который мы получаем при использовании SSD вместо HDD.

ITJ
источник