Collegamento statico vs collegamento dinamico

Ci sono motivi di prestazioni convincenti per scegliere il collegamento statico sul collegamento dinamico o viceversa in determinate situazioni? Ho sentito o letto quanto segue, ma non ne so abbastanza sull’argomento da garantire per la sua veridicità.

1) La differenza nelle prestazioni di runtime tra il collegamento statico e il collegamento dinamico è generalmente trascurabile.

2) (1) non è vero se si utilizza un compilatore di profili che utilizza i dati del profilo per ottimizzare i hotpath del programma perché con il collegamento statico, il compilatore può ottimizzare sia il codice che il codice della libreria. Con il collegamento dinamico solo il tuo codice può essere ottimizzato. Se la maggior parte del tempo viene utilizzata per eseguire il codice della libreria, questo può fare una grande differenza. Altrimenti, (1) si applica ancora.

  • Il collegamento dinamico può ridurre il consumo totale di risorse (se più di un processo condivide la stessa libreria (inclusa la versione in “lo stesso”, ovviamente)). Credo che questo sia l’argomento che lo guida alla sua presenza nella maggior parte degli ambienti. Qui “risorse” include spazio su disco, RAM e spazio cache. Naturalmente, se il tuo linker dinamico non è sufficientemente flessibile, c’è il rischio di un inferno alle DLL .
  • Il collegamento dinamico significa che le correzioni dei bug e gli aggiornamenti alle librerie si propagano per migliorare il tuo prodotto senza che tu debba spedire nulla.
  • I plug-in richiedono sempre collegamenti dinamici .
  • Collegamento statico , significa che è ansible sapere che il codice verrà eseguito in ambienti molto limitati (all’inizio del processo di avvio o in modalità di ripristino).
  • Il collegamento statico può rendere i binari più facili da distribuire a diversi ambienti utente (al costo di inviare un programma di grandi e più affamati di risorse).
  • Il collegamento statico può consentire tempi di avvio leggermente più rapidi, ma ciò dipende in parte dalle dimensioni e dalla complessità del programma e dai dettagli della strategia di caricamento del sistema operativo.

Alcune modifiche per includere i suggerimenti molto pertinenti nei commenti e in altre risposte. Vorrei notare che il modo in cui si interrompe dipende molto da quale ambiente si intende eseguire. I sistemi integrati minimi potrebbero non disporre di risorse sufficienti per supportare il collegamento dinamico. Piccoli sistemi leggermente più grandi possono ben supportare il collegamento, perché la loro memoria è abbastanza piccola da rendere molto attraente il risparmio di RAM dal collegamento dinamico. Come noto Mark, i PC consumer in piena regola hanno enormi risorse e puoi probabilmente lasciare che i problemi di convenienza ti spingano a riflettere su questo argomento.


Per affrontare i problemi di prestazioni ed efficienza: dipende .

Classicamente, le librerie dinamiche richiedono un qualche tipo di strato di colla che spesso significa doppio invio o un ulteriore livello di riferimento indiretto nell’indirizzamento delle funzioni e può costare un po ‘di velocità (ma la funzione tempo di chiamata è in realtà una grande parte del tempo di esecuzione ???).

Tuttavia, se si eseguono più processi che chiamano tutti molto la stessa libreria, si può finire per risparmiare le linee della cache (e quindi vincere sulle prestazioni di esecuzione) quando si utilizza il collegamento dinamico relativo utilizzando il collegamento statico. (A meno che i moderni SO non siano abbastanza intelligenti da notare segmenti identici in binari collegati staticamente. Sembra difficile, qualcuno lo sa?)

Un altro problema: tempo di caricamento. Paghi i costi di caricamento ad un certo punto. Quando si paga questo costo dipende da come funziona il sistema operativo e da quale collegamento si utilizza. Forse preferiresti smettere di pagarlo finché non avrai la certezza che ne hai bisogno.

Si noti che il collegamento statico-dinamico non è tradizionalmente un problema di ottimizzazione, poiché entrambi coinvolgono la compilazione separata in file object. Tuttavia, questo non è necessario: un compilatore può, in linea di principio, “compilare” “librerie statiche” inizialmente su un modulo AST digerito e “collegarle” aggiungendo tali AST a quelli generati per il codice principale, in modo da potenziare l’ottimizzazione globale. Nessuno dei sistemi che uso lo fa, quindi non posso commentare come funziona.

Il modo per rispondere alle domande sulle prestazioni è sempre testando (e utilizzare un ambiente di test il più ansible simile all’ambiente di distribuzione).

Il collegamento dinamico è l’unico modo pratico per soddisfare alcuni requisiti di licenza come la LGPL .

1) si basa sul fatto che chiamare una funzione DLL utilizza sempre un salto extra indiretto. Oggi, questo è generalmente trascurabile. All’interno della DLL c’è un po ‘più di overhead su CPU i386, perché non possono generare codice indipendente dalla posizione. Su amd64, i salti possono essere relativi al contatore del programma, quindi questo è un enorme miglioramento.

2) Questo è corretto. Con le ottimizzazioni guidate dalla profilazione, in genere puoi vincere circa il 10-15% delle prestazioni. Ora che la velocità della CPU ha raggiunto i suoi limiti, potrebbe valere la pena farlo.

Vorrei aggiungere: (3) il linker può organizzare le funzioni in un raggruppamento più efficiente in termini di cache, in modo tale che i costosi errori di livello della cache siano ridotti al minimo. Potrebbe anche influire in modo particolare sul tempo di avvio delle applicazioni (basato sui risultati che ho visto con il compilatore Sun C ++)

E non dimenticare che con la DLL non è ansible eseguire l’eliminazione del codice morto. A seconda della lingua, anche il codice DLL potrebbe non essere ottimizzato. Le funzioni virtuali sono sempre virtuali perché il compilatore non sa se un client lo sta sovrascrivendo.

Per questi motivi, nel caso in cui non ci sia realmente bisogno di una DLL, basta usare la compilazione statica.

MODIFICA (per rispondere al commento, per sottolineatura dell’utente)

Ecco una buona risorsa sul problema del codice indipendente dalla posizione http://eli.thegreenplace.net/2011/11/03/position-independent-code-pic-in-shared-libraries/

Come spiegato, x86 non ha AFAIK per nient’altro che intervalli di salto di 15 bit e non per salti e chiamate incondizionati. Ecco perché le funzioni (dai generatori) che hanno più di 32K sono sempre state un problema e necessitavano di trampolini integrati.

Ma su un popolare sistema operativo x86 come Linux non devi preoccuparti se il file SO / DLL non viene generato con l’ -fpic gcc -fpic (che impone l’uso delle tabelle di salto indirette). Perché se non lo fai, il codice è corretto come un normale linker lo ricollocerebbe. Ma facendo ciò rende il segmento del codice non condivisibile e avrebbe bisogno di una mapping completa del codice dal disco in memoria e di toccarlo prima che possa essere utilizzato (svuotare la maggior parte delle cache, colpire i TLB) ecc. C’è stato un tempo quando questo è stato considerato lento … troppo lento.

Quindi non avresti più alcun beneficio.

Non ricordo quale sistema operativo (Solaris o FreeBSD) mi ha dato problemi con il mio sistema di costruzione Unix perché non stavo facendo questo e mi chiedevo perché si è bloccato fino a quando ho applicato -fPIC a gcc .

Sono d’accordo con i punti che dnmckee menziona, in più:

  • Le applicazioni collegate in modo statico potrebbero essere più facili da implementare, poiché ci sono meno o meno dipendenze di file aggiuntive (.dll / .so) che potrebbero causare problemi quando sono mancanti o installate nel posto sbagliato.

Uno dei motivi per eseguire una build collegata in modo statico è verificare la completa chiusura dell’eseguibile, ovvero che tutti i riferimenti ai simboli siano stati risolti correttamente.

Come parte di un grande sistema che veniva costruito e testato utilizzando l’integrazione continua, i test di regressione notturna venivano eseguiti utilizzando una versione degli eseguibili collegata in modo statico. Occasionalmente, si vedrebbe che un simbolo non si risolve e il collegamento statico non riuscirebbe anche se l’eseguibile collegato dynamicmente si collegasse correttamente.

Questo di solito si verificava quando i simboli che si trovavano in profondità all’interno delle librerie condivise avevano un nome errato e quindi non si collegavano in modo statico. Il linker dinamico non risolve completamente tutti i simboli, indipendentemente dall’uso della valutazione depth-first o breadth-first, quindi puoi finire con un eseguibile collegato dynamicmente che non ha una chiusura completa.

1 / Sono stato in progetti in cui il collegamento dinamico vs il collegamento statico era stato valutato e la differenza non era stata determinata abbastanza piccola da passare al collegamento dinamico (non facevo parte del test, conosco solo la conclusione)

2 / Il collegamento dinamico è spesso associato a PIC (Position Independent Code, codice che non necessita di essere modificato a seconda dell’indirizzo in cui è caricato). A seconda dell’architettura, PIC può portare un altro rallentamento ma è necessario per trarre vantaggio dalla condivisione di una libreria collegata dynamicmente tra due eseguibili (e anche due processi dello stesso eseguibile se il sistema operativo utilizza la randomizzazione dell’indirizzo di carico come misura di sicurezza). Non sono sicuro che tutti i sistemi operativi consentano di separare i due concetti, ma Solaris e Linux fanno e ISTR che anche HP-UX fa.

3 / Sono stato su altri progetti che utilizzavano il collegamento dinamico per la funzione “easy patch”. Ma questa “patch facile” rende la distribuzione di piccole correzioni un po ‘più semplice e complicata da un incubo di versioning. Spesso finivamo per dover spingere tutto oltre a dover tenere traccia dei problemi sul sito del cliente perché la versione sbagliata era un token.

La mia conclusione è che ho usato il collegamento statico tranne:

  • per cose come i plugin che dipendono dal collegamento dinamico

  • quando la condivisione è importante (grandi librerie utilizzate da più processi allo stesso tempo come il runtime C / C ++, le librerie GUI, … che spesso vengono gestite in modo indipendente e per le quali l’ABI è rigorosamente definito)

Se si vuole usare la “patch facile”, direi che le librerie devono essere gestite come le grandi librerie di cui sopra: devono essere quasi indipendenti con un ABI definito che non deve essere modificato dalle correzioni.

Questo discute dettagliatamente sulle librerie condivise su Linux e sull’implicazione delle prestazioni.

Nei sistemi di tipo Unix, il collegamento dinamico può rendere difficile la vita di ‘root’ per utilizzare un’applicazione con le librerie condivise installate in posizioni fuori mano. Questo perché il linker dinamico generalmente non presta attenzione a LD_LIBRARY_PATH o al suo equivalente per i processi con privilegi di root. A volte, quindi, il collegamento statico salva la giornata.

In alternativa, il processo di installazione deve individuare le librerie, ma ciò può rendere difficile la coesistenza di più versioni del software sulla macchina.

È piuttosto semplice, davvero. Quando apporti una modifica al tuo codice sorgente, vuoi aspettare 10 minuti per la creazione o 20 secondi? Venti secondi sono tutto ciò che posso sopportare. Oltre a ciò, o esco dalla spada o comincio a pensare a come posso usare compilation e link separati per riportarlo nella zona di comfort.

Il collegamento dinamico richiede più tempo al sistema operativo per trovare la libreria dynamic e caricarla. Con il collegamento statico, tutto è insieme ed è un caricamento one-shot in memoria.

Inoltre, vedi DLL Hell . Questo è lo scenario in cui la DLL caricata dal sistema operativo non è quella fornita con l’applicazione o la versione prevista dall’applicazione.

Il miglior esempio per il collegamento dinamico è quando la libreria dipende dall’hardware utilizzato. Nei tempi antichi la libreria di matematica C è stata decisa per essere dynamic, in modo che ogni piattaforma possa utilizzare tutte le funzionalità del processore per ottimizzarla.

Un esempio ancora migliore potrebbe essere OpenGL. OpenGl è un’API implementata in modo diverso da AMD e NVidia. E non sei in grado di utilizzare un’implementazione NVidia su una scheda AMD, perché l’hardware è diverso. Non puoi colbind staticamente OpenGL al tuo programma, a causa di ciò. Il collegamento dinamico viene utilizzato qui per consentire all’API di essere ottimizzata per tutte le piattaforms.

Un altro problema non ancora discusso è la risoluzione dei bug nella libreria.

Con il collegamento statico, non devi solo ribuild la libreria, ma dovrai ricolbind e ridistribuire l’eseguibile. Se la libreria è usata solo in un file eseguibile, questo potrebbe non essere un problema. Ma più eseguibili devono essere ricollegati e ridistribuiti, più grande è il dolore.

Con il collegamento dinamico, basta ribuild e ridistribuire la libreria dynamic e il gioco è fatto.

collegamento statico ti dà solo un singolo exe, inorder per fare un cambiamento è necessario ricompilare l’intero programma. Mentre nel collegamento dinamico è necessario apportare modifiche solo alla DLL e quando si esegue il file EXE, le modifiche verranno rilevate in fase di runtime. È più facile fornire aggiornamenti e correzioni di errori tramite collegamenti dinamici (ad esempio: Windows).

Esiste un numero vasto e crescente di sistemi in cui un livello estremo di collegamento statico può avere un enorme impatto positivo sulle applicazioni e sulle prestazioni del sistema.

Mi riferisco a quelli che vengono spesso chiamati “sistemi embedded”, molti dei quali ora usano sempre più sistemi operativi di uso generale, e questi sistemi sono usati per tutto ciò che si può immaginare.

Un esempio estremamente comune sono i dispositivi che utilizzano sistemi GNU / Linux che utilizzano Busybox . L’ho portato all’estremo con NetBSD creando un’immagine di sistema i386 (a 32 bit) avviabile che include sia un kernel che il suo filesystem di root, quest’ultimo che contiene un singolo binario statico (da crunchgen ) con hard-link a tutti i programmi che contiene tutti (ben oltre il 274) dei programmi di sistema standard completi (la maggior parte tranne la toolchain), e ha dimensioni inferiori a 20 mega byte (e probabilmente funziona molto comodamente in un sistema con solo 64 MB di memoria (anche con il filesystem di root non compresso e interamente in RAM), anche se non sono riuscito a trovarne uno così piccolo da testarlo).

È stato menzionato nei post precedenti che il tempo di avvio di un binario statico è più veloce (e può essere molto più veloce), ma questa è solo una parte dell’immagine, specialmente quando tutto il codice object è collegato allo stesso file, e ancor più specialmente quando il sistema operativo supporta la richiesta di paging del codice direttamente dal file eseguibile. In questo scenario ideale il tempo di avvio dei programmi è letteralmente trascurabile poiché quasi tutte le pagine di codice saranno già in memoria e saranno in uso dalla shell (e init qualsiasi altro processo in background che potrebbe essere in esecuzione), anche se il programma richiesto è in esecuzione non è mai stato eseguito sin dall’avvio poiché forse è necessario caricare solo una pagina di memoria per soddisfare i requisiti di runtime del programma.

Comunque non è ancora tutta la storia. Di solito, inoltre, creo e utilizzo le installazioni del sistema operativo NetBSD per i miei sistemi di sviluppo completi tramite il collegamento statico di tutti i file binari. Anche se ciò richiede un’enorme quantità di spazio su disco (circa 6,6 GB totali per x86_64 con tutto, inclusi toolchain e X11 collegati in modo statico) (specialmente se si mantengono le tabelle di simboli di debug complete disponibili per tutti i programmi di altri ~ 2,5 GB), il risultato rimane gira più veloce in generale, e per alcune attività utilizza anche meno memoria di un tipico sistema con collegamento dinamico che pretende di condividere le code page della libreria. Il disco è economico (anche disco veloce), e la memoria per memorizzare i file disco utilizzati frequentemente è anche relativamente economica, ma i cicli della CPU in realtà non lo sono, e il pagamento del costo di avvio ld.so per ogni processo che inizia ogni volta che inizia richiederà ore e le ore di cicli della CPU si allontanano dalle attività che richiedono l’avvio di molti processi, specialmente quando gli stessi programmi vengono utilizzati più e più volte, come i compilatori su un sistema di sviluppo. I programmi di toolchain collegati in modo statico possono ridurre le ore di costruzione multi-architettura dell’intero sistema operativo per i miei sistemi di ore . Devo ancora build la toolchain nel mio singolo crunchgen ‘ed binary, ma sospetto che quando lo farò ci saranno più ore di tempo di salvataggio risparmiate a causa della vittoria per la cache della CPU.

Il collegamento statico include i file necessari al programma in un singolo file eseguibile.

Il collegamento dinamico è ciò che consideri normale, rende un eseguibile che richiede ancora DLL e tale deve trovarsi nella stessa directory (o le DLL potrebbero trovarsi nella cartella di sistema).

(DLL = libreria di collegamento dinamico )

Gli eseguibili collegati dynamicmente sono compilati più velocemente e non sono così pesanti in termini di risorse.