Un compilador optimitzador és un compilador dissenyat per generar codi optimitzat en aspectes com ara minimitzar el temps d'execució del programa, l'ús de memòria, la mida d'emmagatzematge i el consum d'energia. L'optimització s'implementa generalment com una seqüència de transformacions optimitzadores, també conegudes com a optimitzacions del compilador, és a dir algorismes que transformen el codi per produir codi semànticament equivalent optimitzat per a algun aspecte.
L'optimització està limitada per diversos factors. L'anàlisi teòrica indica que alguns problemes d'optimització són NP-complets o fins i tot indecidibles. A més, produir codi perfectament òptim no és possible, ja que optimitzar per a un aspecte sovint degrada el rendiment per a un altre (vegeu: superoptimització). L'optimització és un conjunt de mètodes heurístics per millorar l'ús de recursos en programes típics.: 585
Categorització Àmbit local vs. global L'abast descriu quina part del codi d'entrada es considera per aplicar optimitzacions.
Les optimitzacions d'abast local utilitzen informació local a un bloc bàsic. Com que els blocs bàsics no contenen instruccions de flux de control, aquestes optimitzacions requereixen una anàlisi mínima, cosa que redueix els requisits de temps i emmagatzematge. Tanmateix, no es conserva cap informació entre salts.
Les optimitzacions d'abast global, també conegudes com a optimitzacions intraprocedimentals, operen sobre funcions individuals. Això els dóna més informació amb la qual treballar, però sovint fa que calguin càlculs costosos. Cal fer suposicions en el pitjor dels casos quan es produeixen crides de funcions o s'accedeix a variables globals, ja que hi ha poca informació disponible sobre elles.
Optimització de l'espiell Les optimitzacions de mirilla normalment es duen a terme al final del procés de compilació, després que s'hagi generat el codi màquina. Aquesta optimització examina unes quantes instruccions adjacents (de manera similar a "mirar el codi a través d'una mirilla") per veure si es poden substituir per una sola instrucció o per una seqüència d'instruccions més curta.: 554 Per exemple, una multiplicació d'un valor per dos es podria executar de manera més eficient desplaçant el valor a l'esquerra o sumant el valor a si mateix (aquest exemple també és un cas de reducció de força).
Optimització interprocedimental Les optimitzacions interprocedimentals analitzen tot el codi font d'un programa. Com més informació hi hagi disponible, més efectives poden ser les optimitzacions. La informació es pot utilitzar per a diverses optimitzacions, inclosa la funció inline, on una crida a una funció es substitueix per una còpia del cos de la funció.
Optimització del temps d'enllaç L'optimització en temps d'enllaç (LTO), o optimització de tot el programa, és una classe més general d'optimització interprocedimental. Durant l'LTO, el compilador té visibilitat a través de les unitats de traducció, cosa que li permet realitzar optimitzacions més agressives com ara la integració entre mòduls i la desvirtualització.
Optimització de codi màquina i objecte L'optimització de codi màquina implica l'ús d'un optimitzador de codi objecte per analitzar el programa després que tot el codi màquina s'hagi enllaçat. Tècniques com la compressió de macros, que estalvien espai condensant seqüències d'instruccions comunes, es tornen més efectives quan tota la imatge de la tasca executable està disponible per a l'anàlisi.
Independent de la llengua vs. dependent de la llengua La majoria dels llenguatges de programació d'alt nivell comparteixen construccions i abstraccions de programació comunes, com ara construccions de ramificació (if, switch), construccions de bucle (for, while) i construccions d'encapsulació (estructures, objectes). Per tant, es poden utilitzar tècniques d'optimització similars entre llenguatges. Tanmateix, certes característiques del llenguatge dificulten algunes optimitzacions. Per exemple, els punters en C i C++ dificulten l'optimització de matrius; vegeu anàlisi d'àlies. Tanmateix, llenguatges com PL/I que també admeten punters implementen optimitzacions per a matrius. Per contra, algunes característiques del llenguatge faciliten certes optimitzacions. Per exemple, en alguns llenguatges, les funcions no poden tenir efectes secundaris. Per tant, si un programa fa diverses crides a la mateixa funció amb els mateixos arguments, el compilador pot inferir que el resultat de la funció només s'ha de calcular una vegada. En llenguatges on es permet que les funcions tinguin efectes secundaris, el compilador pot restringir aquesta optimització a funcions que pot determinar que no tenen efectes secundaris.
Independent de la màquina vs. dependent de la màquina Moltes optimitzacions que operen sobre conceptes de programació abstractes (bucles, objectes, estructures) són independents de la màquina a la qual es dirigeix el compilador, però moltes de les optimitzacions més efectives són les que millor exploten les característiques especials de la plataforma de destinació. Alguns exemples són les instruccions que fan diverses coses alhora, com ara decrementar el registre i fer una bifurcació si no és zero.
El següent és un exemple d'optimització dependent de la màquina local. Per establir un registre a 0, la manera òbvia és utilitzar la constant '0' en una instrucció que estableix un valor de registre a una constant. Una manera menys òbvia és fer una operació XOR d'un registre amb si mateix o restar-lo de si mateix. Depèn del compilador saber quina variant d'instrucció utilitzar. En moltes màquines RISC, ambdues instruccions serien igualment apropiades, ja que tindrien la mateixa longitud i trigarien el mateix temps. En molts altres microprocessadors com la família Intel x86, resulta que la variant XOR és més curta i probablement més ràpida, ja que no caldrà descodificar un operand immediat ni utilitzar el "registre d'operand immediat" intern; el mateix s'aplica a IBM System/360 i successors per a la variant de resta. Un problema potencial amb això és que l'operació XOR o la resta poden introduir una dependència de dades sobre el valor anterior del registre, provocant una aturada de la canonada, que es produeix quan el processador ha de retardar l'execució d'una instrucció perquè depèn del resultat d'una instrucció anterior. Tanmateix, els processadors sovint tracten el XOR d'un registre amb si mateix o la resta d'un registre de si mateix com un cas especial que no causa bloquejos.
Factors que afecten l'optimització Màquina objectiu Si es poden i s'han d'aplicar optimitzacions particulars pot dependre de les característiques de la màquina de destinació. Alguns compiladors com ara GCC i Clang parametritzen factors dependents de la màquina perquè es puguin utilitzar per optimitzar per a diferents màquines.
Arquitectura de la CPU de destinació
Nombre de registres: Els registres es poden utilitzar per optimitzar el rendiment. Les variables locals es poden emmagatzemar en registres en lloc de a la pila. Es pot accedir als resultats temporals/intermedis en registres en lloc de memòria més lenta. RISC vs. CISC: Els conjunts d'instruccions CISC sovint tenen longituds d'instrucció variables, sovint tenen un nombre més gran d'instruccions possibles que es poden utilitzar, i cada instrucció pot trigar diferents quantitats de temps. Els conjunts d'instruccions RISC intenten limitar la variabilitat en cadascun d'aquests: els conjunts d'instruccions solen ser de longitud constant, amb poques excepcions, normalment hi ha menys combinacions de registres i operacions de memòria, i la taxa d'emissió d'instruccions (el nombre d'instruccions completades per període de temps, normalment un múltiple enter del cicle de rellotge) sol ser constant en els casos en què la latència de memòria no és un factor. Hi pot haver diverses maneres de dur a terme una determinada tasca, i CISC sol oferir més alternatives que RISC. Els compiladors han de conèixer els costos relatius entre les diverses instruccions i triar la millor seqüència d'instruccions (vegeu selecció d'instruccions). Canonades: Una canonada és una CPU dividida en una línia de muntatge. Permet l'ús de parts de la CPU per a diferents instruccions dividint l'execució d'instruccions en diverses etapes: descodificació d'instruccions, descodificació d'adreces, recuperació de memòria, recuperació de registres, càlcul, emmagatzematge de registres, etc. Una instrucció podria estar a l'etapa d'emmagatzematge de registres, mentre que una altra podria estar a l'etapa de recuperació de registres. Els conflictes de canonada es produeixen quan una instrucció en una etapa de la canonada depèn del resultat d'una altra instrucció anterior a la canonada però que encara no s'ha completat. Els conflictes de canonada poden provocar bloquejos de la canonada: on la CPU malgasta cicles esperant que es resolgui un conflicte. Els compiladors poden programar o reordenar les instruccions de manera que les bloquejos de la canonada es produeixin amb menys freqüència. Nombre d'unitats funcionals: Algunes CPU tenen diverses ALU i FPU que els permeten executar diverses instruccions simultàniament. Hi pot haver restriccions sobre quines instruccions es poden emparellar amb quines altres instruccions ("emparellament" és l'execució simultània de dues o més instruccions) i quina unitat funcional pot executar quina instrucció. També tenen problemes similars als conflictes de pipeline. Les instruccions es poden programar de manera que les unitats funcionals estiguin completament carregades.
Arquitectura de màquines
Mida i tipus de memòria cau de la CPU (mapejada directament, associativa de 2/4/8/16 vies, totalment associativa): Tècniques com l'expansió en línia i el desenrotllament de bucles poden augmentar la mida del codi generat i reduir la localitat del codi. El programa es pot alentir dràsticament si una secció de codi molt utilitzada (com els bucles interns en diversos algoritmes) ja no cap a la memòria cau com a resultat d'optimitzacions que augmenten la mida del codi. A més, les memòries cau que no són completament associatives tenen més probabilitats de col·lisions de memòria cau, fins i tot en una memòria cau no plena. Taxes de transferència de memòria cau/memòria: Aquestes proporcionen al compilador una indicació de la penalització per errors de memòria cau. Això s'utilitza principalment en aplicacions especialitzades.
Ús previst
Depuració: Durant el desenvolupament, les optimitzacions sovint es desactiven per accelerar la compilació o per facilitar la depuració del codi executable. L'optimització de les transformacions, en particular les que reordenen el codi, pot dificultar la relació del codi executable amb el codi font. Ús general: sovint s'espera que el programari preempaquetat s'executi en diverses màquines que poden compartir el mateix conjunt d'instruccions però tenir característiques de rendiment diferents. És possible que el codi no estigui optimitzat per a cap màquina en particular o que estigui ajustat per funcionar millor a la màquina més popular mentre que funciona de manera menys òptima en altres. Ús amb finalitats especials: si el programari es compila per a màquines amb característiques uniformes, el compilador pot optimitzar en gran manera el codi generat per a aquestes màquines.
Alguns casos destacables són el codi dissenyat per a processadors paral·lels i vectorials, per als quals s'utilitzen compiladors de paral·lelització especials.