Чому деякі програми С записуються в один величезний вихідний файл?


88

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

Відповіді:


143

Використання декількох файлів завжди вимагає додаткових накладних витрат. Потрібно налаштувати сценарій складання та / або makefile з розділеними етапами компіляції та зв’язування, переконатися, що залежність між різними файлами керована правильно, написати «zip» скрипт для легшого розповсюдження вихідного коду електронною поштою чи завантаження тощо. на. Сучасні IDE сьогодні, як правило, беруть на себе цей тягар, але я впевнений, що в той час, коли була написана перша програма ping, такого IDE не було. А для файлів, малих як ~ 4000 LOC, без такого IDE, який добре управляє кількома файлами, компроміс між згаданими накладними та переваги від використання декількох файлів може дозволити людям приймати рішення щодо підходу одного файлу.


9
"І для файлів, малих як ~ 4000 LOC ..." Я зараз працюю як JS-розробник. Коли у мене в файлі всього 400 рядків коду, я нервую, наскільки він великий! (Але в нашому проекті є десятки і десятки файлів.)
Кевін

36
@Kevin: одного волосся на голові занадто мало, одного волосся в моєму супі занадто багато ;-) AFAIK у кількох файлах JS не викликає стільки адміністративних витрат, як у "C без сучасного IDE".
Док Браун

4
@Kevin JS - це досить інший звір. JS передається кінцевому користувачеві кожного разу, коли користувач завантажує веб-сайт і не має його вже кешований браузером. Код повинен мати лише один раз переданий код, тоді людина на іншому кінці компілює його і він залишається складеним (очевидно, є винятки, але це загальний очікуваний випадок використання). Крім того, матеріали C мають тенденцію бути застарілим кодом, як і більшість проектів "4000 рядків - це нормально", які люди описують у коментарях.
Фарап

5
@Kevin Тепер перегляньте, як записуються underscore.js (1700 локальний, один файл) та безліч інших розповсюджених бібліотек. Javascript насправді майже такий же поганий, як і C щодо модуляризації та розгортання.
Voo

2
@Pharap Я думаю, що він мав на увазі використовувати щось на кшталт Webpack перед розгортанням коду. За допомогою Webpack ви можете працювати над декількома файлами, а потім компілювати їх в один пакет.
Брайан МакКучон

81

Тому що С не добре в модуляризації. Він стає безладним (файли заголовків і #includes, зовнішні функції, помилки під час зв’язку тощо), і чим більше модулів ви вводите, тим складніше він отримує.

Більш сучасні мови мають кращі можливості модуляризації частково через те, що вони вчилися на помилках C, і вони спрощують розбивати вашу кодову базу на більш дрібні, простіші одиниці. Але з C може бути корисним уникнути або звести до мінімуму всю цю проблему, навіть якщо це означає згуртування того, що в іншому випадку вважатиметься занадто великим кодом в один файл.


38
Я думаю, що несправедливо описувати підхід С як "помилки"; вони були ідеально розумними та розумними рішеннями під час їх прийняття.
Джек Едлі

14
Жоден з цих модулів не є особливо складним. Це може бути зроблено ускладнюються поганою стиль кодування, але це не важко зрозуміти або здійснити, і жоден з них не може бути класифікований як «помилки». Справжня причина, згідно з відповіддю Сноумена, полягає в тому, що оптимізація декількох вихідних файлів у минулому не була такою хорошою, і що драйвер FileMon вимагає високої продуктивності. Крім того, всупереч думці ОП, це не особливо великі файли.
Грем

8
@Graham Будь-який файл, що перевищує 1000 рядків коду, слід розглядати як запах коду.
Мейсон Уілер

11
@JackAidley його НЕ несправедливо взагалі , маючи що - то похибка не взаємовиключна з сказавши , що це розумне рішення , в той час. Помилки неминучі з огляду на недосконалу інформацію та обмежений час, і їх слід довідатися із не ганебних прихованих чи перекласифікованих для збереження обличчя.
Джаред Сміт

8
Кожен, хто стверджує, що підхід C не є помилкою, не розуміє, як начебто файл з десятилінійним С може насправді бути десятитисячним файловим файлом із усіма заголовками #include: d. Це означає, що кожен файл у вашому проекті фактично становить щонайменше десять тисяч рядків, незалежно від того, скільки ліній підраховується "wc -l". Краща підтримка модульності дозволила б легко скоротити час розбору та компіляції на невелику частку.
juhist

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)ніж перемістити все в один файл soudce. Ви навіть можете зробити компіляцію цілої програми як альтернативну ціль традиційному сценарію збірки, який пропускає перекомпіляцію вихідних файлів, які не змінилися, подібно до того, як люди можуть вимкнути профілювання та налагодження виробничої цілі. У вас немає такого варіанту, якщо все знаходиться в одній великій купу ресурсів. Це не те, до чого люди звикли, але в цьому немає нічого громіздкого.
Девіслор

9
@Davislor оптимізація всієї програми / оптимізація часу зв’язку (LTO) також працює, коли ви "компілюєте" код в окремі файли об'єктів (залежно від того, що "компілювати" означає для вас). Наприклад, LTO GCC додасть своє синтаксичне представлення коду до окремих об’єктних файлів під час компіляції, а в час посилання використовуватиме цей замість (також присутній) об'єктного коду для повторної компіляції та складання всієї програми. Таким чином, це працює з налаштуваннями збірки, які спочатку компілюються в окремі об’єктні файли, хоча машинний код, сформований під час початкової компіляції, ігнорується.
Сонник

8
JsonCpp робить це і сьогодні. Ключовим є те, що файли під час розробки не так.
Гонки легкості на Орбіті

15

На додаток до коефіцієнта простоти, про який згадував інший респондент, багато програм C написані однією особою.

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

Коли одна людина працює сам, це не проблема.

Особисто я використовую кілька файлів на основі функції як звичну річ. Але це тільки я.


4
@OskarSkog Але ви ніколи не змінюватимете файл одночасно зі своїм майбутнім.
Лорен Печтел

2

Тому що C89 не мав inlineфункцій. Що означало, що розбиття файлу на функції спричиняє накладні витрати на натискання значень на стек та стрибки навколо. Це додало неабияких витрат на реалізацію коду в 1 великому операторі комутатора (циклі подій). Але цикл подій завжди набагато складніше здійснити ефективно (або навіть правильно), ніж більш модульоване рішення. Тож для великих проектів люди все ж таки відмовляться від модуляризації. Але коли вони заздалегідь продумали проект і змогли контролювати стан за допомогою заяви 1 перемикача, вони вибрали це.

Сьогодні, навіть на C, не потрібно жертвувати продуктивністю, щоб модулювати його, оскільки навіть C функції можуть бути вписані.


2
Функції C можуть бути настільки ж вбудованими в 89, як і сьогодні, inline - це те, що слід використовувати майже ніколи - компілятор знає краще, ніж ви майже у всіх ситуаціях. І більшість з цих 4k файлів LOC - це не одна гігантська функція - це жахливий стиль кодування, який також не матиме помітної переваги від продуктивності.
Voo

@Voo, я не знаю, чому ви згадуєте стиль кодування. Я не виступав за це. Насправді я згадував, що в більшості випадків це гарантує менш ефективне рішення через невдалу реалізацію. Я також зазначив, що це погана ідея, оскільки вона не має масштабів (для великих проектів). Сказавши, що в дуже тісних циклах (що відбувається при близькому до апаратного коду мережевого зв’язку), без потреби натискання та вивільнення значень стека увімкнення / вимкнення (при виклику функцій) додасть вартість запущеної програми. Це не було чудовим рішенням. Але це було найкраще, доступне на той час.
Дмитро Рубанович

2
Обов’язкова примітка: вбудоване ключове слово має лише невелике відношення до вбудованої оптимізації. Це не особливий натяк для компілятора робити цю оптимізацію, натомість це має зв'язок із дублюючими символами.
Гайда

@Dmitry Справа в тому, що стверджувати, що через те, що inlineу компіляторах C89 не було ключового слова, не вдалося вписати, тому вам довелося записати все в одну гігантську функцію, невірно. Ви майже ніколи не використовуєте inlineяк оптимізацію продуктивності - компілятор, як правило, знає краще, ніж ви все одно (і може так само добре ігнорувати ключове слово).
Voo

@Voo: Програміст і компілятор, як правило, знають деякі речі, а інші - не. inlineКлючове слово компоновщика пов'язаного з семантикою , які є більш важливими , ніж питання про те, слід виконувати в лінії оптимізацію, але деякі реалізації мають інші директиви для контролю якості в підкладці і такі речі іноді можуть бути дуже важливі. У деяких випадках функція може виглядати як занадто велика, щоб її варто було вкладати, але постійне складання може майже зменшити розмір та час виконання. Компілятор, який не має сильного поштовху, щоб заохочувати
вкладку,

1

Це вважається прикладом еволюції, про що я дивуюсь, досі не згадувалося.

У похмурі дні програмування складання одного FILE може зайняти кілька хвилин. Якби програма була модулізована, включення необхідних файлів заголовків (без попередньо складених параметрів заголовка) було б істотною додатковою причиною уповільнення. Крім того, компілятор може вибрати / потрібно зберегти деяку інформацію на самому диску, ймовірно, без переваги автоматичного файлу swap.

Звички, які ці фактори навколишнього середовища призвели до перенесення в постійну практику розвитку, з часом лише повільно адаптувалися.

На той час виграш від використання одного файлу був би подібним до того, який ми отримуємо за допомогою SSD замість жорстких дисків.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.