GCC: il miglior compilatore al mondo?

Questa frase, pronunciata però in maniera affermativa, è stata oggetto di discussione qualche tempo fa nell’area Programmazione del forum del nostro sito “madre”, dove un utente tesseva le lodi del compilatore a cui ha dato i natali Richard Stallman.

Non entrerò nel merito di quel thread-valanga (come capita nelle migliori guerre di religione, specialmente quando di mezzo ci sono questioni tecniche), ma ne approfitto per uno spunto di riflessione, che mi trascino già da parecchio tempo, su questo compilatore (o su questa collezione di compilatori, come ormai recita l’acronimo).

Generalmente quattro sono le qualità che servono a discriminare la bontà di un compilatore:

  • prestazioni del codice compilato
  • dimensione dei binari (siano essi file oggetto o eseguibili)
  • velocità di compilazione
  • architetture e sistemi operativi supportati

Le priorità non vanno dalla prima all’ultima, ma dipendono strettamente dall’utilizzatore. Quindi se ho messo le prestazioni davanti a tutte, non è perché devono essere il primo metro di paragone. Ad esempio a me normalmente interessa la velocità di compilazione perché influenza direttamente la mia produttività come programmatore; se, quando tutto funziona, voglio ottenere un eseguibile più veloce e/o di dimensione più contenuta per la build finale, sono disposto a perderci un po’ di tempo in più.

La dimensione diventa importante a seconda del target scelto per la distribuzione del prodotto. Può non essere un problema se il veicolo scelto è un disco Blu-Ray, ad esempio, perché lo spazio che è possibile utilizzare si misura nell’ordine delle decine di GB (ma si può saturare anche quello). Diventa critica se il prodotto lo si dovrà scaricare, invece, con su un telefonino tramite connessione WAP che può essere più o meno lenta, gratuita o a pagamento.

Supportare più architetture / piattaforme può essere inutile, un valore aggiunto, o un requisito indispensabile. Anche qui, manco a dirlo, tutto dipende da quello che vogliamo realizzare. Ad esempio, se voglio realizzare un videogioco per PC, molto probabilmente non m’interesserà questa funzionalità, visto che generalmente si sviluppa per Windows e le architetture x86 (che domina) e/o AMD64 (che ne rappresenta il futuro prossimo). Se, invece, il mio target è rappresentato dai telefonini, come minimo sarà indispensabile il supporto agli ARM.

Una quinta voce che non ho menzionato sarebbe l’affidabilità, ma si tratta di una caratteristica che non dovrebbe nemmeno essere in discussione: da un compilatore, al pari di un altro componente estremamente importante quale può essere un filesystem, ci si aspetta che sia solido e produca codice che faccia fede all’originale sorgente per quanto riguarda i calcoli eseguiti; e che, ovviamente, non presenti problemi che non siano riconducibili al sorgente stesso.

Detto ciò, il “miglior” compilatore al mondo non esiste sostanzialmente per due ragioni. La prima, lapalissiana, perché le esigenze di chi lo usa non sono tutte le stesse, come già accennato in precedenza; io posso, e ho, necessità diverse rispetto a un altro programmatore, inoltre possono benissimo variare in base al progetto a cui sto lavorando.

Secondo, è praticamente impossibile conciliare tutte e quattro le qualità; si tratta di un classico problema di ottimizzazione su delle variabili correlate che non può fornire i migliori risultati per tutte. Tanto per fare qualche esempio, se voglio ottenere le migliori prestazioni è chiaro che dovrò obbligatoriamente spendere più tempo a ottimizzare il codice finale, e ciò incide per forza di cose sulla velocità di compilazione. Se, invece, m’interessa contenere la dimensione dell’eseguibile, dovrò rinunciare in parte alle prestazioni perché, ad esempio, non potrò sfruttare un aggressivo inline delle funzioni.

Fornita la risposta al quesito esposto nel titolo, penso sia utile dare qualche spunto per vedere come si comporta GCC rispetto alle variabili elencate, in modo da farsi un’idea e poter decidere, in base a ciò serve a ognuno, se val la pena utilizzarlo oppure cercare uno strumento che “calzi meglio”.

Per quanto riguarda le prestazioni GCC non ha certo brillato. D’altra parte nei primi di anni di vita di un progetto è più importante farlo funzionare correttamente piuttosto che perdere tempo alla sua ottimizzazione; citando una massima di Kent Beck, “Make it work, make it right, make it fast“. Le sue ultime versioni sono sostanzialmente comparabili a Visual Studio C++, anche se non ho trovato confronti con l’ultima versione di quest’ultima (la 2010).

Di seguito riporto una serie di link ordinati temporalmente che mostrano alcuni risultati e il relativo grado di maturità raggiunto:

Ovviamente con questi benchmark non ho la pretesa di giudicare globalmente i risultati dei compilatori testati. Risultano, comunque, abbastanza variegati per tipologia applicativa, e possono essere utili per farsi un’idea. Fermo restando che, in fin dei conti, gli unici confronti utili rimangono quelli fatti con applicazioni relative ai nostri, specifici, interessi.

Dal quadro emergono alcuni punti: il primo, abbastanza scontato, è che il compilatore Intel è risultato essere sempre imbattibile. Il secondo è che i miglioramenti di GCC sono stati notevoli, anche se hanno comportato un lungo periodo (forse un po’ troppo) di sviluppo. Il terzo, ma l’avevo già accennato, è che il confronto con Visual Studio s’è fatto serrato con le ultime versioni (dalla 4.3 in poi).

Il quarto è che, come si vede dall’ultimo link, non sempre si ottengono prestazioni migliori rispetto alle versioni precedenti, a volte con risultati nettamente inferiori; sono, insomma, presenti delle regressioni (come si suol dire in questi casi), che a quanto pare sfuggono agli sviluppatori ufficiali (e a questo punto è lecito dubitare che esistano delle suite test di regressione, come si vedrà più avanti, parlando dei problemi con alcune architetture).

Relativamente alla dimensione degli eseguibili ho trovato poco in giro, anche perché, purtroppo, il metro di paragone prediletto rimane quello delle prestazioni. Di seguito un paio di utili link (qualche informazione appare anche nel precedente link “Benchmarking With 64 Bits“):

Il compilatore Intel genera gli eseguibili di dimensione più elevata, ma questo era anche prevedibile, se consideriamo che nel binario vengono incluse porzioni di codice specifiche (chiamate “code path” in gergo) per un certo numero di suoi processori. GCC genera codice di dimensione inferiore, ma sempre superiore rispetto a Visual Studio.

La nota dolente è rappresentata dal secondo link, che porta a un ticket del bugtrack (non ancora corretto né preso in carico da qualche sviluppatore) del compilatore, che evidenzia un problema della release 4.5, la quale al momento genera eseguibili e/o codice oggetto di dimensione notevolmente aumentata rispetto alle versioni precedenti.

Anche per quanto riguarda i tempi di compilazione si trova poco materiale. Sempre Aaron Giles, nel suo blog, ha scritto un post qualche tempo fa con dei test di build sulla sua nuova (all’epoca) macchina, dove risultano tempi superiori per una build di debug, decisamente elevati per una di release, ma inferiori rispetto alla release altamente ottimizzata di Visual Studio.

Qualche indicazione più recente si trova anche nel precedente link “Building Qt Static (and Dynamic) and Making it Small with GCC, Microsoft Visual Studio, and the Intel Compiler“, dove risulta che il compilatore Intel è di gran lunga il più lento (il che è in linea con quanto ci si aspetterebbe, perché perderà molto tempo in fase di ottimizzazione). Visual Studio si conferma essere il più veloce anche nella generazione di codice ottimizzato, impiegando 1/3 del tempo rispetto a GCC.

Dal canto mio avrei voluto provare a compilare Python (2.6.4) per misurare i tempi di build sulla mia macchina in ufficio, dove ho installato anche Cygwin e, quindi, GCC, ma purtroppo i tentativi di compilazione di progetti di una certa dimensione sono miseramente falliti, in quanto vengono lanciati diversi processi che prosciugano la memoria della macchina, costringendomi a ucciderli manualmente.

In ogni caso riguardo ai tempi di compilazione troppo lunghi si lamentano anche i miei colleghi in ufficio; “mal comune mezzo gaudio”, come si suol dire. Questo è anche uno dei motivi che ha portato NetBSD a concentrare le sue forze su un compilatore alternativo, PCC, che risulterebbe essere dalle 5 alle 10 volte più veloce, pur generando codice di buona qualità.

Rimane l’ultimo punto, quello del supporto alle architetture e/o s.o. diversi, e qui mi sembra scontato che GCC vinca a mani basse, supportando di tutto ed essendo praticamente onnipresente. Intel, ovviamente, supporta soltanto le sue architetture (x86, x86-64, e Itanium), tant’è che, anche per x86 e x86-64, produce codice ottimizzato che funziona esclusivamente per i suoi processori (e non per quelli di AMD o altri vendor).

VisualStudio Express supporta soltanto x86. Per x86-64, Itanium, e ARM, serve la versione Professional. Anche qui è evidente che Microsoft supporti le architetture esclusivamente a suffragio del suo prodotto principe, Windows (in tutte le sue forme, inclusa quella mobile dove impera ARM).

Non è, però, tutto rose e fiori per GCC, visto che da un po’ di tempo a questa parte la qualità del codice generato per architetture diverse da quelle più gettonate (x86 in particolare) risulta essere peggiorata notevolmente, specialmente per quanto riguarda la famiglia Motorola 68000:

Ma, a giudicare dai cambiamenti apportati al generatore di codice, di cui si trovano degli esempi eloquenti nel primo e nel terzo link, probabilmente altre architetture saranno state toccate da questi interventi.

La cosa più grave è, che, nonostante le segnalazioni siano state effettuate da parecchio tempo, non si è provveduto a sistemare questi bug; anzi, i ticket non vengono nemmeno assegnati. Ma c’è di peggio. Come si può leggere dal primo commento di questa pagina (è la seconda del primo link), i mantainer ufficiali sembrano pure incompetenti e arroganti a fronte degli sforzi fatti da gente interessata a far migliorare GCC (segnalazioni effettuate e patch fornite, come si può leggere dalla prima pagina).

Il risultato è che, similmente al più famoso caso delle libc (dove il mantainer ufficiale si rifiutò in malo modo di correggere un bug che creava problemi con l’architettura ARM, costringendo gli sviluppatori di Debian a eseguirne un fork), i programmatori hanno deciso di passare a un altro compilatore che facesse fronte alle loro esigenze (come per il caso NetBSD di cui ho parlato prima).

Quello dei bug segnalati ma non sistemati è un problema ben noto (basta controllare i link ai ticket, oppure fare qualche ricerca nel bugtracker), sebbene spesso si senta ripetere il solito mantra che nei progetti open source vengano fixati velocemente. Anche in progetti grossi e con una vasta comunità dietro, com’è GCC, ciò non è sempre vero. Al solito, dipende dall’interesse dei mantainer della relativa area.

Purtroppo capita anche con bug che mettono in discussione la quinta, scontata, voce: quella dell’affidabilità. Da qualche tempo è tornato, infatti, alla ribalta un vecchio bug, quello relativo ai crash dovuti alla (mancanza di) allineamento dello stack, quando si compila ottimizzando per le istruzioni SSE. Parlo di roba vecchia perché è da molto che si trascina, e ogni tanto rispunta quella che viene denominata “rottura dell’ABI x86″:

Particolarmente interessanti sono gli interventi di Agner Fog, noto esperto dell’architettura x86, che chiarisce il problema e ribadisce il rispetto dell’ABI x86, oppure ne suggerisce un aggiornamento onde evitare questi inconvenienti particolarmente rognosi.

Non è, infatti, accettabile che si verifichino dei crash a causa della selezione di un’opzione di compilazione più aggressiva. Compilando una libreria dinamica, l’interfaccia da essa esposta è e dev’essere più che sufficiente per poterne effettuare il linking senza alcuna ripercussione. Altrimenti viene meno proprio il concetto di “interfaccia”, che serve a fornire un “contratto” di utilizzo (e a “nascondere” l’implementazione).

Una soluzione ad alcuni problemi potrebbe essere quello di modularizzare il codice, magari caricando al volo soltanto quelli che servono quando si deve compilare per una determinata architettura (mi riferisco sia all’ottimizzatore che al generatore di codice), che contribuirebbe a un miglioramento dei tempi di compilazione e prestazionali (ad esempio l’isolamento impedirebbe che i cambiamenti all’ottimizzatore x86 influenzassero quelli del 68000). Questo lo renderebbe più manutenibile, considerato che al momento è enorme, e il codice non brilla certo per “qualità”.

Sempre a proposito di codice, sarebbe auspicabile che certi mantainer “poco ortodossi”, e magari non all’altezza, fossero allontanati, per il bene del progetto e, soprattutto, di chi ne usufruisce. Non credo che nella numerosa comunità di GCC si faccia fatica a trovare dei rimpiazzi migliori…

Press ESC to close