Dlaczego niektóre programy C są zapisane w jednym wielkim pliku źródłowym?

88

Na przykład, SysInternals narzędzie „FileMon” z przeszłości ma sterownik trybu jądra, którego kod źródłowy jest w całości w jednym pliku 4000 linii. To samo dotyczy pierwszego w historii napisanego programu ping (~ 2000 LOC).

Otręby
źródło

Odpowiedzi:

143

Korzystanie z wielu plików zawsze wymaga dodatkowego obciążenia administracyjnego. Trzeba skonfigurować skrypt kompilacji i / lub plik makefile z oddzielnymi etapami kompilacji i łączenia, upewnić się, że zależności między różnymi plikami są poprawnie zarządzane, napisać skrypt „zip” dla łatwiejszej dystrybucji kodu źródłowego przez e-mail lub do pobrania, i tak dalej na. Współczesne środowiska IDE zwykle obciążają wiele tego obciążenia, ale jestem pewien, że w czasie, gdy napisano pierwszy program ping, takie środowisko IDE nie było dostępne. A w przypadku plików tak małych jak ~ 4000 LOC, bez takiego IDE, które dobrze zarządza wieloma plikami, kompromis między wspomnianym kosztem ogólnym a korzyściami wynikającymi z używania wielu plików może pozwolić ludziom podjąć decyzję o podejściu opartym na jednym pliku.

Doktor Brown
źródło
9
„A dla plików tak małych jak ~ 4000 LOC ...” Obecnie pracuję jako programista JS. Kiedy mam plik o długości zaledwie 400 linii kodu, denerwuję się, jak duży jest jego rozmiar! (Ale w naszym projekcie mamy dziesiątki plików.)
Kevin
36
@Kevin: jedno włosy na głowie to za mało, jedno włosy w mojej zupie to za dużo ;-) AFAIK w JS wiele plików nie powoduje tak dużego obciążenia administracyjnego jak w „C bez nowoczesnego IDE”.
Doc Brown
4
@Kevin JS jest jednak zupełnie inną bestią. JS jest przesyłany do użytkownika końcowego za każdym razem, gdy użytkownik ładuje stronę internetową i nie ma go już w pamięci podręcznej przeglądarki. C musi tylko raz przesłać kod, a następnie osoba po drugiej stronie go skompiluje i pozostanie skompilowana (oczywiście są wyjątki, ale jest to ogólny oczekiwany przypadek użycia). Również C jest zwykle starszym kodem, podobnie jak wiele z „4000 linii jest normalne” projektów, które ludzie opisują w komentarzach.
Pharap
5
@Kevin Teraz idź i zobacz, jak zapisywane są underscore.js (1700 loc, jeden plik) i mnóstwo innych dystrybuowanych bibliotek. JavaScript jest prawie tak samo zły jak C pod względem modularyzacji i wdrażania.
Voo
2
@Pharap Myślę, że miał na myśli użycie czegoś takiego jak Webpack przed wdrożeniem kodu. Dzięki Webpack możesz pracować na wielu plikach, a następnie skompilować je w jeden pakiet.
Brian McCutchon
81

Ponieważ C nie jest dobry w modularyzacji. Robi się bałagan (pliki nagłówkowe i #include, funkcje zewnętrzne, błędy czasu łącza itp.), A im więcej modułów wprowadzasz, tym trudniej robi się.

Bardziej nowoczesne języki mają po części lepsze możliwości modularyzacji, ponieważ nauczyły się na błędach języka C i ułatwiają podział bazy kodu na mniejsze, prostsze jednostki. Ale w przypadku C korzystne może być uniknięcie lub zminimalizowanie wszystkich tych problemów, nawet jeśli oznacza to zbicie tego, co w innym przypadku byłoby zbyt dużym kodem w jednym pliku.

Mason Wheeler
źródło
38
Myślę, że niesprawiedliwe jest opisywanie podejścia C jako „błędów”; w momencie ich podejmowania były całkowicie rozsądne i rozsądne decyzje.
Jack Aidley,
14
Żadna z tych rzeczy związanych z modularyzacją nie jest szczególnie skomplikowana. Może być wykonany komplikuje złego kodowania stylu, ale to nie jest trudne do zrozumienia lub wdrożenia, a żaden z nich mogą być zaklasyfikowane jako „błędy”. Prawdziwym powodem, zgodnie z odpowiedzią Snowmana, jest to, że optymalizacja wielu plików źródłowych nie była tak dobra w przeszłości, a sterownik FileMon wymaga wysokiej wydajności. Ponadto, w przeciwieństwie do opinii OP, nie są to szczególnie duże pliki.
Graham
8
@Graham Każdy plik większy niż 1000 linii kodu powinien być traktowany jako zapach kodu.
Mason Wheeler,
11
@JackAidley jej nie nieuczciwy w ogóle , że coś jest błędem nie jest wzajemna wykluczają z mówiąc, że to rozsądna decyzja w tym czasie. Błędy są nieuniknione, biorąc pod uwagę niedoskonałe informacje i ograniczony czas, i należy ich wyciągać z nieskromnie ukrytych lub przeklasyfikowanych, aby uratować twarz.
Jared Smith
8
Każdy, kto twierdzi, że podejście C nie jest błędem, nie rozumie, w jaki sposób pozornie dziesięcioliniowy plik C może faktycznie być dziesięciotysięczny plik ze wszystkimi nagłówkami #include: d. Oznacza to, że każdy plik w twoim projekcie ma efektywnie co najmniej dziesięć tysięcy linii, bez względu na to, ile liczb linii podano przez „wc -l”. Lepsze wsparcie dla modułowości z łatwością skróci czas analizy i kompilacji na niewielki ułamek.
juhist
37

Oprócz powodów historycznych istnieje jeden powód, aby użyć tego w nowoczesnym oprogramowaniu wrażliwym na wydajność. Gdy cały kod znajduje się w jednej jednostce kompilacyjnej, kompilator może przeprowadzać optymalizacje całego programu. Przy osobnych jednostkach kompilacyjnych kompilator nie może zoptymalizować całego programu w określony sposób (np. Wstawiając określony kod).

Linker z pewnością może wykonać pewne optymalizacje oprócz tego, co potrafi kompilator, ale nie wszystkie. Na przykład: nowoczesne konsolidatory są naprawdę dobre w tworzeniu funkcji, do których nie ma odniesienia, nawet w plikach wielu obiektów. Mogą być w stanie wykonać inne optymalizacje, ale nic podobnego do tego, co kompilator może zrobić wewnątrz funkcji.

Jednym dobrze znanym przykładem modułu kodu z jednym kodem źródłowym jest SQLite. Możesz przeczytać więcej na ten temat na stronie The SQLite Amalgamation .

1. Streszczenie

Ponad 100 oddzielnych plików źródłowych łączy się w pojedyncze duże pliki kodu C o nazwie „sqlite3.c” i nazywane „amalgamacją”. Połączenie zawiera wszystko, czego aplikacja potrzebuje do osadzenia SQLite. Plik połączenia ma ponad 180 000 linii długości i ponad 6 megabajtów.

Połączenie całego kodu SQLite w jeden duży plik ułatwia wdrożenie SQLite - jest tylko jeden plik do śledzenia. A ponieważ cały kod znajduje się w jednej jednostce tłumaczącej, kompilatory mogą przeprowadzić lepszą optymalizację między procedurami, dzięki czemu kod maszynowy jest od 5% do 10% szybszy.


źródło
15
Należy jednak pamiętać, że współczesne kompilatory C mogą przeprowadzać optymalizację całego programu wielu plików źródłowych (chociaż nie, jeśli najpierw skompilujesz je do poszczególnych plików obiektowych).
Davislor
10
@ Davislor Spójrz na typowy skrypt kompilacji: kompilatory nie zamierzają tego zrobić.
4
Znacznie łatwiej jest zmienić skrypt kompilacji na $(CC) $(CFLAGS) $(LDFLAGS) -o $(TARGET) $(CFILES)niż przenieść wszystko do jednego pliku soudce. Możesz nawet wykonać kompilację całego programu jako alternatywny cel dla tradycyjnego skryptu kompilacji, który pomija rekompilację plików źródłowych, które nie uległy zmianie, podobnie jak ludzie mogą wyłączyć profilowanie i debugowanie dla celu produkcyjnego. Nie masz tej opcji, jeśli wszystko znajduje się w jednym źródle dużej sterty. Ludzie nie są do tego przyzwyczajeni, ale nie ma w tym nic uciążliwego.
Davislor
9
@Davislor optymalizacja całego programu / optymalizacja czasu łącza (LTO) działa również, gdy „kompilujesz” kod do pojedynczych plików obiektowych (w zależności od tego, co oznacza dla ciebie „kompilacja”). Na przykład LTO GCC doda reprezentację parsowanego kodu do poszczególnych plików obiektowych w czasie kompilacji, aw czasie łączenia użyje tego zamiast (również obecnego) kodu obiektowego do ponownej kompilacji i budowy całego programu. Działa to więc z konfiguracjami kompilacji, które najpierw kompilują się do poszczególnych plików obiektowych, chociaż kod maszynowy wygenerowany przez kompilację początkową jest ignorowany.
Dreamer
8
JsonCpp robi to również w dzisiejszych czasach. Kluczem jest to, że pliki nie są w ten sposób podczas programowania.
Wyścigi lekkości na orbicie
15

Oprócz czynnika prostoty, o którym wspomniał drugi respondent, wiele programów w języku C jest napisanych przez jedną osobę.

Gdy masz zespół osób, pożądane jest podzielenie aplikacji na kilka plików źródłowych, aby uniknąć nieuzasadnionych konfliktów w zmianach kodu. Zwłaszcza, gdy w projekcie pracują zarówno zaawansowani, jak i bardzo młodsi programiści.

Gdy jedna osoba pracuje sama, nie stanowi to problemu.

Osobiście używam wielu plików w zależności od funkcji jako zwyczajowej rzeczy. Ale to tylko ja.

Ron Ruble
źródło
4
@OskarSkog Ale nigdy nie zmodyfikujesz pliku w tym samym czasie, co twoje przyszłe ja.
Loren Pechtel
2

Ponieważ C89 nie miał inlinefunkcji. Co oznaczało, że rozbicie pliku na funkcje spowodowało narzut związany z wypychaniem wartości na stos i przeskakiwaniem. To dodało sporo narzutu związanego z implementacją kodu w 1 dużej instrukcji switch (pętla zdarzeń). Ale pętla zdarzeń jest zawsze o wiele trudniejsza do wydajnego (lub nawet poprawnego) wdrożenia niż rozwiązanie bardziej modułowe. Dlatego w przypadku dużych projektów ludzie nadal decydują się na modułowość. Ale kiedy mieli przemyślany projekt z wyprzedzeniem i mogli kontrolować stan w instrukcji 1 przełącznika, zdecydowali się na to.

W dzisiejszych czasach, nawet w C, nie trzeba poświęcać wydajności w celu modularyzacji, ponieważ nawet w funkcjach C można wprowadzić.

Dmitrij Rubanowicz
źródło
2
Funkcje C mogą być tak samo wbudowane w 89, jak w dzisiejszych czasach, inline jest czymś, z czego należy korzystać prawie nigdy - kompilator wie lepiej niż ty w prawie wszystkich sytuacjach. I większość tych 4k plików LOC nie jest jedną gigantyczną funkcją - to okropny styl kodowania, który również nie przyniesie zauważalnej poprawy wydajności.
Voo,
@ Voo, nie wiem, dlaczego wspominasz o stylu kodowania. Nie popierałem tego. W rzeczywistości wspomniałem, że w większości przypadków gwarantuje to mniej wydajne rozwiązanie z powodu nieudanej implementacji. Wspomniałem również, że to zły pomysł, ponieważ nie można go skalować (do większych projektów). To powiedziawszy, w bardzo ciasnych pętlach (co dzieje się w prawie sprzętowym kodzie sieciowym) niepotrzebne wypychanie i upuszczanie wartości na stosie / wyłączaniu stosu (podczas wywoływania funkcji) zwiększy koszt uruchomionego programu. To nie było świetne rozwiązanie. Ale to był najlepszy dostępny w tym czasie.
Dmitrij Rubanowicz
2
Uwaga obowiązkowa: wbudowane słowo kluczowe ma niewiele wspólnego z optymalizacją wbudowaną. Nie jest to żadna specjalna wskazówka dla kompilatora, aby wykonać tę optymalizację, zamiast tego dotyczy łączenia z powielonymi symbolami.
hyde
@Dmitry Chodzi o to, że twierdzenie, że ponieważ nie było inlinesłowa kluczowego w kompilatorach C89, nie mogło być inline, dlatego właśnie trzeba było napisać wszystko w jednej gigantycznej funkcji. Prawie nigdy nie powinieneś używać inlinejako optymalizacji wydajności - kompilator i tak będzie wiedział lepiej niż ty (i równie dobrze może zignorować słowo kluczowe).
Voo,
@Voo: Programista i kompilator na ogół znają pewne rzeczy, których inni nie wiedzą. Słowo inlinekluczowe ma semantykę związaną z linkerem, która jest ważniejsza niż pytanie, czy należy przeprowadzać optymalizacje w linii, ale niektóre implementacje mają inne dyrektywy kontrolujące wstawianie i takie rzeczy mogą czasami być bardzo ważne. W niektórych przypadkach funkcja może wyglądać, jakby była zbyt duża, aby była warta wstawienia, ale ciągłe składanie może skrócić rozmiar i czas wykonania prawie do zera. Kompilator, który nie ma silnego nacisku, aby zachęcić do
wstawiania,
1

To liczy się jako przykład ewolucji, co mnie dziwi, nie zostało jeszcze wspomniane.

W ciemnych czasach programowania kompilacja jednego PLIKU może zająć minuty. Jeśli program został zmodularyzowany, wówczas dołączenie niezbędnych plików nagłówkowych (brak wstępnie skompilowanych opcji nagłówka) stanowiłoby znaczącą dodatkową przyczynę spowolnienia. Dodatkowo kompilator może wybrać / wymagać przechowywania niektórych informacji na samym dysku, prawdopodobnie bez korzyści z automatycznego pliku wymiany.

Zwyczaje, które te czynniki środowiskowe przełożyły na ciągłe praktyki rozwojowe i z czasem powoli się dostosowywały.

W tym czasie zysk z korzystania z jednego pliku byłby podobny do tego, jaki uzyskujemy dzięki zastosowaniu dysków SSD zamiast dysków HDD.

itj
źródło