Înainte de a putea optimiza un program, unui compilator îi sunt necesare componente care sa îl ajute sa “înțeleagă” cum folosește programul respectiv resursele disponibile.
Astfel, compilatorul trebuie sa fie capabil sa descrie:
Astfel, mecanismele mai generale, dar mai puțin eficiente, pot fi înlocuite cu mecanisme mai specializate și mai eficiente.
Orice analiza a fluxului de control al unei rutine începe cu determinarea blocurilor de bază(basic blocks). Acest pas constituie rutina și construcția Control Flow Graph (CFG).
Înainte de a descrie tehnici formale folosite în analiza fluxului de control și în analiza fluxului de date, să prezentăm un exemplu simplu.
Fie rutina C de mai jos, care calculează termenul m
din șirul lui Fibonacci (m ≥ 0
). Lângă secvența de cod C, am dat și o posibilă descriere într-un limbaj intermediar a acestei funcții.
Cod sursă | Pseudocod generat |
---|---|
unsigned int fib(unsigned int m) { unsigned int f0 = 0; unsigned int f1 = 1; unsigned int f2, i; if (m <= 1) return m; else { for(i = 2; i <= m; i++) { f2 = f0 + f1; f0 = f1; f1 = f2; } return f2; } } | 1 receive m 2 f0 <- 0 3 f1 <- 1 4 if m <= 1 goto L3 5 i <- 2 6 L1: if i <= m goto L2 7 return f2 8 L2: f2 <- f0+f1 9 f0 <- f1 10 f1 <- f2 11 i <- i+1 12 goto L1 13 L3: return m |
Primul pas este de a descoperi structura de control a programului. Această structură poate fi evidentă în codul sursă (if-then-else
, cu o buclă pe partea cu else
), însă, aceasta nu mai este la fel de evidentă în limbajul intermediar. Mai mult, chiar și în C, există situații mult mai complexe (ex. un for
ar fi putut fi format din instrucțiuni if
și goto
).
Pentru a facilita înțelegerea metodei de analiză a fluxul de control, am reprezentat în figura de mai jos codul intermediar într-o formă “vizuală”, un flowchart
.
Un block de bază (basic block) reprezintă o secvență de instrucțiuni care se execută întotdeauna una după alta. Formal, un basic block (bb) este o secvență maximală de instrucțiuni consecutive în care se poate intra doar prin intermediul primei instrucțiuni și din care se poate ieși doar prin intermediul ultimei instrucțiuni.
Determinare basic blocks
bb
toate instrucțiunile de la lider pana la următorul lider sau până la sfârșitul rutinei.Pe flowchart-ul de mai sus se vede că putem identifica cu ușurință blocurile de bază:
Figura conţine cod intermediar LLVM și a fost obţinută executând următoarele comenzi:
sudo apt-get install clang clang -emit-llvm fibo.c -c -o fibo.bc sudo apt-get install dot2tex opt -dot-cfg fibo.bc ; dot -Tpdf cfg.fib.dot > cfg.fib.pdf SAU opt-3.0 -f -dot-cfg fibo.bc ; dot -Tpdf cfg.fib.dot > cfg.fib.pdf
În graf, există arc (muchie orientată) de la un nod la altul dacă există posibilitatea ca, imediat după execuția ultimei instrucțiuni din bb
-ul din primul nod, să urmeze spre execuție prima instrucțiune din bb
-ul din cel de-al doilea nod.
Există arce de la nodul entry la fiecare nod care reprezintă un basic block de intrare (liderul său poate fi prima instrucțiune executată la apelul rutinei). Analog, există arce de la fiecare nod ce reprezinta un basic block de ieșire (ultima instrucțiunea a bb
-ului poate fi ultima instrucțiune executată la ieșirea din funcție) la nodul exit.
În continuare, definim:
Tipuri de basic blocks:
Un arc înapoi în CFG este un arc a cărui destinatie este întalnită înaintea nodului sursă intr-o parcurgere DFS.
Buclă naturală determinată de arcul înapoi v→u
este definită ca fiind subgraful definit prin:
Atenție! Pentru a avea buclă naturală u dom v
și, deci, nu există noduri din care v poate fi atins fără a trece prin u și care să nu fie la rândul lor dominate de u. Astfel, nodul u este header-ul buclei.
Multe optimizari constau în mutarea codului din interiorul buclei imediat înaintea header-ului buclei. Pentru a garanta că avem un astfel de loc disponibil, introducem conceptul de pre-header, care este un nou basic block, inițal gol, plasat imediat înaintea header-ului buclei. Astfel, toate arcele care înainte intrau în header, venind din afara buclei, acum vor intra în pre-header. În plus, există un singur arc de la pre-header la header. Figura de mai jos arată rezultatul introducerii unui pre-header pentru o buclă.
Nu este greu de observat că dacă două bucle au header-e diferite, ele sunt fie disjuncte, fie imbricate. Pe de altă parte, dacă două bucle au același header (ca în cazul din figura de mai jos), nu este foarte clar dacă una este imbricată în cealaltă (și care este imbricată în care) sau dacă ele formeaza împreună o singură buclă.
Pornind de la faptul ca, fără mai multe detalii despre codul sursa, nu se poate distinge între aceste două situații, astfel de cazuri vor fi tratate ca fiind o singură buclă (există o modalitate de analiză a fluxului de control denumită analiză structurală care poate face acestă distincție).
O buclă naturală este doar un tip particular de componentă tare conexă. În practică, deși rar, pot apărea și alte structuri de tip buclă cu mai multe puncte de intrare (în special în programele nestructurate). Deși asemenea structuri sunt rare, faptul că ele totuși apar, ne obligă să le luăm în considerare. Cea mai generală structură de tip buclă care poate să apară este o componentă tare conexă a CFG-ului.
CFG-ul este bine-structurat dacă acesta conține doar bucle naturale.
Formal, un CFG G=<N,E>
este bine-structurat dacă mulțimea arcelor poate fi partitionată în două mulțimi:
Anumite șabloane de flux de control pot face un CFG prost-structurat. Astfel de sabloane se numesc regiuni improprii și în general sunt componente tare conexe cu mai multe intări. Un exemplu de regiune improprie este dat și în figura de mai jos (stanga).
Regiunile improprii sunt un impediment în special în analiza fluxului de date. Există mai multe metode de a trata problema regiunilor improprii dintr-un CFG dintre care menționăm:
(vezi exemplul de mai sus (figura din dreapta) în care tehnica a fost aplicată pentru nodul B3).
LLVM Passes este un subsistem al framework-ului LLVM ce permite crearea de module simple pentru transformarea, prelucrarea și analiza codului intermediar LLVM IR
.
Există numeroase pass-uri deja implementate în versiunea oficială LLVM. Acestea primesc ca input cod LLVM IR
și oferă ca output fie LLVM IR
(în cazul în care este un pass de transformare), fie un text cu diverse statistici (în cazul în care este un pass de analiză).
LLVM Passes
pot fi rulate cu ajutorul tool-ului opt
.
student@cpl-vm ~ $ opt --help OVERVIEW: llvm .bc -> .bc modular optimizer and analysis printer USAGE: opt [options] <input bitcode file> OPTIONS: -O1 - Optimization level 1. Similar to clang -O1 -O2 - Optimization level 2. Similar to clang -O2 -O3 - Optimization level 3. Similar to clang -O3 -Os - Like -O2 with extra optimizations for size. Similar to clang -Os -Oz - Like -Os but reduces code size further. Similar to clang -Oz -S - Write output as LLVM assembly -analyze - Only perform analysis, no optimization -asm-instrumentation - Instrumentation of inline assembly and assembly source files =none - no instrumentation at all =address - instrument instructions with memory arguments -asm-show-inst - Emit internal instruction representation to assembly file Optimizations available: [...] -bb-vectorize - Basic-Block Vectorization -constprop - Simple constant propagation -da - Dependence Analysis -dce - Dead Code Elimination -domtree - Dominator Tree Construction -dot-callgraph - Print call graph to 'dot' file -dot-cfg - Print CFG of function to 'dot' file -dot-cfg-only - Print CFG of function to 'dot' file (with no function bodies) -dot-dom - Print dominance tree of function to 'dot' file -dot-dom-only - Print dominance tree of function to 'dot' file (with no function bodies) -dot-postdom - Print postdominance tree of function to 'dot' file -dot-postdom-only - Print postdominance tree of function to 'dot' file (with no function bodies) -instcount - Counts the various types of Instructions -loop-deletion - Delete dead loops -loop-reroll - Reroll loops -loop-rotate - Rotate Loops -loop-unroll - Unroll loops -loop-vectorize - Loop Vectorization -loops - Natural Loop Information -mem2reg - Promote Memory to Register -memdep - Memory Dependence Analysis -print-callgraph - Print a call graph -reg2mem - Demote all values to stack slots -sample-profile - Sample Profile loader -simplifycfg - Simplify the CFG -verify - Module Verifier -view-callgraph - View call graph -view-cfg - View CFG of function -view-cfg-only - View CFG of function (with no function bodies) -view-dom - View dominance tree of function -view-dom-only - View dominance tree of function (with no function bodies) -view-postdom - View postdominance tree of function -view-postdom-only - View postdominance tree of function (with no function bodies) -p - Print module after each transformation -stats - Enable statistics output from program (available with Asserts) -time-passes - Time each pass, printing elapsed time for each on exit [...]
Vom rula pass-ul default hello pe un cod simplu.
Codul sursă al pass-ului Hello din LLVM se găsește în directorul $LLVM_SRC/lib/Transforms/Hello
. Vizualizați codul sursă și makefile-ul. Pentru detalii suplimentare consultați pagina de manual.
După cum ați observat anterior, acesta nu este găsit by default de către tool-ul opt
. Va trebui să îi specificăm calea către biblioteca dinamică compilată.
student@cpl-vm ~/llvm-3.6.2 $ opt -load llvm-3.8.0/lib/LLVMHello.so --help OVERVIEW: llvm .bc -> .bc modular optimizer and analysis printer USAGE: opt [options] <input bitcode file> [...] -hello - Hello World Pass -hello2 - Hello World Pass (with getAnalysisUsage implemented) [...]
Fie următorul cod sursă C:
#include <stdio.h> int compute(int x) { return 10 + 4 * x; } int main(void) { int x = 10 * 12; printf("Printing something: %d\n", compute(x)); return 0; }
Vom genera fișierul .bc
corespunzător:
student@cpl-vm ~ $ clang -emit-llvm ex.c -c -o ex.bc
Vom rula pass-ul Hello
peste bitcode-ul obținut:
student@cpl-vm ~ $ opt -load llvm-3.6.2/install/lib/LLVMHello.so --hello < ex.bc WARNING: You're attempting to print out a bitcode file. This is inadvisable as it may cause display problems. If you REALLY want to taste LLVM bitcode first-hand, you can force output with the `-f' option. Hello: compute Hello: main
Pentru a nu mai fi nevoiți să specificăm biblioteca dinamică la runtime, aceasta trebuie să fie integrată în codul utilitarelor opt
și bugpoint
. Pentru a realiza acest lucru, trebuie să modificăm exemplul dat în sursele de LLVM astfel:
lib/Transforms/Hello/Makefile
tools/opt/CMakeLists.txt
tools/bugpoint/CMakeLists.txt
include/llvm/Transforms/Hello.h
cu conţinutul #ifndef LLVM_TRANSFORMS_HELLO_H #define LLVM_TRANSFORMS_HELLO_H namespace llvm { class FunctionPass; FunctionPass *createHelloPass(); } #endif
lib/Transforms/Hello/LLVMBuild.txt
după formatul din sistemul de build llvm.[component_0] type = Library name = Hello parent = Transforms library_name = hello required_libraries = Analysis Support
Hello
. lib/Transforms/Hello/CMakeLists.txt
lib/Transforms/Hello/Hello.cpp
FunctionPass* llvm::createHelloPass() { return new Hello(); }
include/llvm/LinkAllPasses.h
(void) llvm::createHelloPass();
cmake -G "Unix Makefiles" ../llvm-3.8.0.src
make
Arhiva laboratorului.
Intrati in folderul 'ex1' din arhiva de laborator. Compilati fisierul ex1.c pana la nivel LLVM IR, atat in forma human readable, cat si forma binara. Pe fisierul in forma binara, rulati pe rand urmatoarele optimizari: mem2reg, constprop, simplifycfg.
opt $optimizare $fisier.bc > $fisier-$optimizare.bc
. La fiecare pas treceti fisierul optimizat in forma human readable si observati schimbarile fata de fisierul anterior. (Hint: folositi llvm-dis)
Folosindu-va de acelasi fisier sursa (ex1.c), compilati pana la nivel LLVM IR, atat in forma human readable, cat si forma binara. Pe fisierul in format binar rulati urmatoarea comanda:
opt -O3 -print-after-all ex1.bc 1> ex1opt.bc 2> passes.txt
Treceti fisierul optimizat in forma human readable si observati modificarile. Pass-urile executate, impreuna cu output-ul generat dupa fiecare optimizare se gaseste in fisierul passes.txt.
Intrati in folder-ul ex.2 din arhiva de laborator. Compilati in cele 2 forme LLVM IR fisierul loops.c. Faceti urmatoarele optimizari: mem2reg, simplifycfg, loops, loop-rotate, loop-unroll. Observati modificarile.
Folosindu-va de modelul pass-ului Hello world explicat mai sus, scrieți un LLVM pass pentru a afișa:
Rulati pass-ul ca folosind calea catre bilioteca dinamica compilata.
Cel mai simplu mod pentru a crea pass-ul este sa copiati folderul hello din \PathTooLLVMSrc/lib/Transforms/\PathToLLVMSrc/lib/Transforms/PathToLLVMSrc/lib/Transforms/PathToLLVMSrc/lib/Transforms/CMakeLists.txt </code>
Redenumiti \MyPass/Hello.exports in \MyPass/\PathToLLVMSrc </code> Pentru calculatoarele din laborator acesta este
cmake ../llvm-3.8.0.src
Apoi rulati make in folderul nou create din ~/cpl/build/lib/Transforms/$MyPass.
Trebuie sa rerulati cmake si make pentru fiecare modificare pe care o faceti in sursa pass-ului vostru. Pass-ul compilat sub forma de biblioteca dinamica se va gasi in ~/cpl/build/lib.
Hints: