Dividere un grande progetto C ++

2

Come succede a molti, il nostro progetto C ++ è cresciuto sempre di più, e alla fine ha colpito il punto in cui la manutenibilità inizia a essere un problema, soprattutto a causa dei tempi di costruzione: anche utilizzando ccache , ogni modifica richiede da 30 da 5 a 5 minuti per ricostruire e testare (in realtà sembra che il "collegamento" sia la fase che richiede tempo, ma stiamo ancora indagando).

Il progetto consiste principalmente in una grande biblioteca scientifica, insieme ad alcune applicazioni che ne fanno uso. Ad esempio, ci sono applicazioni per aumentare i modelli di simulazione, o binding di linguaggio (python) per l'integrazione in altri pacchetti software.

Ora, sembrerebbe naturale dividere la libreria in librerie più piccole: il codice è già organizzato in "moduli", quindi questo non sarebbe un problema. Il problema è con la gestione delle dipendenze: possiamo pensare all'intero progetto come un grande DAG , dove ogni nodo è un modulo libreria o libreria esterna (a causa della natura del progetto, ci sono molte dipendenze esterne su altre librerie scientifiche).

Quindi, idealmente, quando lavoriamo su un modulo, vorremmo ricostruire solo quel modulo, più forse l'app di test correlata, e collegarlo alle sue dipendenze. Ciò accelererebbe considerevolmente il ciclo di sviluppo.

Di conseguenza, l'idea è di gestire ogni "modulo" come un progetto indipendente, che sarà costruito separatamente in una libreria (statica). Ho due dubbi su questo approccio:

  • Immagina di lavorare sull'app A che dipende dalla libreria B, che a sua volta dipende dalla libreria C. Durante lo sviluppo, trovo che ho bisogno di una nuova funzione nella libreria C, quindi vado avanti e codificalo. Ora devo ricostruire manualmente la libreria C, quindi la libreria B e infine la libreria A prima di continuare con lo sviluppo. Quando la catena delle dipendenze aumenta, questo può essere problematico.
  • Se cambiano le dipendenze di un modulo, come gestire l'impatto sull'intero DAG (altri moduli che cambiano di conseguenza)?

Ho svolto ricerche su diversi strumenti che potrebbero aiutare il mio scenario. I sottomoduli Git sono un'opzione, ma non affronta le mie preoccupazioni sopra. Apache Ivy non sembra adatto a quello che sto cercando di ottenere. Sospetto che la risposta implichi un potente sistema di build (stiamo usando qmake ), quindi ho guardato in cmake (ma ho trovato il suo linguaggio di scripting limitato e difficile da imparare), e quindi buck e pants .

Non sono ancora sicuro se questa è la strada da percorrere, quindi sarei grato se qualcuno potesse condividere consigli e / o esperienze.

    
posta matpen 08.11.2017 - 16:13
fonte

3 risposte

4

Hai menzionato le librerie statiche nella tua domanda. Sto indovinando (forse a torto) che si codifica un programma per Linux.

Dovresti utilizzare le librerie condivise quando possibile. Quindi è necessario molto meno tempo di collegamento (a scapito del collegamento di runtime, che di solito è abbastanza veloce, al momento dell'avvio).

During development, I find that I need a new feature in library C, so I go ahead and code that. I now have to manually rebuild library C, then library B and finally library A before continuing with the development.

Questo non dovrebbe essere il caso con le librerie condivise (purché la loro API non sia cambiata).

Considera forse alcune architetture che utilizzano i e quali carica dinamicamente (ad es. con dlopen su POSIX).

it seems that "linking" is the time-consuming phase

Devi essere sicuro che questo è il caso (ad es. passando -time o pari -ftime-report a g++ ). Quindi potresti utilizzare il linker oro e / o visibility funzione attributo con g++ .

Assicurati che le API dei tuoi sotto-moduli o librerie non cambino spesso.

Evita anche piccoli file sorgente C ++ , ad es. preferisco avere dieci file sorgente C ++ di duemila righe ciascuno invece di un centinaio di file sorgente C ++ di solo due centinaia di righe ciascuno. Un file sorgente C ++ può definire e implementare diverse funzioni o classi (correlate). Ricorda che C ++ non ha moduli e che la maggior parte degli header standard (e tuoi) sono file abbastanza grandi (ad esempio #include <vector> si espande a circa 10KLOC sul mio desktop Linux); ciò che conta per il tempo di compilazione è la dimensione del modulo preelaborato e le espansioni dei modelli richiedono molto tempo. Vedi anche questo .

BTW, devi anche utilizzare un buon strumento di automazione (ad esempio < a href="http://ninja-build.org/"> ninja , GNU make - con parallelizzazione abilitata con make -j 8 , ecc ...). Puoi anche utilizzare gli strumenti di sviluppo distribuito ( distcc , gelato , ...)

Potresti anche (con GCC ) considerare l'utilizzo di un intestazione precompilata . Quindi, probabilmente vorrai avere un singolo file di intestazione, inclusi tutti gli altri. Potresti voler utilizzare l' idioma PIMPL .

Vedi anche questa risposta a una domanda pertinente.

every change requires anywhere from 30 seconds to 5 minutes to rebuild and test

A proposito, un tempo di costruzione incrementale di cinque minuti mi sembra piuttosto piccolo. Perché ti lamenti? Sono vecchio abbastanza da aver avuto negli anni '90, su potenti workstation Sun di quel tempo, costruendo tempi di quasi un'ora (per un software di poche centinaia di migliaia di righe che ho scritto da solo per diversi anni). E a volte contribuisco allo stesso GCC anche in questi anni, il cui tempo di ricostruzione richiede diverse ore.

    
risposta data 09.11.2017 - 06:45
fonte
1

Non sei sicuro del tuo ambiente / budget, ma ecco alcuni suggerimenti:

  • Visual Studio 2017 in particolare con /MP , intestazioni precompilate , build incrementali e possibilmente moduli C ++
  • Incredibuild (mai usato personalmente)
  • Octobuild (mai usato personalmente)
  • Zapcc può essere molto più veloce dei compilatori tradizionali

Il solito consiglio vale per progetti singoli e multipli: esegui la migliore CPU + RAM + SSD (o anche ramdisk) che puoi acquistare, disabilitare la scansione dei virus in accesso e, se possibile, cambiare le intestazioni quando possibile.

    
risposta data 08.11.2017 - 19:27
fonte
1

Ci sono già ottime risposte relative ai sistemi di compilazione e alle librerie condivise, ma affronterò questo da un altro punto di vista. Non vedo le persone che si candidano così spesso.

Per me è utile separare il codice stabile (come improbabile che abbia mai bisogno di ulteriori cambiamenti) dal codice instabile (codice che naturalmente richiede ulteriori modifiche man mano che il software si espande). Se non puoi farlo e vedere la totalità della base di codice come parti potenzialmente mobili, anche se le interfacce sono completamente stabili, penso che stai facendo qualcosa di sbagliato.

Tendiamo già a farlo naturalmente per le librerie di terze parti open source. Generalmente li costruiamo una sola volta, creiamo un dylib in modo ideale per evitare i tempi di collegamento statici e basta cercare i simboli e richiamare le funzioni al loro interno in fase di runtime. Non troviamo la necessità di ricostruirli costantemente più e più volte poiché non abbiamo alcun interesse a toccare il loro codice sorgente, usando solo le funzionalità disponibili - fondamentalmente costruite una volta e usate per sempre. Il codice è al 100% stabile a tale riguardo (non c'è motivo per cui possa mai cambiare, almeno non nelle nostre mani).

Quindi per terze parti li ho inseriti personalmente in una sottodirectory "third_parties" e lo realizzo solo quando aggiungo una nuova libreria di terze parti o sostituisco una vecchia versione con una nuova versione che è estremamente rara, come una volta ogni pochi mesi minimo . Non li sto ricostruendo più e più volte ogni giorno.

La stessa cosa dovrebbe essere in grado di applicare alla propria base di codice. Ho una cosa simile in cui ho una sottodirectory "libs" con il mio codice e raramente costruisco le librerie risultanti dal momento che il codice è molto stabile, affidabile, efficiente, testato a fondo con i test unitari, passato attraverso l'analisi statica, e non qualcosa I bisogno di cambiare in futuro. È separato molto lontano dal codice instabile al di fuori della sottodirectory "libs", che ha bisogno di essere modificato su base giornaliera che viene ricostruito ripetutamente.

Separando così la base di codice in questo modo, dove parti stabili raramente, se non mai, devono cambiare da parti instabili che garantiscono costantemente le modifiche può essere un modo utile per organizzare la base di codice - non solo in termini di ottimizzazione dei tempi di costruzione ma in ordine per separare meglio i pacchetti stabili dai pacchetti instabili con l'obiettivo di finire con il maggior numero possibile di codice stabile che puoi dire con certezza che non garantirà alcuna modifica in un prossimo futuro.

Quando lo fai, quelle parti stabili della base di codice possono essere costruite separatamente e lontane dalle parti instabili che le usano. Le parti instabili non dovrebbero impiegare così tanto tempo per essere costruite man mano che la serie stabile di librerie si espande e si espande mentre le parti instabili si restringono e si restringono.

Tuttavia, questo tende a implicare la duplicazione del codice. Per essere in grado di creare una libreria di immagini stabile, non può dipendere, ad esempio, da una libreria matematica instabile, altrimenti le modifiche alla libreria matematica garantiranno una ricostruzione, se non ulteriori modifiche alla libreria di immagini. Quindi a volte mi sembra utile, ad esempio, una libreria di immagini per duplicare alcune routine matematiche per diventare indipendente da qualsiasi libreria matematica ausiliaria. In tal caso, il disaccoppiamento del codice in questo modo attraverso una modesta duplicazione della logica può effettivamente contribuire a rendere questi pacchetti molto più stabili, eliminando le ragioni per cui devono cambiare e / o essere ricostruiti più e più volte. Se il codice è ben collaudato e funziona magnificamente per gli anni a venire e non dovrebbe quasi mai essere ricostruito, la duplicazione modesta richiesta per la libreria per raggiungere la sua indipendenza e stabilità non è un problema.

Inoltre aiuta a raggiungere il minimalismo nelle tue interfacce. Se la tua libreria di immagini mira a implementare ogni singola operazione di immagine mai immaginabile per l'umanità, allora le sue ambizioni e la sua natura monolitica garantiranno cambiamenti infiniti. Non può mai sperare di diventare una biblioteca stabile con tali obiettivi. Se si prefigge solo di fornire operazioni di base sulle immagini o addirittura nessuna con la possibilità di creare operazioni di immagine al di fuori della libreria utilizzando ciò che la libreria fornisce, allora può potenzialmente raggiungere uno stato di perfetta stabilità (non trovando alcun motivo per cambiare in futuro).

Ad ogni modo, se hai intenzione di iniziare a suddividere una base di codice, ti suggerisco di iniziare a suddividere le parti stabili e ben collaudate in cui sei abbastanza sicuro di non aver bisogno di cambiarle ulteriormente dal parti instabili in cui si anticipano almeno alcune possibilità di necessità di modifiche future. La costruzione delle parti stabili dovrebbe richiamare un processo di compilazione separato (applicato raramente) dalle parti instabili (spesso ricostruito).

    
risposta data 29.11.2017 - 09:27
fonte

Leggi altre domande sui tag