Perché alcuni programmi C sono scritti in un enorme file sorgente?


88

Ad esempio, lo strumento SysInternals "FileMon" del passato ha un driver in modalità kernel il cui codice sorgente è interamente in un file di 4.000 righe. Lo stesso per il primo programma di ping mai scritto (~ 2.000 LOC).

Risposte:


143

L'uso di più file richiede sempre un sovraccarico amministrativo aggiuntivo. Uno deve impostare uno script di compilazione e / o makefile con fasi di compilazione e collegamento separate, assicurarsi che le dipendenze tra i diversi file siano gestite correttamente, scrivere uno script "zip" per una più facile distribuzione del codice sorgente via e-mail o download, e così via sopra. Gli IDE moderni oggi in genere assumono molto di quel peso, ma sono abbastanza sicuro al momento in cui è stato scritto il primo programma di ping, tale IDE non era disponibile. E per file piccoli come ~ 4000 LOC, senza un simile IDE che gestisce bene più file per te, il compromesso tra l'overhead menzionato e i vantaggi derivanti dall'uso di più file potrebbe consentire alle persone di prendere una decisione per l'approccio a file singolo.


9
"E per file piccoli come ~ 4000 LOC ..." Sto lavorando come sviluppatore JS in questo momento. Quando ho un file lungo solo 400 righe di codice, mi innervosisco di quanto sia diventato grande! (Ma abbiamo dozzine e dozzine di file nel nostro progetto.)
Kevin

36
@Kevin: un pelo sulla mia testa è troppo pochi, un pelo nella mia zuppa è troppi ;-) AFAIK in JS più file non causa così tanto sovraccarico amministrativo come in "C senza un IDE moderno".
Doc Brown

4
@Kevin JS è comunque una bestia abbastanza diversa. JS viene trasmesso a un utente finale ogni volta che un utente carica un sito Web e non lo ha già memorizzato nella cache dal proprio browser. C deve solo trasmettere il codice una volta, quindi la persona all'altro capo lo compila e rimane compilato (ovviamente ci sono eccezioni, ma questo è il caso d'uso previsto generale). Anche le cose C tendono ad essere un codice legacy, come lo sono molti dei progetti "4000 lines is normal" che le persone descrivono nei commenti.
Pharap

5
@Kevin Ora vai a vedere come sono scritti underscore.js (1700 loc, un file) e una miriade di altre librerie distribuite. Javascript è in realtà quasi cattivo quanto C per quanto riguarda la modularizzazione e la distribuzione.
Voo

2
@Pharap Penso che intendesse usare qualcosa come Webpack prima di distribuire il codice. Con Webpack, puoi lavorare su più file e quindi compilarli in un unico pacchetto.
Brian McCutchon,

81

Perché C non è bravo nella modularizzazione. Diventa disordinato (file di intestazione e #include, funzioni esterne, errori di link-time, ecc.) E più moduli porti, più diventa complicato.

I linguaggi più moderni hanno migliori capacità di modularizzazione in parte perché hanno imparato dagli errori di C e semplificano la suddivisione della base di codice in unità più piccole e più semplici. Ma con C, può essere utile evitare o minimizzare tutti quei problemi, anche se ciò significa raggruppare ciò che altrimenti verrebbe considerato troppo codice in un singolo file.


38
Penso che sia ingiusto descrivere l'approccio C come "errori"; erano decisioni perfettamente sensate e ragionevoli al momento in cui venivano prese.
Jack Aidley,

14
Nessuna di queste cose di modularizzazione è particolarmente complicata. Può essere fatta complicata da cattivo stile di codifica, ma non è difficile da capire o implementare, e nessuna di esse potrebbe essere classificato come "errori". Il vero motivo, secondo la risposta di Snowman, è che l'ottimizzazione su più file di origine non era così buona in passato e che il driver FileMon richiede prestazioni elevate. Inoltre, contrariamente al parere del PO, quelli non sono file particolarmente grandi.
Graham

8
@Graham Qualsiasi file di dimensioni superiori a 1000 righe di codice deve essere trattato come un odore di codice.
Mason Wheeler

11
@JackAidley la sua non è ingiusto a tutti , avere qualcosa sia un errore non è reciproca esclusiva con dicendo che era una decisione ragionevole, al momento. Gli errori sono inevitabili dati informazioni imperfette e tempo limitato e dovrebbero essere appresi da non vergognosamente nascosti o riclassificati per salvare la faccia.
Jared Smith

8
Chiunque sostenga che l'approccio di C non sia un errore non riesce a capire come un file C apparentemente di 10 righe possa effettivamente essere un file di diecimila righe con tutte le intestazioni #include: d. Ciò significa che ogni singolo file nel tuo progetto è effettivamente almeno diecimila righe, indipendentemente da quanto sia il conteggio delle righe dato da "wc -l". Un migliore supporto per la modularità ridurrebbe facilmente i tempi di analisi e compilazione in una piccola frazione.
juhist

37

A parte le ragioni storiche, c'è un motivo per usarlo nei moderni software sensibili alle prestazioni. Quando tutto il codice si trova in un'unità di compilazione, il compilatore è in grado di eseguire ottimizzazioni dell'intero programma. Con unità di compilazione separate, il compilatore non può ottimizzare l'intero programma in determinati modi (ad es. Incorporando un determinato codice).

Il linker può certamente eseguire alcune ottimizzazioni oltre a ciò che il compilatore può fare, ma non tutti. Ad esempio: i moderni linker sono davvero bravi a eludere le funzioni senza riferimenti, anche su più file oggetto. Potrebbero essere in grado di eseguire alcune altre ottimizzazioni, ma nulla di simile a quello che un compilatore può fare all'interno di una funzione.

Un esempio ben noto di un modulo di codice a sorgente singola è SQLite. Puoi leggere di più a riguardo nella pagina The SQLite Amalgamation .

1. Sintesi

Oltre 100 file di origine separati vengono concatenati in un unico file di grandi dimensioni di codice C denominato "sqlite3.c" e chiamato "fusione". La fusione contiene tutto ciò di cui un'applicazione ha bisogno per incorporare SQLite. Il file di fusione è lungo più di 180.000 righe e ha dimensioni superiori a 6 megabyte.

La combinazione di tutto il codice per SQLite in un unico grande file semplifica l'implementazione di SQLite: c'è solo un file da tenere traccia. E poiché tutto il codice si trova in una singola unità di traduzione, i compilatori possono eseguire una migliore ottimizzazione tra procedure, ottenendo un codice macchina compreso tra il 5% e il 10% più veloce.


15
Ma nota che i moderni compilatori C possono eseguire l'ottimizzazione dell'intero programma di più file sorgente (anche se non se li compili prima in singoli file oggetto).
Davislor

10
@Davislor Guarda il tipico script di compilazione: i compilatori non lo faranno realisticamente.

4
È significativamente più semplice cambiare uno script di build $(CC) $(CFLAGS) $(LDFLAGS) -o $(TARGET) $(CFILES)piuttosto che spostare tutto in un singolo file soudce. Puoi persino eseguire la compilazione dell'intero programma come destinazione alternativa allo script di compilazione tradizionale che salta la ricompilazione dei file di origine che non sono cambiati, in modo simile al modo in cui le persone potrebbero disattivare la creazione di profili e il debug per la destinazione di produzione. Non hai questa opzione se tutto è in un'unica grande risorsa. Non è ciò a cui le persone sono abituate, ma non c'è nulla di ingombrante al riguardo.
Davislor

9
L'ottimizzazione dell'intero programma / ottimizzazione link-time (LTO) di @Davislor funziona anche quando si "compila" il codice in singoli file oggetto (a seconda del significato di "compilazione"). Ad esempio, l'LTO di GCC aggiungerà la sua rappresentazione di codice analizzata ai singoli file oggetto in fase di compilazione, e al momento del collegamento utilizzerà quello anziché il codice oggetto (anche presente) per ricompilare e costruire l'intero programma. Quindi funziona con le configurazioni di compilazione che vengono compilate prima in singoli file oggetto, sebbene il codice macchina generato dalla compilazione iniziale venga ignorato.
Dreamer

8
JsonCpp lo fa anche al giorno d'oggi. La chiave è che i file non sono così durante lo sviluppo.
Razze di leggerezza in orbita

15

Oltre al fattore di semplicità menzionato dall'altro rispondente, molti programmi C sono scritti da un individuo.

Quando hai un team di persone, diventa desiderabile dividere l'applicazione su più file sorgente per evitare conflitti gratuiti nelle modifiche al codice. Soprattutto quando ci sono programmatori avanzati e molto junior che lavorano al progetto.

Quando una persona lavora da sola, non è un problema.

Personalmente, utilizzo più file in base alla funzione come una cosa abituale. Ma sono solo io.


4
@OskarSkog Ma non modificherai mai un file contemporaneamente a te stesso.
Loren Pechtel,

2

Perché C89 non aveva inlinefunzioni. Ciò significava che suddividere il file in funzioni causava il sovraccarico di spingere i valori in pila e saltare. Ciò ha aggiunto un po 'di sovraccarico all'implementazione del codice in 1 istruzione switch di grandi dimensioni (loop di eventi). Ma un loop di eventi è sempre molto più difficile da implementare in modo efficiente (o persino corretto) rispetto a una soluzione più modulare. Quindi, per i progetti di grandi dimensioni, la gente opterebbe comunque per la modularizzazione. Ma quando hanno avuto il progetto pensato in anticipo e sono stati in grado di controllare lo stato in 1 istruzione switch, hanno optato per quello.

Al giorno d'oggi, anche in C, non è necessario sacrificare le prestazioni per modularizzare perché anche nelle funzioni C possono essere incorporate.


2
In 89 le funzioni C potrebbero essere tanto in linea quanto in questi giorni, inline è qualcosa che non dovrebbe essere usato quasi mai - il compilatore conosce meglio di te in quasi tutte le situazioni. E la maggior parte di quei file LOC 4K non sono una funzione gigantesca - è uno stile di programmazione orribile che non avrà alcun vantaggio evidente sulle prestazioni.
Voo

@Voo, non so perché accenni allo stile di programmazione. Non lo stavo sostenendo. In effetti, ho detto che nella maggior parte dei casi garantisce una soluzione meno efficiente a causa di un'implementazione mal riuscita. Ho anche detto che è una cattiva idea perché non si adatta (a progetti più grandi). Detto questo, in loop molto stretti (che è ciò che accade nel codice di rete vicino all'hardware), spingere e far esplodere inutilmente i valori sullo stack (quando si chiamano le funzioni) aumenterà il costo del programma in esecuzione. Questa non è stata un'ottima soluzione. Ma era il migliore disponibile al momento.
Dmitry Rubanovich

2
Nota obbligatoria: la parola chiave inline ha poco a che fare con l'ottimizzazione inline. Non è un suggerimento speciale per il compilatore di fare tale ottimizzazione, ma ha a che fare con il collegamento con simboli duplicati.
hyde,

@Dmitry Il punto è che affermare che non esistendo una inlineparola chiave nei compilatori C89 non potesse essere in linea, motivo per cui è stato necessario scrivere tutto in un'unica funzione gigante non è corretto. Praticamente non dovresti mai usare inlinecome ottimizzazione delle prestazioni - il compilatore saprà comunque meglio di te (e può anche ignorare la parola chiave).
Voo

@Voo: un programmatore e un compilatore generalmente sapranno ciascuno delle cose che l'altro non sa. La inlineparola chiave ha una semantica relativa al linker che è più importante della questione se eseguire o meno le ottimizzazioni in linea, ma alcune implementazioni hanno altre direttive per il controllo in linea e tali cose a volte possono essere molto importanti. In alcuni casi, una funzione può sembrare troppo grande per essere degna di essere allineata, ma la piegatura costante potrebbe ridurre le dimensioni e il tempo di esecuzione a quasi nulla. Un compilatore a cui non viene dato un forte impulso per incoraggiare l'in-lining potrebbe non ...
Supercat

1

Questo è un esempio di evoluzione, che mi sorprende non è ancora stato menzionato.

Nei giorni bui della programmazione, la compilazione di un singolo FILE potrebbe richiedere alcuni minuti. Se un programma fosse modularizzato, l'inclusione dei file di intestazione necessari (senza opzioni di intestazione precompilate) sarebbe una causa aggiuntiva significativa di rallentamento. Inoltre, il compilatore potrebbe scegliere / necessitare di conservare alcune informazioni sul disco stesso, probabilmente senza il vantaggio di un file di scambio automatico.

Le abitudini che questi fattori ambientali hanno portato a trasferire nelle pratiche di sviluppo in corso e si sono lentamente adattate nel tempo.

Al momento, il guadagno derivante dall'uso di un singolo file sarebbe simile a quello ottenuto dall'utilizzo di SSD anziché HDD.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.