09. LLVM Passes - displaying code structure

Control Flow Analysis

Î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:

  • caracteristicile fluxului de control al programelor
  • cum sunt manipulate datele

Astfel, mecanismele mai generale, dar mai puțin eficiente, pot fi înlocuite cu mecanisme mai specializate și mai eficiente.

Abordări ale analizei fluxului de control

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).

Exemplu

Î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.

Flowchart pentru fib()

Basic blocks

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

  1. Prima instrucțiune a unui basic block (denumită lider) poate fi:
    • punctul de intrare în rutină
    • ținta unei instrucțiuni de salt
    • instrucțiunea imediat următoare unei instrucțiuni de salt
  2. Pentru a determina bb-urile care formează o rutina întâi se identifică toți liderii și apoi, pentru fiecare lider, includem în 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ă:

  • nodurile 1-4 formează un basic block
  • nodurile 8-11 formează alt basic block
  • Toate celelalte noduri formează singure câte un basic block

Graful fluxului de control (CFG)

Graful fluxului de control pentru fib (CFG) generate cu LLVM

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 general, exista un singur basic block de intrare, dar, în unele limbaje (ex. FORTRAN), pot exista mai multe puncte de intrare într-o rutina).

În continuare, definim:

  • Succ(b) - setul de succesori ai unui basic block
  • Pred(b) - setul de predecesori ai unui basic block.

Tipuri de basic blocks:

  • Branch - blocul are mai mult de un succesor
  • Join - blocul are mai mult de un predecesor

Bucle naturale și componente tare conexe

Arc înapoi

Un arc înapoi în CFG este un arc a cărui destinatie este întalnită înaintea nodului sursă intr-o parcurgere DFS. Arc înapoi

Buclă naturală

Buclă naturală determinată de arcul înapoi v→u este definită ca fiind subgraful definit prin:

  • mulțimea de noduri - formată din nodul u și toate nodurile din care poate fi atins nodul v fără a trece prin u
  • mulțimea de arce - toate arcele conectând nodurile din mulțime.

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. Buclă naturală

O buclă naturală este o componentă tare conexă cu o singură intrare.

Introducerea nodului PRE-HEADER

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ă.

Înainte de adăugarea nodului de preheader După adăugarea nodului de preheader
Înainte de adăugarea nodului de preheader După adăugarea nodului de preheader

Bucle imbricate

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ă. Bucle cu header comun

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).

Regiuni improprii

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:

  • mulțimea arcelor înainte - acele arce care formează un graf aciclic orientat (DAG) în care fiecare nod poate fi atins din nodul entry
  • multimea arcelor înapoi - acele arce care sunt conforme cu definiția de mai sus, adică nodul destinație domină nodul sursă

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).

Regiune improprie După “node-splitting”
Regiune improprie După "node-splitting"

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:

  • analiza iterativă a fluxului de date
  • node-splitting pentru transformarea unui CFG ireductibil într-unul reductibil

(vezi exemplul de mai sus (figura din dreapta) în care tehnica a fost aplicată pentru nodul B3).

Opțional de citit

LLVM Passes

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ă).

Crearea unui Pass

LLVM Passes pot fi rulate cu ajutorul tool-ului opt.

Pass-uri existente

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

Hello World

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:

ex.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

Integrarea unui pas în LLVM

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
    • se şterge: LOADABLE_MODULE = 1
    • și se adaugă: BUILD_ARCHIVE = 1
  • tools/opt/CMakeLists.txt
    • se adaugă noul pass în lista
      • set(LLVM_LINK_COMPONENTS ${LLVM_TARGETS_TO_BUILD} Analysis CodeGen Hello)
    • la fel și pentru tools/bugpoint/CMakeLists.txt
  • se crează fişierul 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
  • se creeaza fișierul lib/Transforms/Hello/LLVMBuild.txt după formatul din sistemul de build llvm.
    • ex:
      [component_0]
      type = Library
      name = Hello
      parent = Transforms
      library_name = hello
      required_libraries = Analysis Support
    • se updatează și fișierul LLVMBuild.txt din directorul părinte pentru a include sub-directorul Hello.
  • lib/Transforms/Hello/CMakeLists.txt
    • se şterge add_llvm_loadable_module( LLVMHello Hello.cpp )
    • și se adaugă
      • add_llvm_library( LLVMHello Hello.cpp )
  • lib/Transforms/Hello/Hello.cpp
    • se include
      • llvm/Transforms/Hello.h
    • se adaugă
      • FunctionPass* llvm::createHelloPass()
        {
           return new Hello();
        }
  • include/llvm/LinkAllPasses.h
    • se include llvm/Transforms/Hello.h
    • și se adaugă
      • (void) llvm::createHelloPass();
  • se creeaza un director nou în care vor fi compilate sursele. De exemplu dacă sursele se află în directorul llvm-3.8/0.src și am creat pe același nivel directorul llvm, executăm din interiorul directorului nou creat comanda
    • cmake -G "Unix Makefiles" ../llvm-3.8.0.src
    • urmată de comanda
       make 

Exerciții de laborator

Arhiva laboratorului.

Ex.1 - Mem2Reg. Constant Propagation. Deadcode Elimination (3p)

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)

Ex.2 - O3 (3p)

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.

Ex.3 - Loops (3p)

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.

Ex. 4 - LLVM pass (6p)

Folosindu-va de modelul pass-ului Hello world explicat mai sus, scrieți un LLVM pass pentru a afișa:

  • toate basic block-urile unui program
  • pentru fiecare basic-block:
    • basic block-urile predecesoare
    • basic block-urile următoare

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 \$PathToLLVMSrc/lib/Transforms/Hello in \$PathTooLLVMSrc/lib/Transforms/\$MyPass.

<code>
$PathToLLVMSrc/lib/Transforms/$MyPass/CMakeLists.txt
$PathToLLVMSrc/lib/Transforms/$MyPass/Makefile
$PathToLLVMSrc/lib/Transforms/CMakeLists.txt </code>

Redenumiti \$PathToLLVMSrc/lib/Transforms/\$MyPass/Hello.exports in \$PathToLLVMSrc/lib/Transforms/\$MyPass/\$MyPass.exports


Mergeti in folderul de build. Pe calculatoarele din laborator este ~/cpl/build. Rulati
<code>
cmake $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:

cpl/labs/09.txt · Last modified: 2016/12/06 23:02 by bogdan.nitulescu
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0