Compilador optimitzador

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.[1]

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.[2] 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.:[3] 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.[4] 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.[5] 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.:[6] 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.[7] 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.[8]

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,[9] 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.

El firmware d'un sistema integrat es pot optimitzar per a la CPU i la memòria de destinació. El cost o la fiabilitat del sistema poden ser més importants que la velocitat del codi. Per exemple, els compiladors per a programari integrat solen oferir opcions que redueixen la mida del codi a costa de la velocitat. El temps del codi pot necessitar ser predictible, en lloc de ser el més ràpid possible, de manera que l'emmagatzematge en memòria cau del codi pot estar desactivat, juntament amb les optimitzacions del compilador que ho requereixin.

Temes comuns

L'optimització inclou els temes següents, de vegades contradictoris.

Optimitzar el cas comú
El cas comú pot tenir propietats úniques que permeten un camí ràpid a costa d'un camí lent. Si el camí ràpid es pren més sovint, el resultat és un millor rendiment general.
Evitar la redundància
Reutilitzar els resultats que ja s'han calculat i emmagatzemar-los per a un ús posterior, en comptes de tornar-los a calcular.
Menys codi
Elimina els càlculs innecessaris i els valors intermedis. Menys treball per a la CPU, la memòria cau i la memòria normalment resulta en una execució més ràpida. Alternativament, en els sistemes integrats, menys codi comporta un cost del producte més baix.
Menys salts utilitzant codi de línia recta, també anomenat codi sense ramificacions
Codi menys complicat. Els salts (branques condicionals o incondicionals) interfereixen amb la precàrrega d'instruccions, cosa que alenteix el codi. L'ús de l'enllaçament en línia o el desplegament de bucles pot reduir la ramificació, a costa d'augmentar la mida del fitxer binari per la longitud del codi repetit. Això tendeix a fusionar diversos blocs bàsics en un de sol.
Localitat
El codi i les dades a les quals s'accedeix a curta distància en el temps s'han de col·locar a prop de la memòria per augmentar la localitat espacial de referència.
Explotar la jerarquia de memòria
Els accessos a la memòria són cada cop més cars per a cada nivell de la jerarquia de memòria, així que col·loqueu els elements més utilitzats primer als registres, després a les memòries cau i després a la memòria principal, abans d'anar al disc.
Paral·lelitzar
Reordenar les operacions per permetre que diversos càlculs es produeixin en paral·lel, ja sigui a nivell d'instrucció, memòria o fil.
Millor la informació més precisa
Com més precisa sigui la informació que tingui el compilador, millor podrà emprar qualsevol o totes aquestes tècniques d'optimització.
Les mètriques de temps d'execució poden ajudar
La informació recopilada durant una execució de prova es pot utilitzar en l'optimització guiada per perfils. La informació recopilada en temps d'execució, idealment amb una sobrecàrrega mínima, pot ser utilitzada per un compilador JIT per millorar dinàmicament l'optimització.
Reducció de força
Substitueix operacions complexes, difícils o cares per operacions més senzilles. Per exemple, substitueix la divisió per una constant per la multiplicació pel seu recíproc, o utilitza l'anàlisi de variables d'inducció per substituir la multiplicació per un índex de bucle per la suma.

Tècniques específiques

Optimitzacions de bucles

L'optimització de bucles actua sobre les instruccions que formen un bucle, com ara un bucle for, per exemple, el moviment de codi invariant en bucles. Les optimitzacions de bucles poden tenir un impacte significatiu perquè molts programes passen un gran percentatge del seu temps dins de bucles.:[10] 596 

Algunes tècniques d'optimització dissenyades principalment per operar en bucles inclouen:

Anàlisi de variables d'inducció
A grans trets, si una variable d'un bucle és una funció lineal simple de la variable d'índex, com ara j := 4*i + 1, es pot actualitzar adequadament cada vegada que es canvia la variable de bucle. Això és una reducció de força i també pot permetre que les definicions de la variable d'índex esdevinguin codi mort.:[11] 596–598 Aquesta informació també és útil per a la comprovació de límits, l'eliminació i l'anàlisi de dependències, entre altres coses.
Fissió en bucle o distribució en bucle
La fissió de bucles intenta trencar un bucle en múltiples bucles sobre el mateix rang d'índex, on cada nou bucle només pren una part del cos del bucle original. Això pot millorar la localitat de la referència tant a les dades a les quals s'accedeix dins del bucle com al codi del cos del bucle.
Fusió de bucles o combinació de bucles o pissó de bucles o interferència de bucles
Una altra tècnica que intenta reduir la sobrecàrrega del bucle. Quan dos bucles adjacents iterarien el mateix nombre de vegades independentment de si aquest nombre es coneix o no en temps de compilació, els seus cossos es poden combinar sempre que no facin referència a les dades de l'altre.
Inversió de bucle
Aquesta tècnica canvia un bucle while estàndard en un bucle do/while (també conegut com a repeat/until) embolicat en un condicional if, reduint el nombre de salts en dos, per als casos en què s'executa el bucle. Fer-ho duplica la comprovació de la condició (augmentant la mida del codi) però és més eficient perquè els salts solen provocar una aturada del pipeline. A més, si la condició inicial es coneix en temps de compilació i se sap que no té efectes secundaris, es pot ometre la protecció if.
Intercanvi de bucle
Aquestes optimitzacions intercanvien bucles interns amb bucles externs. Quan les variables del bucle s'indexen en una matriu, aquesta transformació pot millorar la localitat de la referència, depenent de la disposició de la matriu.
Moviment de codi invariant en bucle
Si una quantitat es calcula dins d'un bucle durant cada iteració, i el seu valor és el mateix per a cada iteració, es pot millorar enormement l'eficiència si es treu fora del bucle i es calcula el seu valor només una vegada abans que comenci el bucle.:[12] 596  Això és particularment important amb les expressions de càlcul d'adreces generades per bucles sobre matrius. Per a una implementació correcta, aquesta tècnica s'ha d'utilitzar amb inversió de bucle, perquè no tot el codi és segur per ser aixecat fora del bucle.
Optimització del niu de bucles
Alguns algoritmes omnipresents com la multiplicació de matrius tenen un comportament de memòria cau molt deficient i accessos excessius a memòria. L'optimització del niu de bucles augmenta el nombre de visites a la memòria cau operant sobre blocs petits i utilitzant un intercanvi de bucles.
Inversió de bucle
La inversió de bucles inverteix l'ordre en què s'assignen els valors a la variable d'índex. Aquesta és una optimització subtil que pot ajudar a eliminar dependències i, per tant, permetre altres optimitzacions. A més, en algunes arquitectures, la inversió de bucles contribueix a un codi més petit, ja que quan es decrementa l'índex del bucle, la condició que s'ha de complir perquè el programa en execució surti del bucle és una comparació amb zero. Sovint es tracta d'una instrucció especial sense paràmetres, a diferència d'una comparació amb un nombre, que necessita el nombre amb què comparar-se. Per tant, la quantitat de bytes necessaris per emmagatzemar el paràmetre s'estalvia mitjançant la inversió de bucles. A més, si el nombre de comparació supera la mida de la paraula de la plataforma, en l'ordre estàndard dels bucles, caldrien executar diverses instruccions per avaluar la comparació, cosa que no passa amb la inversió de bucles.
Desenrotllament del bucle
Desplegar duplica el cos del bucle diverses vegades per disminuir el nombre de vegades que es prova la condició del bucle i el nombre de salts; les proves i els salts poden afectar el rendiment deteriorant la cadena d'instruccions. Una optimització de "menys salts". Desplegar completament un bucle elimina tota la sobrecàrrega, però requereix que el nombre d'iteracions es conegui en temps de compilació.
Divisió de bucles
La divisió de bucles intenta simplificar un bucle o eliminar dependències dividint-lo en diversos bucles que tenen els mateixos cossos però que iteren sobre diferents porcions contigües del rang d'índex. Un cas especial útil és el "loop peeling", que pot simplificar un bucle amb una primera iteració problemàtica realitzant aquesta iteració per separat abans d'entrar al bucle.
Desconnexió del bucle
En desactivar la commutació, es mou un condicional de dins d'un bucle a fora del bucle duplicant el cos del bucle dins de cadascuna de les clàusules if i else del condicional.
Canalització de programari
El bucle es reestructura de manera que el treball realitzat en una iteració es divideix en diverses parts i es fa al llarg de diverses iteracions. En un bucle ajustat, aquesta tècnica amaga la latència entre la càrrega i l'ús de valors.
Paral·lelització automàtica
Un bucle es converteix en codi multifil o vectoritzat (o fins i tot tots dos) per utilitzar diversos processadors simultàniament en una màquina multiprocessador de memòria compartida (SMP), incloses les màquines multinucli.

Optimitzacions de botigues Prescient

Les optimitzacions d'emmagatzematge premonitòries permeten que les operacions d'emmagatzematge es produeixin abans del que es permetria en el context de fils i bloquejos. El procés necessita alguna manera de saber per endavant quin valor emmagatzemarà l'assignació que hauria d'haver seguit. L'objectiu d'aquesta relaxació és permetre que l'optimització del compilador realitzi certs tipus de reordenaments de codi que preservin la semàntica dels programes correctament sincronitzats.[13]

Optimitzacions del flux de dades

Les optimitzacions del flux de dades, basades en l'anàlisi del flux de dades, depenen principalment de com certes propietats de les dades es propaguen per les vores de control del graf del flux de control. Algunes d'aquestes inclouen:

Eliminació comuna de subexpressions
En l'expressió (a + b) - (a + b)/4, la "subexpressió comuna" fa referència a la (a + b) duplicada. Els compiladors que implementen aquesta tècnica s'adonen que (a + b) no canviarà i, per tant, només calculen el seu valor una vegada.:[14] 592–594
Plegament i propagació constants
Substitució d'expressions que consisteixen en constants (per exemple, 3 + 5 ) pel seu valor final ( 8 ) en temps de compilació, en lloc de fer el càlcul en temps d'execució.[15] S'utilitza a la majoria de llenguatges moderns.
Reconeixement i eliminació de variables d'inducció
Vegeu la discussió anterior sobre l'anàlisi de variables d'inducció
Classificació d'àlies i anàlisi de punters
En presència de punters, és difícil fer cap optimització, ja que potencialment qualsevol variable pot haver estat modificada quan s'assigna una ubicació de memòria. En especificar quins punters poden fer d'àlies de quines variables, es poden ignorar els punters no relacionats.
Eliminació de botigues mortes
Eliminació d'assignacions a variables que no es llegeixen posteriorment, ja sigui perquè finalitza el període de vida de la variable o a causa d'una assignació posterior que sobreescriurà el primer valor.

Optimitzacions basades en SSA

Aquestes optimitzacions estan pensades per fer-se després de transformar el programa en una forma especial anomenada forma estàtica d'assignació única, en què cada variable s'assigna en un sol lloc. Tot i que algunes funcionen sense SSA, són més efectives amb SSA. Moltes optimitzacions enumerades en altres seccions també es beneficien sense canvis especials, com ara l'assignació de registres.

Numeració de valors globals
El GVN elimina la redundància construint un gràfic de valors del programa i, a continuació, determinant quins valors es calculen mitjançant expressions equivalents. El GVN pot identificar alguna redundància que l'eliminació comuna de subexpressions no pot, i viceversa.
Propagació constant condicional dispersa
Combina la propagació constant, el plegament constant i l'eliminació de codi mort, i millora el que és possible executant-los per separat.[16][17] Aquesta optimització executa simbòlicament el programa, propagant simultàniament valors constants i eliminant parts del gràfic de flux de control que això fa inabastables.

Optimitzacions del generador de codi

Assignació de registres
Les variables més utilitzades s'han de mantenir als registres del processador per a un accés més ràpid. Per trobar quines variables posar als registres, es crea un graf d'interferència. Cada variable és un vèrtex i quan s'utilitzen dues variables alhora (tenen un rang de temps que s'interseca) tenen una aresta entre elles. Aquest graf es acoloreix utilitzant, per exemple, l'algorisme de Chaitin amb el mateix nombre de colors com registres hi ha. Si la coloració falla, una variable es "vessa" a la memòria i es torna a intentar la coloració.
Selecció d'instruccions
La majoria d'arquitectures, en particular les arquitectures CISC i les que tenen molts modes d'adreçament, ofereixen diverses maneres de realitzar una operació concreta, utilitzant seqüències d'instruccions completament diferents. La tasca del selector d'instruccions és fer una bona feina en general triant quines instruccions implementar quins operadors en la representació intermèdia de baix nivell. Per exemple, en molts processadors de la família 68000 i l'arquitectura x86, es poden utilitzar modes d'adreçament complexos en sentències com ara lea 25(a1,d5*4), a0, permetent que una sola instrucció realitzi una quantitat significativa d'aritmètica amb menys emmagatzematge.
Planificació d'instruccions
La planificació d'instruccions és una optimització important per als processadors moderns en pipeline, que evita bloquejos o bombolles en el pipeline agrupant instruccions sense dependències, alhora que té cura de preservar la semàntica original.
Rematerialització
La rematerialització recalcula un valor en comptes de carregar-lo des de la memòria, eliminant un accés a la memòria. Això es realitza conjuntament amb l'assignació de registres per evitar vessaments.
Factorització de codi
Si diverses seqüències de codi són idèntiques, o es poden parametritzar o reordenar per ser idèntiques, es poden substituir per crides a una subrutina compartida. Això sovint pot compartir codi per a la configuració de subrutines i, de vegades, recursivitat final.[18]
Trampolins
Molts Les CPU tenen instruccions de crida de subrutines més petites per accedir a poca memòria. Un compilador pot estalviar espai utilitzant aquestes petites crides al cos principal del codi. Les instruccions de salt amb poca memòria poden accedir a les rutines a qualsevol adreça. Això multiplica l'estalvi d'espai derivat de la factorització de codi.[18]
Reordenació de càlculs
Basats en la programació lineal entera, els compiladors reestructurants milloren la localitat de les dades i exposen més paral·lelisme reordenant els càlculs. Els compiladors que optimitzen l'espai poden reordenar el codi per allargar les seqüències que es poden factoritzar en subrutines.

Optimitzacions de llenguatges funcionals

Tot i que molts d'aquests també s'apliquen a llenguatges no funcionals, o bé s'originen o bé són particularment crítics en llenguatges funcionals com ara Lisp i ML.

Optimització de trucades finals
Una crida de funció consumeix espai de pila i implica una certa sobrecàrrega relacionada amb el pas de paràmetres i el buidatge de la memòria cau d'instruccions. Els algoritmes recursius de cua es poden convertir en iteració mitjançant un procés anomenat eliminació de recursió de cua o optimització de crida de cua.
Desforestació (fusió d'estructures de dades )
En els llenguatges on és comú aplicar una seqüència de transformacions a una llista, la desforestació intenta eliminar la construcció d'estructures de dades intermèdies.
Avaluació parcial
Els càlculs que produeixen la mateixa sortida independentment de l'entrada dinàmica en temps d'execució es poden avaluar en temps de compilació.

Altres optimitzacions

Eliminació de comprovació de límits
Molts llenguatges, com ara Java, imposen la comprovació de límits a tots els accessos a matrius. Això és un coll d'ampolla important en certes aplicacions com ara el codi científic. L'eliminació de la comprovació de límits permet al compilador eliminar de manera segura la comprovació de límits en moltes situacions on pot determinar que l'índex ha de caure dins dels límits vàlids; per exemple, si es tracta d'una variable de bucle simple.
Optimització del desplaçament de les bifurcacions (dependent de la màquina)
Trieu el desplaçament de la branca més curt que arribi a l'objectiu.
Reordenació de blocs de codi
La reordenació de blocs de codi altera l'ordre dels blocs bàsics d'un programa per reduir les branques condicionals i millorar la localitat de referència.
Eliminació de codi mort
Elimina les instruccions que no afectaran el comportament del programa, per exemple, les definicions que no tenen cap utilitat, anomenades codi mort. Això redueix la mida del codi i elimina els càlculs innecessaris.
Factorització d'invariants (invariants de bucle)
Si una expressió s'executa tant quan es compleix una condició com quan no es compleix, es pot escriure només una vegada fora de l'instrucció condicional. De la mateixa manera, si certs tipus d'expressions (per exemple, l'assignació d'una constant a una variable) apareixen dins d'un bucle, es poden treure'n perquè el seu efecte serà el mateix independentment de si s'executen moltes vegades o només una vegada. Això també es coneix com a eliminació de redundància total. Una optimització similar però més potent és l'eliminació de redundància parcial (PRE).
Expansió en línia o expansió macro
Quan algun codi invoca un procediment, és possible inserir directament el cos del procediment dins del codi que fa la crida en lloc de transferir-hi el control. Això estalvia la sobrecàrrega relacionada amb les crides de procediment, a més de proporcionar una oportunitat per a moltes optimitzacions específiques de paràmetres diferents, però té un cost d'espai; el cos del procediment es duplica cada vegada que el procediment es crida en línia. Generalment, l'inline és útil en codi crític de rendiment que fa un gran nombre de crides a procediments petits. Aquesta és una optimització de "menys salts". Les sentències dels llenguatges de programació imperatius també són un exemple d'aquesta optimització. Tot i que les sentències es podrien implementar amb crides de funció, gairebé sempre s'implementen amb codi inline.
Compressió de macros
Una optimització d'espai que reconeix seqüències comunes de codi, crea subprogrames ("macros de codi") que contenen el codi comú i substitueix les aparicions de les seqüències de codi comunes per crides als subprogrames corresponents. Això es fa de manera més efectiva com a optimització de codi màquina, quan tot el codi és present. La tècnica es va utilitzar per primera vegada per conservar espai en un flux de bytes interpretatiu utilitzat en una implementació de Macro Spitbol en microordinadors.[19] Se sap que el problema de determinar un conjunt òptim de macros que minimitzi l'espai requerit per un segment de codi determinat és NP-complet, però les heurístiques eficients aconsegueixen resultats gairebé òptims.[20]
Reducció de les col·lisions de la memòria cau
(p. ex., alterant l'alineació dins d'una pàgina)
Reducció de l'alçada de la pila
Reorganitzar un arbre d'expressions per minimitzar els recursos necessaris per a l'avaluació d'expressions.
Prova de reordenació
Si tenim dues proves que són la condició per a alguna cosa, primer podem tractar les proves més simples (per exemple, comparar una variable amb alguna cosa) i només després les proves complexes (per exemple, les que requereixen una crida de funció). Aquesta tècnica complementa l'avaluació mandrosa, però només es pot utilitzar quan les proves no depenen l'una de l'altra. El curtcircuit semàntic pot dificultar-ho.

Optimitzacions interprocedimentals

L'optimització interprocedimental funciona en tot el programa, a través dels límits de procediments i fitxers. Funciona estretament amb les contraparts intraprocedimentals, realitzades amb la cooperació d'una part local i una part global. Les optimitzacions interprocedimentals típiques són l'inlineing de procediments, l'eliminació de codi mort interprocedimental, la propagació de constants interprocedimentals i la reordenació de procediments. Com és habitual, el compilador ha de realitzar una anàlisi interprocedimental abans de les seves optimitzacions reals. Les anàlisis interprocedimentals inclouen l'anàlisi d'àlies, l'anàlisi d'accés a matrius i la construcció d'un gràfic de crides.

L'optimització interprocedimental és habitual en els compiladors comercials moderns de SGI, Intel, Microsoft i Sun Microsystems. Durant molt de temps, el GCC de codi obert va ser criticat per la manca d'anàlisi i optimitzacions interprocedimentals potents, tot i que ara això està millorant.[21] Un altre compilador de codi obert amb una infraestructura completa d'anàlisi i optimització és Open64.

A causa del temps i l'espai addicionals que requereix l'anàlisi interprocedimental, la majoria de compiladors no la realitzen per defecte. Els usuaris han d'utilitzar les opcions del compilador explícitament per indicar-li que activi l'anàlisi interprocedimental i altres optimitzacions costoses.

Consideracions pràctiques

Hi pot haver una àmplia gamma d'optimitzacions que un compilador pot dur a terme, des d'optimitzacions simples i directes que requereixen poc temps de compilació fins a optimitzacions elaborades i complexes que impliquen quantitats considerables de temps de compilació.:[22] 15  En conseqüència, els compiladors sovint proporcionen opcions a la seva ordre o procediment de control per permetre a l'usuari del compilador triar quanta optimització sol·licitar; per exemple, el compilador IBM FORTRAN H permetia a l'usuari especificar cap optimització, optimització només a nivell de registres o optimització completa.:[22] 737 A la dècada del 2000, era habitual que els compiladors, com ara Clang, tinguessin diverses opcions de comandament del compilador que podien afectar una varietat d'opcions d'optimització, començant pel conegut interruptor -O2.[23]

Un mètode per aïllar l'optimització és l'ús dels anomenats optimitzadors post-pass (algunes versions comercials dels quals daten del programari mainframe de finals dels anys 70).[24] Aquestes eines prenen la sortida executable d'un compilador optimitzador i l'optimitzen encara més. Els optimitzadors post-pass solen treballar a nivell de llenguatge assemblador o codi màquina (a diferència dels compiladors que optimitzen les representacions intermèdies dels programes). Un exemple d'això és el Portable C Compiler (PCC) dels anys 80, que tenia una passada opcional que realitzava post-optimitzacions sobre el codi assemblador generat.:[25] 736 

Una altra consideració és que els algoritmes d'optimització són complicats i, especialment quan s'utilitzen per compilar llenguatges de programació grans i complexos, poden contenir errors que introdueixen errors en el codi generat o causen errors interns durant la compilació. Els errors de compilació de qualsevol tipus poden ser desconcertants per a l'usuari, però sobretot en aquest cas, ja que pot no estar clar que la lògica d'optimització sigui la culpable.[26] En el cas d'errors interns, el problema es pot millorar parcialment mitjançant una tècnica de programació "a prova d'errors" en què la lògica d'optimització del compilador es codifica de manera que es bloqueja un error, s'emet un missatge d'advertència i la resta de la compilació continua fins a completar-se correctament.[27]

Història

Els primers compiladors dels anys seixanta sovint es preocupaven principalment per compilar codi correctament o eficientment, de manera que els temps de compilació eren una preocupació important. Un compilador d'optimització destacat va ser el compilador IBM FORTRAN H de finals dels anys seixanta.:[28] 737 Un altre dels primers i importants compiladors d'optimització, que va ser pioner en diverses tècniques avançades, va ser el de BLISS (1970), que es va descriure a The Design of an Optizing Compiler (1975).[28](pp740, 779) A finals de la dècada de 1980, l'optimització dels compiladors era prou efectiva com per fer que la programació en llenguatge assemblador decaigués. Això va coevolucionar amb el desenvolupament de xips RISC i funcions avançades de processador com ara processadors superescalars, execució fora d'ordre i execució especulativa, que van ser dissenyats per ser dirigits per l'optimització dels compiladors en lloc del codi assemblador escrit per humans.

Referències

  1. ↑ Oshana, Robert. Optimizing DSP Software (en anglès). Elsevier, 2006, p. 159–227. DOI 10.1016/B978-075067759-2/50008-9. ISBN 978-0-7506-7759-2.
  2. ↑ «Lecture 15: NP-completeness, Optimization and Separation» (en anglès). IE 511: Integer Programming, Spring 2021.
  3. ↑ Aho, Alfred V. Compilers: Principles, Techniques, and Tools (en anglès). Reading, Massachusetts: Addison-Wesley, 1986. ISBN 0-201-10088-6.
  4. ↑ Cooper, Keith D. Engineering a Compiler (en anglès). Morgan Kaufmann, 2003, p. 404, 407. ISBN 978-1-55860-698-2.
  5. ↑ Cooper, Keith D. Engineering a Compiler (en anglès). Morgan Kaufmann, 2003, p. 404, 407. ISBN 978-1-55860-698-2.
  6. ↑ Aho, Alfred V. Compilers: Principles, Techniques, and Tools (en anglès). Reading, Massachusetts: Addison-Wesley, 1986. ISBN 0-201-10088-6.
  7. ↑ A Programmer's Introduction to IBM System/360 Assembly Language (en anglès). IBM, p. 42. GC20-1645-5.
  8. ↑ «GCC – Machine-Dependent Options» (en anglès). GNU Project.
  9. ↑ «RISC vs. CISC» (en anglès). cs.stanford.edu. [Consulta: 15 octubre 2024].
  10. ↑ Aho, Alfred V. Compilers: Principles, Techniques, and Tools (en anglès). Reading, Massachusetts: Addison-Wesley, 1986. ISBN 0-201-10088-6.
  11. ↑ Aho, Alfred V. Compilers: Principles, Techniques, and Tools. Reading, Massachusetts: Addison-Wesley, 1986. ISBN 0-201-10088-6.
  12. ↑ Aho, Alfred V. Compilers: Principles, Techniques, and Tools (en anglès). Reading, Massachusetts: Addison-Wesley, 1986. ISBN 0-201-10088-6.
  13. ↑ James Gosling. «17 Threads and Locks». A: The Java Language Specification (en anglès). 1.0.
  14. ↑ Aho, Alfred V. Compilers: Principles, Techniques, and Tools (en anglès). Reading, Massachusetts: Addison-Wesley, 1986. ISBN 0-201-10088-6.
  15. ↑ Muchnick, Steven. Advanced Compiler Design Implementation (en anglès). Morgan Kaufmann, 15 August 1997, p. 329–. ISBN 978-1-55860-320-2.
  16. ↑ Wegman, Mark N.; Zadeck, F. Kenneth ACM Transactions on Programming Languages and Systems, 13, 2, 4-1991, p. 181–210. DOI: 10.1145/103135.103136.
  17. ↑ Click, Clifford; Cooper, Keith. ACM Transactions on Programming Languages and Systems, 17, 2, 3-1995, p. 181–196. DOI: 10.1145/201059.201061.
  18. 1 2 Cx51 Compiler Manual, version 09.2001, p. 155, Keil Software Incorporated.
  19. ↑ Dewar, Robert B. K.. MICRO SPITBOL (en anglès). 11. Courant Institute of Mathematical Sciences, August 2013 (Computer Science Department Technical Report).
  20. ↑ Golumbic, Martin Charles. "Macro Substitutions in MICRO SPITBOL – a Combinatorial Analysis" a 11th Southeastern Conference on Combinatorics, Graph Theory and Computing. 29 
  21. ↑ Foundations of Scientific Research.
  22. 1 2 Aho, Alfred V. Compilers: Principles, Techniques, and Tools (en anglès). Reading, Massachusetts: Addison-Wesley, 1986. ISBN 0-201-10088-6.
  23. ↑ Guelton, Serge. «Customize the compilation process with Clang: Optimization options» (en anglès). Red Hat, 05-08-2019.
  24. ↑ Evans, Michael Communications of the ACM, 25, 12, 12-1982, p. 874–882. DOI: 10.1145/358728.358732 [Consulta: 10 agost 2013].
  25. ↑ Aho, Alfred V. Compilers: Principles, Techniques, and Tools (en anglès). Reading, Massachusetts: Addison-Wesley, 1986. ISBN 0-201-10088-6.
  26. ↑ Sun, Chengnian (July 18–20, 2016). "Toward understanding compiler bugs in GCC and LLVM" a Issta 2016.  
  27. ↑ Schilling, Jonathan L. ACM SIGPLAN Notices, 28, 8, 8-1993, p. 39–42. DOI: 10.1145/163114.163118.
  28. 1 2 Aho, Alfred V. Compilers: Principles, Techniques, and Tools (en anglès). Reading, Massachusetts: Addison-Wesley, 1986. ISBN 0-201-10088-6.