52
UNIVERSITÀ DEGLI STUDI DI PARMA Dipartimento di Matematica e Informatica Corso di Laurea in Informatica Un caso di studio sulla Thread Safety: la Parma Polyhedra Library Candidato: Maxim Gaina Relatore: Prof. Enea Zaffanella Anno Accademico 2013/2014

UNIVERSITÀ DEGLI STUDI DI PARMA Corso di Laurea in Informatica · UNIVERSITÀ DEGLI STUDI DI PARMA Dipartimento di Matematica e Informatica Corso di Laurea in Informatica Uncasodistudio

  • Upload
    lamnhan

  • View
    218

  • Download
    0

Embed Size (px)

Citation preview

UNIVERSITÀ DEGLI STUDI DI PARMADipartimento di Matematica e Informatica

Corso di Laurea in Informatica

Un caso di studiosulla Thread Safety:

la Parma Polyhedra Library

Candidato: Maxim Gaina

Relatore: Prof. Enea Zaffanella

Anno Accademico 2013/2014

Indice

Introduzione 3

1 Thread Safety 61.1 Codice Thread Unsafe . . . . . . . . . . . . . . . . . . . . . . 6

1.1.1 Tabella dei simboli . . . . . . . . . . . . . . . . . . . . 81.2 Strategie per scrivere codice Thread Safe . . . . . . . . . . . . 10

1.2.1 Programmazione funzionale . . . . . . . . . . . . . . . 101.2.2 Conservazione stato nel thread chiamante . . . . . . . 111.2.3 Thread Local Storage . . . . . . . . . . . . . . . . . . . 111.2.4 Mantenere istanze diverse di una libreria . . . . . . . . 121.2.5 Stati Condivisi . . . . . . . . . . . . . . . . . . . . . . 131.2.6 Strategie adottate nel caso della PPL . . . . . . . . . . 13

2 L’ambiente di sviluppo 152.1 Piattaforma . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15

2.1.1 Configurazione elaboratore . . . . . . . . . . . . . . . . 162.2 Configurazione dell’ambiente . . . . . . . . . . . . . . . . . . . 162.3 Debug . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 172.4 Altri strumenti . . . . . . . . . . . . . . . . . . . . . . . . . . 18

2.4.1 GNU nm . . . . . . . . . . . . . . . . . . . . . . . . . . 182.4.2 GNU grep . . . . . . . . . . . . . . . . . . . . . . . . . 19

3 PPL e Thread Local Storage 203.1 Conditional Thread Safety . . . . . . . . . . . . . . . . . . . . 20

3.1.1 Standard Template Library C++ e variabili const . . . 203.1.2 PPL e variabili const . . . . . . . . . . . . . . . . . . . 21

3.2 Thread Local Storage . . . . . . . . . . . . . . . . . . . . . . . 223.2.1 Alternative allo standard C++: TLS in GCC . . . . . 223.2.2 TLS in C++11 . . . . . . . . . . . . . . . . . . . . . . 23

3.3 Ricerca di codice thread-unsafe . . . . . . . . . . . . . . . . . 253.3.1 Ricerca testuale . . . . . . . . . . . . . . . . . . . . . . 26

INDICE 2

3.3.2 Ricerca sulla tabella dei simboli . . . . . . . . . . . . . 273.4 Modifiche TLS . . . . . . . . . . . . . . . . . . . . . . . . . . 28

3.4.1 Dichiarazioni PPL_TLS . . . . . . . . . . . . . . . . . 283.5 Inizializzazione . . . . . . . . . . . . . . . . . . . . . . . . . . 303.6 Funzione wrapper . . . . . . . . . . . . . . . . . . . . . . . . . 313.7 Interfaccia C . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33

3.7.1 Inizializzazione . . . . . . . . . . . . . . . . . . . . . . 34

4 Testing 364.1 ppl_lcdd . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36

4.1.1 Modifiche a ppl_lcdd . . . . . . . . . . . . . . . . . . . 374.1.2 Prova pratica . . . . . . . . . . . . . . . . . . . . . . . 40

4.2 ppl_lpsol . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 424.2.1 Modifiche a ppl_lpsol . . . . . . . . . . . . . . . . . . 434.2.2 Prova pratica . . . . . . . . . . . . . . . . . . . . . . . 45

Bibliografia 50

Introduzione

Un thread di esecuzione è un singolo flusso di controllo in un programma, apartire dall’invocazione iniziale di una funzione e includendo ricorsivamenteogni funzione successivamente invocata. In un processo, che è il programmain esecuzione, ogni thread può accedere ad un oggetto o funzione ad essoappartenente. L’esecuzione di un programma consiste nell’esecuzione di tuttii suoi thread.

Per single threading si intende l’esecuzione di un processo formato da ununico thread, per cui in ogni momento è in esecuzione una sola istruzione delprogramma.

Per multi threading si intende un metodo di programmazione ed esecuzio-ne che permette l’esistenza di più thread nello stesso contesto, che riguardanoun processo. In altre parole è l’esecuzione concorrente di diversi thread, do-ve questi ultimi condividono risorse come la memoria, il codice eseguibile, idescrittori dei file e le periferiche in uso.

L’insieme degli elementi appena elencati formano lo stato del processo.

Parma Polyhedra LibraryLa Parma Polyhedra Library (PPL, [1]) fornisce astrazioni numeriche indiriz-zate soprattutto alle applicazioni nel campo dell’analisi e verifica di sistemicomplessi. Queste astrazioni includono:

• poliedri convessi, definiti come l’intersezione di un numero finito disemispazi (aperti o chiusi), ciascuno descritto da una disuguaglianzalineare (stretta o non stretta) con coefficienti razionali;

• alcune classi speciali di poliedri che hanno lo scopo di offrire un com-promesso tra complessità e precisione;

• grid (griglie) che rappresentano punti regolarmente distanziati che sod-disfano una serie di relazioni di congruenza lineari.

Introduzione 4

La libreria supporta anche sottoinsiemi finiti di poliedri e prodotti cartesia-ni tra poliedri (di qualsiasi tipo) e grid, un risolutore di problemi di pro-grammazione lineare a interi misti tramite una versione ad aritmetica esattadell’algoritmo del simplesso, un risolutore per la programmazione lineare pa-rametrica, e primitive per l’analisi di terminazione via sintesi automatica difunzioni lineari di ranking.

PPL e MultithreadingPer iniziare è bene distinguere due diversi obiettivi: quello di scrivere libre-rie multi-thread e quello di scrivere librerie che possono essere chiamate daprogrammi multi-thread, garantendo la correttezza.

È possibile creare una libreria che è multi-thread internamente, cioè allaricezione di una chiamata essa suddivide il lavoro su diversi thread per ot-tenere un miglioramento prestazionale (vedere Figura 1). Alternativamente,

Figura 1: Libreria che genera internamente dei thread

si può volere una libreria che si comporta in maniera corretta se chiamatada un programma multithread. Quando una libreria possiede questa secondaproprietà, si dice che essa è Thread Safe (vedere Figura 2). Altrimenti, sidirà che la libreria è Thread Unsafe.

Introduzione 5

Figura 2: Libreria thread-safe

Inizialmente la PPL non aveva nessuna delle proprietà sopra elencate: erauna libreria single-thread e thread-unsafe. Infatti, lo scopo di questo lavoro èrendere la PPL thread-safe e non verrà presa in considerazione la possibilità(seppure interessante) di aumentare le prestazioni della libreria facendolegenerare thread internamente. Ci si concentra appunto sulla sua correttezzae robustezza nel caso l’utente volesse utilizzarla in un applicativo a threadmultipli. Una libreria, ovviamente, può contemporaneamente essere thread-safe e generare thread internamente.

Capitolo 1

Thread Safety

Come già accennato, ogni thread viene eseguito indipendentemente dagli altrie ognuno esegue diverse sequenze di istruzioni. In ogni caso tutti i threadcondividono lo stesso spazio degli indirizzi, e possono accedere alle stesseistanze di certe categorie di dati. Infatti nella programmazione multi-threadle variabili globali rimangono tali, puntatori e riferimenti ad oggetti possonoessere condivisi fra thread.

In questo capitolo si parlerà degli elementi più sensibili dal punto di vistadella thread-safety, di come individiarli e delle possibili soluzioni.

1.1 Codice Thread UnsafeVerranno mostrati in maniera preliminare le tipologie di variabili che rendonothread-unsafe una porzione di codice. Come è stato già detto i thread con-dividono le risorse del proecesso, quindi bisogna vedere come è strutturatoquest’ultimo per individuarne i punti critici. Trattandosi di una libreria de-dicata a calcoli matematici è possibile escludere ragionamenti su risorse chenon siano la memoria di un processo: sarà l’utente a preoccuparsi di even-tuali condivisione degli stessi file descriptor, socket e così via. Sarà invecefondamentale la gestione della memoria. Nell’architettura di un processo 1.1,la memoria assegnatagli è generalmente composta dalle seguenti aree:

• stack ;

• heap;

• area per allocazione statica dei dati, cioè segmento dei dati;

• segmento del codice del programma;

1.1 Codice Thread Unsafe 7

Stack L’area dello stack non presenta momenti critici in relazione all’o-biettivo posto, dal momento che ogni thread ne possiede uno ed è destinatoa contenere le variabili locali, che contano esclusivamente nello stack framein cui si trovano. Questa sicurezza però potrebbe essere compromessa se siprende l’indirizzo di un oggetto sullo stack e lo si passa ad un altro thread:tali situazioni vanno gestite con cura, possono essere causati dei data race epuntatori ad aree deallocate (se il thread a cui apparteneva tale stack nonc’è più).

Heap L’heap è l’area assegnata al processo per rispondere alle esigenzedi allocazione dinamica della memoria. Essa è condivisa da tutti i threadpresenti nel processo e a prima vista potrebbe essere un problema per lathread-safety. Lo standard C++11 (come anche quello precedente) offre lenew expression [8, New, Sezione 5.3.4]. Esempio di new expression:

1 Object* object = new Object(args ...);23 int* array1 = new int[5] {1, 2, 3, 4, 5};

Una volta chiamata, una new expression ottiene spazio per l’oggetto chia-mando l’operator new, se il tipo da allocare è un array invoca l’operatornew[] [8, Dynamic memory management, Sezione 18.6]. Dopo di che la newexpression invoca il costruttore del relativo oggetto inizializzandolo nella lo-cazione appena ottenuta. Gli elementi sui cui porre attenzione ora sono due:operator new e costruttori. Per quanto riguarda l’operator new, lo stan-dard promette che, in caso di chiamate concorrenti, esso allocherà una por-zione di memoria alla volta, senza quindi creare problemi [8, Data races,Sezione 18.6.1.4]. La new expression potrebbe invocare più costruttori: quel-lo dell’oggetto per il quale è stata chiamata, costruttori di sue classi basee costruttori di suoi membri. Tutti loro devono essere thread-safe per far sìche lo sia l’intera new expression. Il ragionamento è analogo per le deleteexpression [8, Delete, Sezione 5.3.5] e relativi operator delete, operatordelete[] e distruttori coinvolti. Anche qui c’è il rischio che un thread cheha ottenuto un indirizzo tramite la new expression lo renda disponibile adaltri thread; quindi è necessario evitare data race e puntatori non validi.

Allocazione statica dei dati Il segmento dei dati è usato per contenerele variabili globali e static inizializzate già a tempo di compilazione. Ilsegmento può essere ulteriormente suddiviso in area per sola lettura o perlettura e scrittura. Mentre le variabili di sola lettura non presentano alcunpericolo, le altre fanno parte dello stato condiviso del processo modificabile.

1.1 Codice Thread Unsafe 8

1 // Costante globale (nessun pericolo)2 const int a = 1;34 // Variabile globale in area dati5 char b = ’B’;67 void foo() {8 // Variabile static in area dati9 static int c = 2;10 return;11 }

L’area .bss che solitamente fa parte del segmento dati, contiene le variabilinon inizializzate. Essa contiene tutte le variabili static e globali che sonostate inizializzate a valore nullo o non hanno nessuna inizializzazione esplicitanel codice sorgente.

1 // Variabile globale costante (nessun pericolo)2 const int a;34 // Variabili globali in area .bss5 extern char b;6 double c = 0;78 void foo() {9 // Variabile static in area .bss10 static int d;1112 return;13 }

Quindi si è visto che gli oggetti critici per la thread-safety sono costruttorie distruttori, variabili allocate staticamente e le variabili allocate su stack eheap soggette a condvisione. La responsabilità per la condivisione di variabiliallocate su stack e heap è nelle mani dell’utente. Nel capitolo a venire tuttigli aspetti critici verranno trattati separatamente fornendo delle soluzioniadeguate.

1.1.1 Tabella dei simboli

La tabella dei simboli è una struttura dati usata da compilatori e interpreti,dove ogni identificatore nel codice sorgente viene associato alle informazioni

1.1 Codice Thread Unsafe 9

Figura 1.1: Esempio di astrazione dell’architettura di Processo e Thread

relative a dove è stato dichiarato e dove appare, il suo tipo e la porzione delprogramma in cui tale identificatore è valido. Attraverso l’uso della tabella deisimboli è possibile, infatti, risolvere correttamente tutti i riferimenti ai nomiintrodotti dal programmatore, risalendo così all’opportuno oggetto denotato.Senza cominciare ad analizzare le scelte fatte da ogni singolo compilatore, ingenere ogni linguaggio che supporta scope statico, uso di funzioni, proceduree diversi tipi di passaggio dei parametri, fa generare una tabella dei simboliche deve fornire:

• per ogni nome la categoria, cioè variabile, parametro, funzione o pro-cedura;

1.2 Strategie per scrivere codice Thread Safe 10

• per ogni variabile il suo tipo, l’offset nel Record di Attivazione del bloccoin cui è definita;

• per ogni parametro il suo tipo, la modalità di passaggio e l’offset;

• per ogni funzione o procedura l’indirizzo della locazione di memorianella zona codice alla quale inizia la sua traduzione.

In presenza di riferimenti a nomi non appartenenti all’ambiente locale diun blocco, sarà necessario cercare il nome nell’ambiente locale dei blocchi incui il blocco attivo in un determinato momento risulta annidato. Al fine diimplementare correttamente lo scope statico e dinamico è necessario, quindi,conoscere quanti livelli di annidamento ci sono tra il blocco attivo e quelloche definisce il nome utilizzato. Questa informazione può essere calcolata sta-ticamente (e quindi opportunamente memorizzata nella tabella dei simboli)in regime di scope statico.

La tabella dei simboli risulterà particolarmente importante e la si vedrànella Sezione 3.3.2.

1.2 Strategie per scrivere codice Thread SafePrima di tutto è bene conoscere alcuni approcci da seguire per arrivare adavere una libreria thread-safe. Si vedrà di seguito quali di questi approcci siriveleranno utili per la PPL.

1.2.1 Programmazione funzionale

Si tratta di un paradigma di programmazione in cui il flusso di esecuzione delprogramma assume la forma di una serie di valutazioni di funzioni. Il puntodi forza principale di questo paradigma è la mancanza di effetti collateralidelle funzioni, il che comporta una più facile verifica della correttezza e dellamancanza di bug del programma e la possibilità di una maggiore ottimizza-zione dello stesso. Un uso particolare del paradigma, per l’ottimizzazione deiprogrammi, è quello di trasformare gli stessi per utilizzarli nella program-mazione parallela. I problemi che sorgono quando si costruisce una libreriathread-safe generalmente derivano dall’uso di stati condivisi tra le chiamate.Una scelta è quella di eliminare semplicemente qualsiasi stato memorizzato,utilizzando uno stile di programmazione funzionale.

Tuttavia la programmazione funzionale è una scelta opportuna se si scriveuna libreria da zero, e non è il caso della PPL. Quest’ultima contiene codicescritto anche non recentemente, e in funzione soprattutto dell’efficienza: sono

1.2 Strategie per scrivere codice Thread Safe 11

presenti un numero non trascurabile di variabili globali (e anche variabililocali allocate staticamente).

1.2.2 Conservazione stato nel thread chiamante

In questo modello, lo stato delle variabili viene mantenuto dal chiamante.L’API (Application Programming Interface) deve fornire un modo alla libre-ria per accedere allo stato, in genere utilizzando un puntatore passato adogni chiamata alla libreria. Così facendo ogni chiamante mantiene uno statocompletamente isolato da altri thread ed è un approccio comodo quando siprevedono scambi di flussi di controllo fra diversi thread (Figura 1.2).

Figura 1.2: Conservazione stato nel chiamante

Il principale svantaggio è che l’API in questione va modificata, richieden-do modifiche alle applicazioni. Prendere questa strada significa complicareparecchio la situazione, nel caso della PPL.

1.2.3 Thread Local Storage

Gli stati delle variabili si possono salvare nell’ambiente locale dei thread.Da qui nascono le variabili Thread Local, esse permettono di avere istanzeseparate dello stesso oggetto per ogni thread del programma. Il punto di

1.2 Strategie per scrivere codice Thread Safe 12

forza di questo approccio è che non richiede la modifica dell’interfaccia dellalibreria (Figura 1.3).

Figura 1.3: Thread Local Storage

Si potrebbe obiettare che il metodo Thread Local Storage (TLS), in alcunicasi, trasformi in difetto il suo più grande pregio: quello di isolare comple-tamente i thread gli uni dagli altri. Questo può essere un problema quandoun applicativo prevede di passare flussi di esecuzione fra thread. Si vedràcomunque che tale problema è aggirabile e, siccome il meccanismo TLS sem-bra interessante e complica di meno l’obiettivo posto, verrà scelto e discussomeglio più in avanti (si veda nella Sezione 3.2).

1.2.4 Mantenere istanze diverse di una libreria

Durante l’esecuzione, è possibile caricare in memoria copie separate dellastessa libreria che può essere Thread Unsafe, quindi con un’istanza unicaper ogni thread chiamante. Il vantaggio di questa scelta sta nel fatto che ilprogrammatore può caricare in memoria tante copie della stessa libreria (nonthread-safe) quanti sono i thread di cui ha bisogno, senza per questo doverconoscere la sua implementazione (Figura 1.4). Non lo si può fare facilmentesu tutti i sistemi operativi, dato che alcuni di loro seguono la politica dellelibrerie condivise, ed è senza dubbio una soluzione esigente dal punto di vistadelle risorse.

1.2 Strategie per scrivere codice Thread Safe 13

Figura 1.4: Istanze diverse di una Libreria Thread Unsafe

È inoportuno pensare a una strada del genere, dato che si sposa poco conl’obiettivo di rilasciare una versione thread-safe della PPL, semplificando illavoro degli utenti.

1.2.5 Stati Condivisi

Si vedrà più in avanti che, in un processo, non sempre si ha la necessitàdi isolare una variabile a livello di thread. Talvolta questi ultimi devonocondividere lo stato di un oggetto, sarà compito della libreria mantenerlo inuno stato ben definito e sincronizzare le letture e gli accessi, se ce ne fossebisogno (Figura 1.5).

1.2.6 Strategie adottate nel caso della PPL

Si noti che è possibile mescolare più di una tra le metodologie discusse. Inquesto caso particolare, l’attenzione cadrà sulle variabili TLS e sugli statidelle variabili condivisi. Il meccanismo TLS verrà ampiamente discusso nellaSezione 3.2. Non è possibile sfruttare i principi della programmazione funzio-nale in quanto la PPL va modificata e non scritta da zero. La PPL è stata giàimplementata secondo principi che non rispettano i criteri della programma-zione funzionale. Non è nemmeno possibile la conservazione dello stato nel

1.2 Strategie per scrivere codice Thread Safe 14

Figura 1.5: Stati Condivisi

thread chiamante, dato che andrebbe modificata l’interfaccia della libreria. Asua volta questo implicherebbe dover modificare tutti i programmi già scrittiche usano la PPL. Anche mantenere istanze diverse della libreria può essereutile in alcuni casi ma non fa al caso della PPL.

Capitolo 2

L’ambiente di sviluppo

Prima di apporofondire il lavoro svolto, in questo capitolo si vedrà la de-scrizione degli strumenti più rilevanti che sono stati usati. Tutti i dettaglidegli strumenti usati verranno presentati qui; il come sono stati impiegati infunzione dell’obiettivo posto lo si vedrà in un secondo momento.

2.1 PiattaformaLa PPL è scritta in C++ standard, con l’intento di essere portabile. Sono pre-senti interfacce in altri linguaggi di programmazione quali C, Java, OCaml eProlog.

Per apportare modifiche interne alla PPL è necessario un supporto alla pro-grammazione multi-thread che non comprometta la portabilità della libreria.A tale scopo lo sviluppo punterà sullo standard C++11, talvolta indicato conC++0x. Esso infatti è il primo a introdurre il supporto per la programmazionemulti-thread, e, fra gli altri, aggiunge i seguenti elementi:

• libreria per il supporto di thread di esecuzione multipli, e per l’intera-zione fra loro;

• miglioramento per il modello di accesso sequenziale alla memoria, chepermette la coesistenza di più thread;

• oggetti e operazioni atomiche;

• Thread Local Storage (TLS).

Il meccanismo TLS si rivelerà particolarmente importante, è stato introdottonella sezione 1.2.3 e verranno approfonditi i suoi aspetti nella sezione 3.2.Ci si potrebbe chiedere per esempio quali fra le implementazioni pthread e

2.2 Configurazione dell’ambiente 16

std::thread sia più efficiente. Tuttavia, come già discusso precedentemente,le modifiche sulla libreria non prevedono l’utilizzo interno di thread attivi,e, per questo motivo, questo lavoro non ha interesse nello stabilire qualesupporto alla programmazione multi-thread sia più efficiente: sarà l’utente aseguire le sue preferenze a riguardo, nelle proprie applicazioni.Verranno analizzate brevemente le alternative all’uso del supporto al mul-tithreading fornito dallo standard C++11. L’utilizzo della libreria Boost [2]avrebbe comportato alla dipendenza da terzi. L’uso diretto dello standardPOSIX [3] per l’utilizzo dei thread avrebbe portato la necessità di considerareil modello TLS implementato da GNU Compiler Collection (GCC, [4]), cheporta delle limitazioni. Esso verrà visto nella sezione 3.2.1.Allo stato attuale esiste già lo standard C++14 che è un’estensione di C++11,ed esiste già il nome informale C++17 per indicare il prossimo rilascio. Tutta-via C++11 è l’ultima versione completamente supportata da un numero piùampio di compilatori [10] quali GCC, Clang++ [5] e Intel C++ [6].

2.1.1 Configurazione elaboratore

Hardware Il lavoro è stato svolto su una macchina che supporta 8 thread, resipossibili da 4 core fisici e altrettanti core logici. È assente qualsiasisupporto al calcolo parallelo tramite scheda grafica, cioè il così dettoGeneral Purpose Graphic Processing Unit (GPGPU), oggi ampiamen-te usato. Quindi nel lavoro svolto è stato impiegato esclusivamente ilprocessore centrale.

SO e Kernel Il sistema operativo alla base è la distribuzione Linux Fedora 21, 64bit. La versione del kernel è 3.18.7-200.fc21.x86_64.

gcc Versione del compilatore gcc 4.9.2 20150212 (Red Hat 4.9.2-6)

clang Versione del compilatore clang 3.5.0 x86_64-redhat-linux-gnu,Thread model: posix

2.2 Configurazione dell’ambienteFacendo uso della nota piattaforma di versionamento dei sorgenti git [11],è stato creato un branch separato da quello ufficiale della libreria PPL. Tra-mite alcune passaggi meccanici sono stati scaricati i file sorgenti della PPL. Loscript di configurazione è stato modificato per aggiungere l’opzione–enable-thread-safe, mediante il quale si può abilitare il supporto allathread-safety. È stato usato il comando autoreconf per aggiornare gli script

2.3 Debug 17

di configurazione. In una directory diversa da quella delle sorgenti, sono statecreate due cartelle dedicate alla generazione di eseguibili e ausiliari tramiteil comando make. La prima directory, build, viene usata per compilare lalibreria PPL in modalità produzione. Nel caso della cartella debug, la PPL vie-ne compilata in funzione della ricerca di anomalie e bug. In questa modalitàperò, la libreria stessa e i programmi demo presenti nella PPL (per esempioppl_lcdd), effettuano numerosi calcoli non necessari per controllare le con-sistenza interna della libreria e quindi possono risultare estremamente lentirispetto alla versione ottimizzata. Ci si sposta in build e si lancia il seguentecomando:

/ppl/configure --enable-interfaces=c,c++ --enable-thread-safe

Dopo di che tocca alla directory debug:

/ppl/configure --enable-interfaces=c,c++ --enable-thread-safe--disable-optimization --enable-assertions

Il comando configure ha diverse opzioni. Tramite –enable-interfacessi comunica quali interfacce verso altri linguaggi abilitare. Oltre all’interfac-cia c e c++ abilitata sopra, si può abilitare l’interfaccia della PPL verso java,ocaml e prolog. Nel caso della directory debug è stata disabilitata l’ottimiz-zazione e sono state abilitate le asserzioni. L’opzione –enable-thread-safeè stata aggiunta in seguito a questo stesso lavoro per far scegliere all’utente sevuole usufruire della PPL in modalità di programmazione parallela. Se questaopzione è assente, verrà compilata la versione della PPL thread-unsafe, ovveroquella iniziale. Si vedrà più avanti come è stato realizzato tale meccanismo,nella Sezione 3.4.

2.3 DebugDurante la fase di stesura di codice aggiuntivo o della modifica di quelloesistente si sono anche verificati casi in cui il codice non funzionava comedesiderato, portando a fallimenti a tempo di esecuzione o risultati inattesi.Per analizzare le situazioni anomale e risalire alla loro causa sono stati usati,in questo lavoro di tirocinio, diversi tool per il debugging del codice. In parti-colare è stato utilizzato il debugger gdb ed i checker messi a disposizione davalgrind (helgrind, memcheck e drd). Per utilizzare questi strumenti in fa-se di sviluppo del codice della PPL è necessario invocarli attraverso libtool,nel modo seguente:

1. libtool --mode=execute gdb --args./<prog_name> <prog_options>

2.4 Altri strumenti 18

2. libtool --mode=execute valgrind --tool=helgrind./<prog_name> <prog_options>

3. libtool --mode=execute valgrind --tool=drd./<prog_name> <prog_options>

4. libtool --mode=execute valgrind --tool=memcheck./<prog_name> <prog_options>

Nella PPL i comandi precedenti vanno eseguiti nella directory debug. gdbpermette di vedere cosa sta succedendo all’interno di un programma men-tre viene eseguito. Esso permette di far partire il programma da analizzarespecificando dei comportamenti che deve assumere; fermare l’esecuzione inun punto a piacere ed esaminare l’accaduto; apportare dei cambiamenti alprogramma in modo da sperimentarne i comportamenti. valgrind offre uninsieme di strumenti che aiutano a rendere il programma più veloce e corret-to. helgrind è particolarmente utile in quanto è specializzato nell’identificareerrori di sincronizzazione in ambienti di programmazione parallela. È statousato particolarmente per la sua abilità di invidividuare accessi a memoriasenza un’adeguata gestione degli stessi. Uno strumento utile da affiancarea helgrind è drd, che svolge funzioni simili. memcheck invece è utile perindividuare accessi a porzioni di memoria già rilasciate, individuare l’uso divalori non ancora inizializzati, memory leak e altro. Quest’ultimo tool si èrivelato particolarmente utile.

2.4 Altri strumenti

2.4.1 GNU nm

Sui sistemi operativi Unix like, GNU nm è un comando usato per esaminare filebinari quali librerie, codice oggetto o eseguibili. La tabella dei simboli 1.1.1viene costruita a tempo di compilazione, una parte delle sue informazionisono salvate nell’object file per consentire il collegamento. Le informazioni adisposizione sono più precise se, compilando con gcc, si attiva l’opzione -g.Esso permette di visualizzare i contenuti di questi file e le meta-informazionicontenute al loro interno, specialmente quella parte della tabella dei simbolicontenuta nell’object file. Per ogni simbolo viene visualizzato suo valore etipo, che può appartenere alle seguenti categorie (lettera minuscola se si trattadi simboli locali, maiuscola se di tipo extern):

• B b, indica che il simbolo si trova nell’area .bss;

2.4 Altri strumenti 19

• D d, indica che il simbolo si trova nel segmento dati;

• R r, indica che il simbolo è read only.

Per un elenco completo delle categorie di simboli previsti da nm, si rimanda illettore alla documentazione ufficiale completa [9]. L’impiego di questo stru-mento ai fini dello studio della PPL verrà descritto più avanti, nella Sezione3.3.2.

2.4.2 GNU grep

GNU grep è un programma che prende in input uno o più nomi di file e cerca,al loro interno, le righe che soddisfano determinati pattern di ricerca. Unavolta individuate, le righe sono copiate su standard output, e questo outputpuò essere formattato a seconda di opzioni aggiuntive che grep offre. Questistrumenti torneranno utili per la ricerca testuale di variabili nella Sezione3.3.1.

Capitolo 3

PPL e Thread Local Storage

3.1 Conditional Thread SafetyLa thread-safety si suddivide in tre livelli, partendo dal più debole per finirecon il più forte:

1. Thread Unsafe, quando al codice sorgente non deve accedere più di unthread alla volta (la situazione iniziale della PPL), nel caso si abbianoaccessi da parte di più thread, l’esito è imprevedibile;

2. Conditionally Thread Safe, quando al codice sorgente vi possono ac-cedere più thread contemporaneamente, ma a condizione che essi nonoperino sullo stesso oggetto, nemmeno in lettura. Come nel caso pre-cedente, l’accesso contemporaneo da parte di più thread allo stessooggetto, se non protetto da opportuni meccanismi di sincronizzazione(la cui predisposizione è a carico dell’utente), potrebbe incorrere in undata race e dare quindi luogo ad comportamento imprevedibile;

3. Thread Safe, se più thread accedono in sola lettura allo stesso oggettoallora l’utente fa a meno di usare meccanismi di sincronizzazione; se al-meno un thread accede in scrittura, vanno implementati degli adeguatimeccanismi di sincronizzazione;

Questi tre termini sono spesso usati dai produttori di software per specificarecon esatteza le proprietà dei loro prodotti.

3.1.1 Standard Template Library C++ e variabili const

Citando lo standard C++11, due valutazioni di espressione vanno in conflittose una di loro modifica una locazione di memoria mentre l’altra cerca di acce-dervi o cercare di modificare la stessa locazione [8, Multi-threaded executions

3.1 Conditional Thread Safety 21

and data races, Sezione 1.10/4]. L’esecuzione di un programma contiene unadata race se contiene due azioni che vanno in conflitto in thread differenti,una delle quali non è atomica e accade che una non venga eseguita primadell’altra [8, Multi-threaded executions and data races, Sezione 1.10/21], incasi del genere si ottiene undefined behavior.

Lo standard offre anche dei requisiti che ogni implementazione di funzioneappartenente alla Standard Template Library (STL) deve rispettare, affinchénon accadano fenomeni di data race [8, Data race avoidance, Sezione 17.6.5.9].Una implementazione della STL non deve modificare direttamente o indiret-tamente oggetti accessibili da thread che non siano quello corrente, a menoche agli oggetti stessi sia stato fatto accesso, direttamente o indirettamente,usando gli argomenti non-const della funzione data, this incluso data race[8, Data race avoidance, Sezione 17.6.5.9/3].

In altre parole questo significa che qualsiasi operazione sugli oggetti ditipo const è thread-safe, e che quindi la STL è thread-safe. Nella prossimasezione si vedrà che la PPL, invece, non garantisce questa proprietà.

3.1.2 PPL e variabili const

Verrà spiegato ora, mediante un esempio astratto, il motivo per il quale la PPLnon può essere facilmente resa thread safe. Queste difficoltà ci indirizzeran-no verso l’obiettivo della conditional-thread-safety. Si vedrà ora un esempioastratto.

Siano in un processo due thread t1 e t2. Supponiamo che entrambi ope-rino sullo stesso numero razionale r che in un determinato momento vale 4

8.

Consideriamo anche il caso apparentemente più innocuo, siano i metodi m1

e m2 che prendono in input il razionale r e promettono di non modificarne ilvalore (ce ne sono diversi di questo tipo nella PPL). t1 invoca m1 passandoglir, e contemporaneamente t2 invoca m2 passandogli r. Come è stato detto,m1 e m2 non modificano r, ma potrebbero avere delle preferenze diverse sul-la rappresentazione interna di r, assumiamo che t1 decida all’interno di m1

di trasformare la rappresentazione 48in 1

2, semplificandolo. Esso comincia a

modificare il numeratore e gli assegna 1, ma in questo momento t2 (tramitem2) decide di leggere r e ottiene 1

8anziché 4

8.

Per ovviare a questo problema, nei metodi di una libreria dovrebberoessere inserite delle operazioni atomiche o la gestione delle risorse tramitesemafori. La PPL non può permettersi il "lusso" di implementare tali mec-canismi: questo implicherebbe un sostanziale peggioramento dell’efficienza,che, stando ai vincoli imposti dagli sviluppatori, è una caratteristica crucialedi questa libreria. La scelta è quella di lasciare che la PPL rimanga a livello

3.2 Thread Local Storage 22

conditional-thread-safe, dove la condizione per gli utilizzatori è quella di nonavere più thread diversi che operano sullo stesso oggetto.

D’ora in poi verrà detto per semplicità che la PPL vorrà essere thread-safe,ma di fatto questo significherà conditional-thread-safe.

3.2 Thread Local StorageQui verrà affrontato in maniera più dettagliata il metodo di programmazio-ne Thread Local Storage (TLS). Esso usa aree di memoria statica o globalelocalmente per un thread. Si ricorre a ciò perché diversi thread possono ri-ferirsi alla stessa area di memoria e non sempre lo si vuole. In altre parole,le variabile global e static vengono allocate normalmente nella stessa areadi memoria, quando ci si riferisce a loro da diversi thread. Le variabili nellostack delle chiamate sono comunque locali ai thread, perché ognuno ha unostack proprio.

3.2.1 Alternative allo standard C++: TLS in GCC

Il metodo di programmazione TLS era supportato dal compilatore GCC ancoraprima dello standard C++11, con una propria implementazione. Consultandola documentazione [4], si evince che essa richiede un supporto significativoda parte del linker ld, linker dinamico ld.so e le librerie di sistema libc.soe libpthread.so, che non sono disponibili ovunque. Il qualificatore di unavariabile thread-local è __thread e può essere usato singolarmente o con leparole chiave extern e static, che vanno messe prima di __thread.

1 // Variabile thread -local2 __thread int i;34 // Variabile thread -local definita altrove5 extern __thread struct state s;67 // Variabile thread -local statica8 static __thread char* p;

Inoltre, se è presente lo specificatore __thread non sono ammesse altreparole chiave oltre alle due già citate. Esso può essere applicato a:

• variabili globali;

• variabili static nello scope di un file;

3.2 Thread Local Storage 23

• variabili static a scope di funzione;

• dati membro static di una classe.

Il qualificatore __thread non può essere usato su variabili non statichenello scope di un blocco o su dati membro non static di una classe.

Quando l’operatore address-of (&) viene applicato ad una variabile thread-local, l’indirizzo viene valutato a tempo di esecuzione e ritorna l’indirizzoche ha tale variabile nell’istanza del thread corrente. L’indirizzo ottenutopuò essere usato poi anche da altri thread, e una volta che il thread origi-nario "muore", qualsiasi puntatore a sue variabili thread-local diventa nonvalido. Nessuna inizializzazione statica può riferirsi all’indirizzo di una va-riabile __thread. In C++, se c’è un inizializzatore per una variabile thread-local, deve essere una constant expression così come definita nello standardANSI/ISO C++ 5.19.2. Come si può vedere appena sotto, quindi, una varia-bile thread-local deve essere inizializzata con una espressione nota a tempodi compilazione e non a run time.

1 // Errato2 int a = 1;3 static __thread int b = a;45 // Corretto6 const int c = 2;7 static __thread int d = c;

3.2.2 TLS in C++11

In C++11, si qualifica una variabile come thread-local tramite la parola chia-ve thread_local. Seguendo lo standard [8, Storage Class Specifiers, Sezio-ne 7.1.1] tale specificatore fa parte della categoria degli storage-class-specifierche comprendono:

• register;

• static;

• thread_local;

• extern;

• mutable.

3.2 Thread Local Storage 24

Al massimo uno di questi può comparire nella dichiarazione di una varia-bile, eccetto thread_local che può essere accompagnato dagli specificatoriextern e static. Se lo specificatore thread_local compare nella dichia-razione di una variabile, lo stesso deve essere presente in ogni sua altradichiarazione. Possono essere dichiarate thread-local:

• le variabili nello scope di un namespace;

• i dati membro static di una classe;

• le variabili locali.

Infatti, secondo lo standard [8, Class Members, Sezione 9.2], il membro diuna classe non può essere dichiarato thread_local se non è static. Se unmembro static è dichiarato thread_local c’è esattamente una copia ditale membro per ogni thread, altrimenti, c’è una copia condivisa da tutti glioggetti di tale classe [8, Static Data Members, Sezione 9.4.2].

1 // Variabile thread -local a scope di namespace2 thread_local int x;34 class tls {5 // Membro static e thread -local di una classe6 static thread_local std:: string s;7 };89 void foo() {10 // Variabile thread -local locale11 thread_local std::vector <int > v;12 }

La storage duration, ovvero la durata di "conservazione", è la proprietàdi un oggetto che definisce il tempo di vita di un contenitore di informazione.thread_local indica che l’oggetto ha la durata di vita del thread, ed è dettathread storage duration [8, Thread Storage Duration, Sezione 3.7.2].

Inizializzazione e Distruzione

L’inizializzazione di una generica variabile con la proprietà thread-storage-duration deve avvenire prima del suo primo utilizzo, e, se costruita, deveessere distrutta prima della terminazione del thread. Se il thread t2 vuoleinizializzare una variabile mentre lo sta già facendo t1, t2 dovrà aspettarefinché l’inizializzazione non avverrà per mano di t1. La distruzione di una

3.3 Ricerca di codice thread-unsafe 25

variabile thread-local locale avverrà solo se è stata in precedenza inizializzata[8, Declaration Statement, Sezione 6.7].

L’inizializzazione di una variabile locale (block scoped) con la proprietàthread-storage-duration, avviene la prima volta che un flusso di controllo cipassa all’interno. Se l’inizializzazione lancia un’eccezione, essa è incompleta, el’inizializzazione verrà ritentata al prossimo flusso di controllo che incontreràtale dichiarazione.

L’inizializzazione di variabili thread_local a visibilità di namespace emembri di classi è implementation defined. Esse sono costruite prima del loroutilizzo proveniente dalla stessa unità di traduzione, ma senza specificarequanto prima. Alcune implementazioni possono inizializzarle appena primadell’utilizzo; appena il thread è stato creato, oppure in istanti ancora diversi.Se nessuna delle variabili thread-local nella stessa unità di traduzione vieneusata, non c’è nessuna garanzia che esse siano mai state costruite.

Per il resto, le variabili thread_local condividono le altre proprietà conquelle static: sono zero-inizializzate prima ancora che avvenga ogni altrotipo di inizializzazione (come quella dinamica). I distruttori delle variabilithread-local vengono invocati in ordine inverso alla loro costruzione, se unthread è ancora in esecuzione quando il thread padre è già uscito, i distruttoridelle sue variabili non vengono invocati. Bisogna tenere conto che l’ordine diinizializzazione non è specificato, ed evitare interdipendenze fra distruttori.

Come è stato già detto, le variabili thread-local hanno diversi indirizziper ogni thread. È possibile ottenere il puntatore di una di loro e accedere atale locazione a partire da diversi flussi di esecuzione, ma il relativo accessoè undefined behaviour se il thread d’origine è già terminato, e la sue relativevariabili thread-local sono state già distrutte.

3.3 Ricerca di codice thread-unsafePer cercare entità thread-unsafe è stata analizzata la directory delle sorgen-ti della PPL e la directory che contiene le interfacce per i diversi linguaggiprevisti:

1. src/;

2. interfaces/<lang>/.

Dove lang è uno dei linguaggi tra C, Java, OCaml e Prolog. In questo lavorodi tirocinio è stata modificata soltanto l’interfaccia C.

La directory src contiene prevalentemente classi, suddivise per file comesegue:

3.3 Ricerca di codice thread-unsafe 26

• file di implementazione;

• *_defs.hh file header contenente la definizione della classe;

• *_inlines.hh file header contenente le sue funzioni di tipo inline;

• *_types.hh file header contenente contenente le dichiarazioni "pure"della classe (le cosiddette forward declaration).

Ci sono alcuni file e altre classi che non rispettano questo schema.Essendo il codice sorgente da analizzare molto ampio, sia in termini di

numero di file sorgenti che, in alcuni casi, di dimensioni dei file stessi, non èproponibile pensare ad una ricerca manuale delle porzioni di codice thread-unsafe; inoltre, l’approccio sarebbe troppo sensibile ad errori e sviste.

3.3.1 Ricerca testuale

Per prima cosa ci si è concentrati sulle variabili di tipo extern. Se una varia-bile è dichiarata con il qualificatore extern questo implica che è soggetta adallocazione statica, è può essere stata dichiarata e/o definita altrove. È statousato lo strumento grep 2.4.2 per la ricerca di tale parola chiave. Sia quindiil seguente insieme di righe generate dal comando

grep -w -n "extern" <dir>/ppl/src/*.

Si usa -w per estrarre solo le righe in cui extern forma una parola intera e-n per mostrare il numero della riga nel file, in modo da facilitare la ricerca.

In C++, non è detto che la parola chiave extern indichi per forza che sitratti di una variabile. Infatti, oltre a un tipo di durata dello storage externviene usata per linkare moduli scritti in linguaggi diversi e viene usata ancheinsieme ai template, per evitare l’istanziazione implicita (a partire da C++11).Per ogni riga quindi, c’è la necessità di verificare manualmente di cosa sitratta: se non è una variabile, la riga viene eliminata dall’insieme. Sia oravar una variabile appartenente a tale insieme, per ogni var è stato verificatose essa è stata definita o dichiarata altrove ripetendo il comando precedente

grep -w -n "<var>" <dir>/ppl/src/*.

Il risultato di questa serie di ricerche su ogni var viene unito con quello delcomando precedente, eliminando i duplicati, in questo modo si è riusciti arisalire ad una parte delle variabili globali della PPL e loro dichiarazioni.

Le variabili static si possono individuare nella stessa maniera, tramiteil comando

3.3 Ricerca di codice thread-unsafe 27

grep -w -n "static" <dir>/ppl/src/*.

Oltre che variabili, di tipo static possono essere anche funzioni membro diuna classe. Anche in questo caso si è andati a verificare manualmente e, se sitrattava di una funzione, essa veniva eliminata dalla lista ottenuta lasciandosolo le variabili.

3.3.2 Ricerca sulla tabella dei simboli

Il comando nm (visto nella Sezione 2.4.1) è stato usato sulle varie libre-rie prodotte da linker nella directory di compilazione build (Sezione 2.3).Spostandosi nella seguente directory

cd build/src/.libs/

è possibile trovare le librerie statiche con estensione .a e quelle dinamiche,con estensione .so. Sono state analizzate le librerie statiche con la seguentesequenza di comandi:

nm -A -C *.a | egrep ’ [A-Z] ’ | egrep -v ’ [UTWRV] ’,

dove -A fa visualizzare il nome del file da dove viene un dato simbolo, e graziea -C è possibile leggere facilmente i namespace ed il nome della variabile.Tramite egrep ’ [A-Z] ’ si filtrano i simboli globali. Tramite egrep -v ’[UTWRV] ’ vengono rimossi i simboli indefiniti; i simboli nell’area del codice;i così detti weak objects ; i simboli che rappresentano variabili read only ed icosì detti weak symbol. Nella ricerca era possibile includere anche i simbolimarcati tramite la u minuscola che aiutano a risalire alle variabili locali ditipo static, ma ciò è superfluo dato che per esse è stata usata la ricercatestuale.

Lo svantaggio di questo metodo è che il procedimento va ripetuto perogni tipo di configurazione di una directory di compilazione. Per esempio,in modalità -enable-assertions, a causa di eventuali direttive condizionalidel preprocessore, potrebbe essere dichiarato un numero diverso di variabiliglobali. Un altro punto a sfavore è che è necessario studiare il comportamentodi nm in base al compilatore usato. Ad ogni modo, esistono altri modi piùeleganti per cercare in maniera sistematica le variabili globali: si può peresempio scrivere un visitor per il parser clang, approccio che comunque nonè stato preferito per via della sua complessità.

3.4 Modifiche TLS 28

3.4 Modifiche TLSUna volta individuato l’insieme delle variabili thread-unsafe esse vanno con-vertite in variabili thread-local. A prescindere dalle varie implementazioniesistenti, il costo dell’utilizzo di variabili thread-local è di norma relativamen-te basso, tanto da non far percepire all’utente alcuna differenza. Tuttavia, sealle variabili thread-local si accede troppo frequentemente il costo aumenta asua volta. C’è anche l’eventualità che l’utente non abbia intenzione di usarela PPL in ambiente multithread, quindi il costo del meccanismo TLS è deltutto superfluo in ogni caso. È stato creato il file header thread_safe.hh incui viene definita la macro PPL_TLS:

1 #ifdef PPL_THREAD_SAFE2 #define PPL_TLS thread_local3 #else4 #define PPL_TLS5 #endif

Alle variabili trovate nella sezione precedente è stato aggiunto come quali-ficatore la macro PPL_TLS. Per errori di mancanza di dichiarazione di PPL_TLS,l’header thread_safe.hh è stato incluso nei seguenti file:

1. global_defs.hh

2. Init_defs.hh

3. Temp_defs.hh

4. Threshold_Watcher_defs.hh

5. Variable_defs.hh

Lo standard C++11 afferma che se lo storage class specifier thread_localcompare in una qualsiasi dichiarazione di una variabile, lo stesso specificatoredeve essere inserito in tutte le sue dichiarazioni [8, Storage Class Specifiers,Sezione 7.1.1/2]. Quando ciò non accadeva, tramite gli errori stessi del com-pilatore GCC è stato possibile risalire a un numero maggiore di dichiarazionidi variabili.

3.4.1 Dichiarazioni PPL_TLS

Verrano ora proposti degli esempi di variabili che hanno ottenuto il quali-ficatore PPL_TLS, nella directory /src. Gli esempi verranno raggruppati inbase al tipo di variabile. Si cercherà anche di quantificare le variabili per ogni

3.4 Modifiche TLS 29

categoria; per le variabili di tipo static i valori possono non essere esattidatto che la categorizzazione è stata fatta manualmente.

Variabili extern

Lo specificatore PPL_TLS è stato inserito per 12 variabili di tipo extern. Sipuò vedere un esempio di variabili PPL_TLS di tipo extern nel file headerCoefficient_inlines.hh:

extern PPL_TLS const Coefficient* Coefficient_zero_p;extern PPL_TLS const Coefficient* Coefficient_one_p;

Variabili static locali

Due delle circa 10 variabili locali static che hanno ottenuto il qualificatorePPL_TLS sono le seguenti:

PPL_TLS static N stop_points[] = {N(-2, ROUND_UP),N(-1, ROUND_UP),N( 0, ROUND_UP),N( 1, ROUND_UP),N( 2, ROUND_UP)

};PPL_TLS static Coefficient zero(0);

stop_points si trova nel file Octagonal_Shape_inlines.hh mentre la se-conda la si trova nel file Coefficient_inlines.hh.

Dati membro static

Hanno ottenuto lo specificatore PPL_TLS circa 32 dati membro di tipo static.Un esempio si può tovare nel file header Congruence_defs.hh:

static const Congruence* zero_dim_false_p;static const Congruence* zero_dim_integrality_p;

Variabili static con visibilità di file

Esistono circa 30 variabili static con visibilità di file che hanno ottenuto lospecificatore PPL_TLS:

3.5 Inizializzazione 30

PPL_TLS static Parma_Polyhedra_Library::Thread_InitParma_Polyhedra_Thread_initializer;

PPL_TLS static Parma_Polyhedra_Library::Thread_Init*Parma_Polyhedra_Thread_initializer_p = 0;

Come già specificato in precedenza, lo standard C++11 esige che se laparola chiave thread_local (che nel nostro caso equivale a PPL_TLS) comepare in una dichiarazione di una variabile essa deve comparire in tutte lealtre dichiarazioni e definizioni. Anche a causa di questa direttiva, alla fine,lo specificatore PPL_TLS è stato inserito come qualificatore per 135 variabili.

Una volta concluso l’inserimento dello specificatore PPL_TLS, ci sono sta-ti problemi nei programmi di prova (che verranno approfonditi nella Sezione4.1): la libreria non veniva più inizializzata automaticamente. Per questo,all’interno del programma, è comparsa la necessità di inizializzare esplicita-mente la PPL prima di ogni utilizzo della stessa.

3.5 InizializzazioneNella PPL, la classe Init è una classe di inizializzazione: essa contiene funzionidi inizializzazione di tutte le altre classi necessarie, e quindi anche variabili.Tutte quante le funzioni vengono eseguite prima di usare la libreria. Dopol’inserimento del meccanismo TLS descritto nella sezione precedente, ci siaspetta avere di più thread di esecuzione e, per ognuno di questi, va eseguital’inizializzazione in quanto hanno tutti una propria versione degli stati dellevariabili. Tuttavia, le funzioni di inizializzazione da invocare, alcune dellequali inizializzano variabili globali, possono essere ulteriormente classificatenel seguente modo:

• Inizializzazione a livello di libreria, cioè funzioni che inizializzanovariabili inizializzabili una sola volta prima di usare la PPL, dove ilnumero dei thread possibili e il loro ordine di esecuzione non influisconosul loro stato;

• Inizializzazione a livello di thread, cioè funzioni che inizializzanovariabili inizializzabili ogni volta che diventa operativo un nuovo thread,in quanto quelle variabili hanno un’istanza diversa in base al thread.

Se tutte le funzioni di inizializzazione fossero a livello di thread, la classeInit non avrebbe dovuto subire modifiche, ma non è così. Infatti, comeafferma la documentazione GMP [12], la funzione

ppl_set_GMP_memory_allocation_functions();

3.6 Funzione wrapper 31

fa uso di variabili globali per memorizzare le funzioni di allocazione di me-moria impostate. Non essendo una libreria su cui si possa mettere mano aifini della tesi, la funzione mp_set_memory_functions non è thread-safe. Nonpotrebbe far parte dell’inizializzazione a livello di thread. Infine, è stato pen-sato che anche modifiche future della PPL possano introdurre variabili globalia livello di libreria, e che quindi una differenziazione torni ancora utile.

La classe Init verrà suddivisa nelle classi Library_Init e Thread_Init.È stato fatto in modo che nella PPL i restanti oggetti globali siano di proprietàesclusiva di un thread; di conseguenza tutti loro verranno inizializzati a livellodi thread, quindi nella classe Thread_Init. Va ricordato ancora che, l’utentenon deve condividere oggetti PPL fra thread diversi.

3.6 Funzione wrapperA questo punto in ogni programma è necessario, prima di ogni altra opera-zione con la PPL, inizializzarla a livello di libreria:

PPL::Library_Init library_init;

In C++11, per iniziare l’esecuzione di un thread è necessario passare al suocostruttore un diverso thread già creato, oppure un oggetto chiamabile (fun-zione, oggetto funzione...) da eseguire nel nuovo thread, seguito dagli argo-menti che servono per invocare l’oggetto chiamabile. All’inizio di ognuno diquesti oggetti chiamabili l’utente deve inizializzare la PPL a livello di thread:

PPL::Thread_Init thread_init;

Si supponga che per qualche ragione l’utente non voglia o non possa modifi-care un oggetto chiamabile già esistente, in cui si usa la PPL. Una soluzione èquella di implementare una funzione wrapper, ovvero una funzione che rivestel’oggetto chiamabile originario, e prima di eseguirlo inizializza la PPL a livellodi thread.

1 template <typename Fun >2 class Threadable_Fun {3 private:4 Fun fun;56 public:7 explicit Threadable_Fun(Fun f) : fun(f) {}89 template <typename ... Args >

3.6 Funzione wrapper 32

10 auto operator ()(Args &&... args) const11 -> decltype(fun(std::forward <Args >(args)...))

{12 Thread_Init thread_init;1314 return fun(std::forward <Args >(args)...);15 }16 };1718 template <typename Fun >19 Threadable_Fun <Fun > make_PPL_Threadable(Fun fun)

{20 return Threadable_Fun <Fun >(fun);21 };

Tale implementazione permette di creare un nuovo thread che prendecome oggetto chiamabile la funzione wrapper che prende come parametrol’oggetto chiamabile, seguita dagli argomenti necessari all’oggetto chiamabile:

std::thread t(PPL::make_PPL_Threadable(oggetto_chiamabile),arg_1,...,arg_n);

Nel caso in cui l’utente facesse uso di thread-pool (verrà visto nella Sezione4.1.1), il seguente è un altro esempio di utilizzo della funzione wrapper:

std::function<void()> func = std::bind(oggetto_chiamabile,arg1_,...,arg_n);

thread_pool.submit(PPL::make_PPL_Threadable(func));

Dove si suppone che il thread-pool sia una classe con il metodo submit cheaggiunge un altro compito da svolgere. In questo caso, tramite std::bind sicrea un oggetto funzione che viene passato alla funzione wrapper.

La funzione templatica make_PPL_Threadable prende come parametroformale un oggetto fun di tipo Fun, e crea un oggetto della classe templaticaThreadable_Fun. Il costruttore della classe Threadable_Fun prende in in-put fun. L’overloading dell’operatore () della classe Threadable_Fun ha lostesso tipo di ritorno di fun, interrogandolo nella dichiarazione (decltype).L’operatore () prende in input gli argomenti necessari a fun. A questo punto

3.7 Interfaccia C 33

viene inizializzata la libreria a livello di thread e, dopo l’esecuzione di fun,viene ritornato il suo stesso tipo di ritorno.

Si ricorda che alla fine di un blocco vengono invocati i distruttori ditutti gli oggetti create in tale blocco. Non è quindi necessario finalizzareesplicitamente la PPL, sia a livello di thread che di libreria. L’operazionedi finalizzazione viene eseguita dai distruttori invocati su library_init ethread_init.

3.7 Interfaccia CTra tutte le interfacce presenti nella PPL, è stato affrontata la thread-safetydell’interfaccia C. Per farlo si è subito pensato di usare lo standard C11, ma ilsupporto per il multithreading secondo questo standard è ancora incompletoe quindi si è deciso di usare le estensioni di GCC. Nel file ppl_c_header èstata definita la macro PPL_C_TLS nel seguente modo:

1 #ifdef PPL_THREAD_SAFE2 #define PPL_C_TLS __thread3 #else4 #define PPL_C_TLS5 #endif

Come nel caso della macro PPL_TLS, PPL_C_TLS verrà usata come qualifica-tore delle variabili che necessitano di essere TLS, se richiesto. Analogamente aquanto fatto con il codice sorgente della PPL, sono stati combinati i risultatidelle seguenti ricerche testuali:

grep -w -n "extern" /ppl/interfaces/C/*grep -w -n "static" /ppl/interfaces/C/*

Dopo di che è stata fatta un ricerca per informazioni della tabella dei simbolinei seguenti file:

nm -A -C *.a | egrep ’ [A-Z] ’ | egrep -v ’ [UTWRV] ’

Nell’interfaccia C della PPL ci sono relativamente pochi file sorgenti, quin-di è stata possibile anche una breve ricerca visiva. Le variabili allocabilistaticamente che necessitano di essere TLS sono le seguenti:

error_handler_type user_error_handler = 0;static char buffer[20];ppl_io_variable_output_function_type* c_variable_output_function;Variable::output_function_type* saved_cxx_Variable_output_function;

3.7 Interfaccia C 34

Parma_Polyhedra_Library::Watchdog* p_timeout_object = 0;Weightwatch* p_deterministic_timeout_object = 0;static timeout_exception e;static timeout_exception e;extern error_handler_type user_error_handler;

Le variabili elencate si trovano tutte nel file ppl_c_implementation_common.ccad eccezione dell’ultima, che è una dichiarazione e nel fileppl_c_implementation_common_defs.cc.

3.7.1 Inizializzazione

L’header initializer.hh che si trova nella directory delle sorgenti della PPL,fornisce le seguenti funzioni:

inline void library_initialize();inline void library_finalize();inline void thread_initialize();inline void thread_finalize();

Le funzioni library_initalize() e thread_initalize() allocano rispetti-vamente gli oggetti Library_Init e Thread_Init. Le funzioni library_finalize()ethread_finalize() invece, li deallocano. Tali funzioni sono chiamabili dainterfacce dei vari linguaggi per inizializzare e finalizzare la PPL. Per quantoriguarda l’interfaccia C, per inizializzare e finalizzare la PPL c’erano i metodi

int ppl_initialize(void);int ppl_finalize(void);

Queste sono state sostituite con le seguenti:

1. int ppl_library_initialize(void) inizializza a livello di libreria,chiamando il metodo library_initalize() e inizializza le variabiliallocabili staticamente che non necessitano di essere TLS, nell’interfacciaC;

2. int ppl_thread_initialize(void) inizializza a livello di thread, chia-mando il metodo thread_initalize() e inizializza un sottoinsiemedelle variabili allocabili staticamente che necessitano di essere TLS,nell’interfaccia C;

3. ppl_library_finalize(void) finalizza a livello di libreria, chiamandoil metodo library_finalize() e impostando la funzione di output didefault;

3.7 Interfaccia C 35

4. ppl_thread_finalize(void) finalizza a livello di thread, chiamandoil metodo thread_finalize() e impostando la funzione di output didefault.

Capitolo 4

Testing

In questo capitolo si descriveranno le modifiche effettuate ad alcuni program-mi demo che utilizzano la PPL, inizialmente sviluppati in versione single-thread, al fine di testare la correttezza e l’efficienza della versione thread-safedella PPL.

4.1 ppl_lcddppl_lcdd è un programma demo allegato alla libreria PPL ed è basato suquest’ultima. Ci sono più modi per rappresentare un poliedro, tramite unaH-rappresentazione o una V-rappresentazione. ppl_lcdd prende in input unpoliedro in V-rappresentazione e produce una H-rappresentazione (o vicever-sa) dello stesso poliedro. All’interno della PPL, ppl_lcdd fa uso di alcunecache globali alle quali si accede senza alcun controllo di sincronizzazione.Come discusso nei capitoli precedenti, nel caso di thread multipli questo cau-serebbe dei data race e comportamenti indefiniti, per cui tali cache sono statedichiarate thread_local. Per questo motivo, il programma ppl_lcdd costi-tuisce un test significativo per la versione thread-safe della libreria. Il pro-gramma è stato modificato per renderlo in grado di operare su più poliedricontemporaneamente producendo risultati corretti per ognuno dei poliedri ininput. Inizialmente ppl_lcdd supportava le seguenti opzioni:

• -CSECS, –max-cpu=SECS, limita il tempo di utilizzo della CPU;

• -RMB, –max-memory=MB, limita l’utilizzo di della memoria a MB mega-byte;

• -h, –help, stampa il testo dell’help su standard output;

• -oPATH, –output=PATH, appendi l’output a PATH;

4.1 ppl_lcdd 37

• -t, –timings, stampa i tempi su stderr;

• -v, –verbose, produce più output durante l’esecuzione;

• -V, –version, stampa su standard output la versione del programma;

• -cPATH, –check=PATH, controlla se il poliedro ottenuto è uguale aquello che c’è in PATH.

Si vedrà come ognuna di queste opzioni finirà nella versione modificata

4.1.1 Modifiche a ppl_lcdd

Verranno elencate le più rilevanti modifiche apportate a ppl_lcdd. Per laprogrammazione parallela è stata usata la classe std::thread.

Come prima cosa, a inizio del main è stata introdotta l’inizializzazione alivello di libreria, cioè creando un oggetto della classe Library_Init.

Il corpo del main si occupa della conversione tra i due tipi di rappresen-tazione del poliedro. Siccome ora i poliedri in input saranno più di uno, ilcodice che si occupa della conversione è stato spostato nel corpo di una nuovafunzione:

void convert(char* input_file_name);

Ogni thread potrà far partire l’esecuzione della funzione convert, ognuno conun nome del file di input diverso. All’inizio della funzione convert è possi-bile inserire l’inizializzazione a livello di thread, ma avendo a disposizione lafunzione wrapper (Sezione 3.6) ciò non è necessario.

ppl_lcdd possiede la funzione

void process_options(int argc, char* argv[]);

che viene chiamata dal main per processare le opzioni ed i file in ingresso.Sono state fatte delle modifiche a process_options in modo che, oltre a cam-biare il comportamento di ppl_lcdd in base alle opzioni, sia capace di rilevarenomi di file multipli in ingresso. Questi file rappresentano i poliedri rappre-sentati in una certa forma. Per ogni poliedro in ingresso process_optionslancia un nuovo thread. Il thread cambia la rappresentazione del poliedro elo salva in una directory specificata tramite l’opzione -o.

A questo punto dello sviluppo è comparso un problema: cosa succede se ilnumero dei poliedri in ingresso supera il numero dei thread supportati dallamacchina? Tale numero, ovviamente, è limitato.

4.1 ppl_lcdd 38

Thread Pool

Il thread-pool indica un gestore software di thread, viene utilizzato per sem-plificare ed ottimizzare l’utilizzo dei thread all’interno di un programma.C’è una moltitudine di tipi di thread-pool e più numerose sono le possibiliimplementazioni.

Nel caso di ppl_lcdd è bastato un thread-pool relativamente semplice.Non è necessario badare a un valore di ritorno dei thread dato che, una voltainiziata la conversione della rappresentazione del poliedro, per tutta la suadurata e alla fine, il master -thread non deve più interagire con il worker -thread. Una ulteriore semplificazione è data dal fatto che tutti i thread sonoindipendenti fra loro. Nel frame 4.1.1 viene rappresentata l’interfaccia delthread-pool inserito nella directory del programma demo ppl_lcdd.

1 class Thread_Pool2 {3 private:4 std:: atomic_bool done;5 Threadsafe_Queue <std::function <void()> >6 work_queue;7 std::vector <std::thread > threads;8 Join_Threads joiner;910 void worker_thread ();1112 public:13 Thread_Pool ();1415 template <typename FunctionType >16 void submit(FunctionType f);17 void free();1819 ~Thread_Pool ();20 };

Nel seguito si fornirà una breve spiegazione del funzionamento di questothread-pool, senza scendere troppo nei dettagli implementativi. Al riguardo,il lettore interessato può consultare il libro [13], dal quale si è preso spun-to per l’implementazione del thread-pool. Una volta creato l’oggetto di tipoThread_Pool, il suo costruttore (riga 13) interroga la macchina hardware suquanti thread riesca a supportare, tramite la funzionestd::thread::hardware_concurrency(). A volte hardware_concurrency()

4.1 ppl_lcdd 39

può ritornare il valore 0: se questo accade il thread-pool assume che la macchi-na sia in grado di supportare 4 thread. Questi thread appena creati vengonosalvati nel vettore threads (riga 7). A tutti i thread viene fatta eseguire lastessa funzione membro worker_thread (riga 10). worker_thread non fa al-tro che verificare se sulla work_queue (riga 6) ci siano oggetti chiamabili daeseguire, che sono di fatto i task che l’utente impone di eseguire al thread-pool, tramite la funzione membro templatica void submit(FunctionTypef). submit infatti aggiunge generici oggetti chiamabili alla work_queue. Inaltre parole, l’utente assegna al thread-pool dei lavori in coda da far fareai thread, che sono tanti quanti la macchina riesce a supportare senza crea-re overhead. Quando un thread finisce un task cerca di prelevarne un’altradalla coda. Quando l’utente è sicuro che tutte i task le abbia assegnate althread-pool, può invocare il metodo void free() per comunicarlo, e taleinformazione verrà salvata in std::atomic_bool done.

Se il blocco in cui è stato creato l’oggetto della classe Thread_Pool giungealla sua fine, viene invocato il suo distruttore che potrebbe mettere fine allavoro dei thread anche se questi non hanno ancora finito la conversione dellarappresentazione dei poliedri. Ma ciò non accade perché Thread_Pool()tenterà di distruggere i membri dati in ordine inverso alla loro dichiarazione,ed il primo è l’oggetto joiner (riga 8) della classe Join_Threads 4.1.1.

1 class Join_Threads2 {3 private:4 std::vector <std::thread >& threads;56 public:7 explicit8 Join_Threads(std::vector <std::thread >&9 threads_);1011 ~Join_Threads ();12 };

L’oggetto joiner viene costruito prendendo in input l’insieme dei threadcreati dal thread-pool. Il distruttore invocato su joiner prima di uscire invo-ca la funzione std::thread::join su ogni thread all’interno del thread-pool.std::thread::join non fa altro che fermare il flusso di esecuzione finché ilthread in questione non ha finito. In questo modo il distruttore invocatosull’oggetto joiner farà aspettare anche il distruttore invocato sull’oggettodella classe Thread_Pool, impedendo la sua distruzione prima che il lavoroin corso venga finito.

4.1 ppl_lcdd 40

L’oggetto work_queue è della classe implementata Threadsafe_Queue,che si comporta come una generica struttura dati di tipo queue, con le stesseoperazioni, ma è anche thread-safe.

Opzioni ppl_lcdd_mt

La versione multi-thread di ppl_lcdd verrà d’ora in poi chiamata ppl_lcdd_mt.Le opzioni eventualmente modificate che sono supportate da ppl_lcdd_mtsono le seguenti:

• -CSECS, –max-cpu=SECS, che limita il tempo di utilizzo della CPU delprocesso, causando la morte di tutti i threads;

• -RMB, –max-memory=MB, che limita l’utilizzo di della memoria a MBmegabyte, a livello di processo;

• -h, –help, stampa i tempi su stderr;

• -oPATH, –output=PATH, tutti gli output vengono salvati come file nelladirectory PATH, i nuovi file avranno lo stessi nome (assieme alla vecchiaestensione) dei file in input, ma con l’aggiunta della stringa .out allafine;

• -V, –version, stampa su standard output la versione del programma.

In ppl_lcdd_mt è stata lasciata anche l’opzione –verbose, ma non è stataadeguatamente testata dal momento che ogni thread potrebbe impegnarelo standard output senza sincronizzazione. È stata tolta del tutto l’opzione–check=PATH.

4.1.2 Prova pratica

La correttezza di ppl_lcdd_mt è stata testata facendolo partire ogni volta conun numero arbitrario di poliedri (a partire da 12 fino a 15) su cui operare. Cosìè stato fatto per più gruppi di poliedri. Ogni volta che finiva la conversionedi un gruppo di poliedri, i risultati venivano confrontati con i risultati dippl_lcdd sugli stessi poliedri. Non sono mai state riscontrate differenze: neitest effettuati ppl_lcdd_mt e ppl_lcdd si sono comportati nello stesso modoper ogni singolo poliedro.

4.1 ppl_lcdd 41

ppl_lcdd_mt con la PPL thread-unsafe

Come verifica ulteriore, sono stati anche effettuati alcuni test con il program-ma ppl_lcdd_mt al quale, però, era stata collegata la versione thread-unsafedella PPL. Come prevedibile, in questo caso l’esecuzione mostra i classici sin-tomi dell’undefined behavior, causando errori a tempo di esecuzione e/o laproduzione di output non corretto. Tali errori sono facilmente riscontrabili,ma avendo natura non deterministica non possono essere riprodotti con esat-tezza e rientrano quindi nella categoria di errori estremamente difficile dacorreggere. Le variabili staticamente allocabili all’interno della PPL non sonoTLS, e quindi ogni thread cerca di scrivere e di leggere sulle stesse variabili,che hanno un’unica istanza, senza alcuna sincronizzazione. Il contenuto deifile in output è imprevedibile.

ppl_lcdd e ppl_lcdd_mt su singolo poliedro

Si vogliono confrontare le prestazioni di ppl_lcdd (sulla PPL thread-unsafe)e ppl_lcdd_mt (sulla PPL thread-safe) su singoli poliedri, con l’intento diriuscire a vedere eventuali inefficenze introdotte dal meccanismo TLS e dallamaggiore complessità di ppl_lcdd_mt. Nella tabella appena sotto è possibilevedere un confronto dei tempi (espresso in secondi) di esecuzione di ognisingolo file elencato. Ogni valore è una media di più prove eseguite. Si nota che

Secondi ppl_lcdd ppl_lcdd_mtcyclic17_8.ine 0.03 0.045dcube12.ext 0.06 0.08kkd38_6.ext 0.1 0.13in7.ine 0.77 1.04mit31-20.ine 2.46 2.95sampleh8.ine 8.05 10.51mit41-16.ine 16.05 18.2

l’esecuzione di ppl_lcdd_mt richiede più tempo man mano che la complessitàdei calcoli aumenta.

ppl_lcdd_mt con più poliedri

Si prenda come esempio il gruppo dei poliedri nella tabella sotto. I tempi diesecuzione sono di ppl_lcdd abbinato alla PPL thread-unsafe. Far convertirea ppl_lcdd tutti questi file in sequenza impiegherebbe almeno 43 secondi,nella condizione ideale in cui tra un comando e l’altro i tempi sono nulli.ppl_lcdd_mt invece, prendendo in input tutti i file elencati alla volta, richiede

4.2 ppl_lpsol 42

Nome file ppl_lcddccc6.ine 0.03dcube12.ext 0.06kkd38_6.ext 0.1in7.ine 0.77mit31-20 2.46cyclic25_13.ext 15.49sampleh8.ine 8.05mit41-16.ine 16.05

20,9 secondi. Il tempo richiesto per convertire il poliedro più impegnativo(mit41-16.ine) è di 18.2 secondi, ciò significa che ppl_lcdd_mt convertetutti i file in un tempo che supera di poco il file più impegnativo.

4.2 ppl_lpsolppl_lpsol, un altro programma demo basato sulla PPL, è un risolutore diproblemi nella programmazione lineare intera e mista scritto in C, anche alloscopo di testare la funzionalità dell’interfaccia della PPL verso tale linguag-gio. Lo useremo quindi, in modo analogo a quanto fatto precedentemente perppl_lcdd, per testare la correttezza della versione thread-safe dell’interfacciaC. ppl_lpsol legge un file nel formato .mps e cerca di trovare la soluzioneusando degli algoritmi di ottimizzazione forniti dalla PPL. ppl_lpsol usastrumenti della PPL che fanno uso della classe MIP_solver. Per facilitare illavoro, le opzioni di ppl_lpsol sono state classificate in due categorie: leopzioni che impongono dei vincoli sul problema da risolvere e le opzioni checondizionano il comportamento del programma, da un punto di vista tecnico.Alla seconda categoria appartengono le seguenti opzioni:

• -c, –check[=THRESHOLD], verifica il risultato ottenuto usadndo GLPK(GNU Linear Programming Kit [14]);

• -CSECS, –max-cpu=SECS, limita il tempo di utilizzo della CPU a SECSsecondi;

• -RMB, –max-memory=MB, limita l’utilizzo di della memoria a MB mega-byte;

• -h, –help, stampa il testo dell’help su standard output;

• -oPATH, –output=PATH, appendi l’output a PATH;

4.2 ppl_lpsol 43

• -t, –timings, stampa i tempi su stderr;

• -v, –verbosity=LEVEL, produce più output durante l’esecuzione;

• -V, –version, stampa su standard output la versione del programma.

Mentre le opzioni che esprimono vincoli per la risoluzione del problema sonole seguenti:

• -i, –incremental, risolvere il problema in maniera incrementale;

• -m, –min, minimizza la funzione oggetto;

• -M, –max, massimizza la funzione oggetto;

• -n, –no-optimization, verificare soltanto la soddisfacibilità;

• -r, –no-mip, considerare variabili intere come reali;

• -e, –enumerate, usare il costoso metodo dell’enumerazione;

• -pM, –pricing=M, da usare insieme al metodo del simplesso, sceglie diusare lo pricing method M;

• -s, –simplex, usare il metodo del simplesso.

Le opzioni di vincolo sulla risoluzione del problema non verranno modificatein alcun modo.

4.2.1 Modifiche a ppl_lpsol

Per la programmazione parallela è stata usata a libreria POSIX ThreadsProgramming [3]. Verrano descritte le principali modifiche in maniera sinte-tica. Non è stato implementato un thread-pool: i thread da eseguire possonoessere al massimo 8. Se ppl_lpsol_mt riceve in input più di 8 file, ne leggerài primi 8, gli altri file verranno ignorati.

È stata introdotta l’inizializzazione a livello di libreria fornita dall’inter-faccia C

if (ppl_library_initialize() < 0)fatal("cannot initialize the Parma Polyhedra Library");

La funzione

static void process_options(int argc, char* argv[]);

4.2 ppl_lpsol 44

è stata modificato in modo che sia capace di fare il parsing di più file in input,che rappresentano i problemi da risolvere. ppl_lpsol fa uso della libreriaglpk che, dopo alcuni tentativi di parallelizzare il programma, ha causatoproblemi dimostrando di non essere thread-safe. A causa di questo problemail procedimento della risoluzione del problema è stato diviso in due funzioni.Dato un problema in input infatti, c’è una prima fase di preparazione delproblema. In questa prima fase si legge il problema e se ne crea l’ambienteper tale problema. Questa prima fase è rappresentata dalla funzione

static pthread_t solve(char* file_name);

Alla fine, la funzione solve crea un thread worker al quale passa il co-strutto di tipo thread_args_t specificamente preparato per il problema daaffrontare:

1 typedef struct {2 ppl_Constraint_System_t ppl_cs;3 ppl_Linear_Expression_t ppl_objective_le;4 ppl_dimension_type* integer_variables;5 ppl_dimension_type num_integer_variables;6 mpz_t den_lcm;7 char* input_file_name;8 FILE* output_file;9 int dimension;10 #ifdef PPL_LPSOL_SUPPORTS_TIMINGS11 struct timeval saved_ru_utime;12 #endif13 } thread_args_t;

solve inizializza i campi nella maniera dovuta. Tale costrutto servirà al fu-turo thread di tipo p_thread per usare i metodi forniti dalla PPL. Il nuovothread rappresenta la seconda fase della risoluzione del problema ed essoeseguirà la funzione

static void* thread_solve(void* vp);

Qui si andrà a operare con gli strumenti forniti dalla PPL. Prima di tutto,all’interno di thread_solve la PPL va inizializzata a livello di thread:

if (ppl_thread_initialize() < 0)fatal("cannot thread-initialize the Parma Polyhedra Library");

Nella fase finale di thread_solve vengono finalizzate tutte le componenti delcostrutto di tipo thread_args_t. A differenza di ppl_lcdd_mt, la libreria va

4.2 ppl_lpsol 45

finalizzata esplicitamente dato che non si dispone in C del meccanismo deidistruttori di un oggetto:

(void) ppl_thread_finalize();

Per riassumere, la funzione solve viene chiamata per ogni problema in inpute ogni chiamata viene risolta in sequenza. Ogni chiamata di solve generaun nuovo thread che continua con la risoluzione del problema usando peròla PPL. In altre parole la risoluzione di un problema in ppl_lpsol è stataparallelizzata a metà.

Sia ora il programma ppl_lpsol_mt il programma ppl_lpsol paralleliz-zato come descritto sopra. Le opzioni che sono state lasciate ed eventualmentemodificate in ppl_lpsol_mt sono le seguenti:

• -CSECS, –max-cpu=SECS, limita il tempo di utilizzo della CPU a SECSsecondi, a livello di processo;

• -RMB, –max-memory=MB, limita l’utilizzo di della memoria a MB mega-byte, a livello di processo;

• -h, –help, stampa il testo dell’help su standard output;

• -oPATH, –output=PATH, tutti gli output vengono salvati come file nelladirectory PATH, i nuovi file avranno lo stessi nome (assieme alla vecchiaestensione) dei file in input, ma con l’aggiunta della stringa .out allafine;

• -V, –version, stampa su standard output la versione del programma.

Come per ppl_lcdd_lpsol, è stata lasciata anche l’opzione –verbose, manon è stata adeguatamente testata dal momento che ogni thread potrebbeimpegnare lo standard output senza sincronizzazione. È stata tolta del tuttol’opzione -c.

4.2.2 Prova pratica

ppl_lpsol_mt con la PPL thread-unsafe

Presi in input più di un problema da risolvere, ppl_lpsol_mt che fa uso dellaPPL thread-unsafe è semplicemente undefined behavior. Le variabili statica-mente allocabili della PPL non sono TLS. Come nel caso di ppl_lcdd_mt,l’esito degli output è imprevedibile.

4.2 ppl_lpsol 46

ppl_lpsol e ppl_lpsol_mt su singolo problema

Si vogliono confrontare le prestazioni di ppl_lpsol e ppl_lpsol_mt su sin-goli problemi, con l’intento di riuscire a vedere eventuali inefficienze intro-dotte dal meccanismo TLS. ppl_lpsol userà la PPL thread-unsafe, mentreppl_lpsol_mt userà la versione thread-safe. Nella tabella appena sotto èpossibile vedere un confronto dei tempi (espresso in secondi) di esecuzione diogni singolo file elencato. Ogni valore è una media di più prove eseguite.

ppl_lpsol ppl_lpsol_mtmarkashare2.mps 0.16 0.16kb2.mps 0.27 0.27mas76.mps 2.48 2.54blend.mps 3.56 7.49mas74.mps 5.01 5.22boeing1.mps 5.85 6.71lseu.mps 21.66 23.02opt1217.mps 62.85 67.19

ppl_lpsol_mt con più problemi

Per risolvere più problemi alla volta, ppl_lpsol_mt necessita che questi pro-blemi siano risolvibili tutti quanti con gli stessi vincoli. Si intende che tuttii problemi devono essere risolvibili con l’unica configurazione di opzioni chel’utente dà al programma. Il test verrà fatto sul seguente insieme di file:

ppl_lpsolmarkashare2.mps 0.16kb2.mps 0.27mas76.mps 2.48blend.mps 3.56mas74.mps 5.01boeing1.mps 5.85lseu.mps 21.66opt1217.mps 62.85

Il tempo richiesto per eseguirli tutti quanti in sequenza sarebbe di almeno102 secondi, con ppl_lpsol. Con ppl_lpsol_mt sono il tempo richiesto è di67.8 secondi, per eseguirli tutti quanti. Si noti anche che ppl_lpsol_mt, pereseguire tutti i file, ha richiesto tanti secondi quanti ne richiede il problemaopt1217.mps, che è il più complesso da risolvere.

Conclusioni

In questo lavoro di tesi si è partiti da una panoramica generale sugli approccida assumere per scrivere una generica libreria thread-safe. In particolare ci siè concentrati sulle tecniche per rendere thread-safe una libreria esistente (ori-ginariamente sviluppata per thread singoli e quindi thread-unsafe). In seguitoè stato selezionato l’approccio che meglio si adatta al caso della PPL. Sonostate fornite le caratteristiche dell’ambiente di sviluppo e si sono discusse lelinee guida da usare nella scelta fra le varie alternative.

Sono stati discussi i vari livelli della thread-safety di una libreria, arrivan-do alla conclusione che l’opzione migliore per la PPL è il livello conditionally-thread-safe.

Sono state approfondite alcune implementazioni del meccanismo ThreadLocal Storage. In seguito ad una serie di valutazioni si è arrivati a optare perla specifica fornita dallo standard C++11, specifica che è stata studiata peresteso per conoscerne al meglio le caratteristiche. Si è quindi passati alla fasedi implementazione che ha richiesto, fra le altre cose, la ricerca sistematicadelle occorrenze di variabili staticamente allocate.

La fase di inizializzazione della PPL è stata spezzata in due: inizializzazionea livello di libreria e a livello di thread. Tale cambiamento nel processo diinizializzazione, non più implicita, comporta un intervento diretto da partedell’utente, che deve ricordarsi di inizializzare la PPL ogni volta che crea unnuovo thread. Per aiutare l’utente è stata introdotta una funzione wrapperche inizializza automaticamente la PPL a livello di thread. In seguito il lavoroè stato esteso per rendere thread-safe anche l’interfaccia verso il linguaggioC della PPL.

Per verificare la correttezza della soluzione adottata sono stati fatti deitest tramite programmi demo della PPL, modificati a loro volta in modoche lavorassero in ambiente multithread. Si è verificato che la PPL producerisultati corretti per ppl_lcdd_mt e ppl_lpsol_mt. Sia con ppl_lcdd_mtche con ppl_lpsol_mt si è verificato che in un processo a singolo thread laversione thread-safe della PPL risulta leggermente più lenta. Si è verificato chein ambienti a thread multipli il vantaggio in termini di velocità di esecuzione

4.2 ppl_lpsol 48

è sostanziale.Il lavoro svolto ha permesso di garantire la thread-safety di una vasta

porzione della PPL, e costituisce un primo e sostanziale passo per ottenere unaversione della PPL thread-safe. Possibili estensioni future potranno riguardarequesti punti:

• estendere la thread-safety alle interfacce Java, Prolog e OCaml;

• adattare la documentazione della PPL spiegando l’implementazione adot-tata per la thread-safety, fornendo esempi su cosa non si può fare;

• testare la portabilità della soluzione adottata, utilizzando compilatoriche non siano GCC e Clang;

• trovare una soluzione per rendere thread-safe la classe Watchdog, cheimplementa dei meccanismi di timeout per la PPL.

Ringraziamenti

Vorrei ringraziare in particolar modo i miei genitori Tamara e Valentin, iquali mi hanno dato l’opportunità di intraprendere questo cammino e mihanno supportato sia durante i momenti di successo che nei momenti piùdifficili. Ringrazio il mio relatore Enea Zaffanella, professore dal quale nonho mai smesso di imparare; che mi ha offerto la possibilità di arricchire lamia esperienza con argomenti che mi appassionano più degli altri nel vastomondo dell’informatica; e professore che ogni ateneo vorrebbe avere. Un altroringraziamento importante per la mia formazione va a Gianfranco Rossi,Roberto Bagnara, Alessandro Zaccagnini, Grazia Lotti, Federico Bergenti eAlessandro Dal Palù. Ringrazio i miei compagni e le compagne di studioche hanno reso questo percorso più vario e divertente: Francesco Trombi,Daniela Pedroni, Sebastian Davrieux, Jacopo Fagio Freddi, Paolo Grossi,Alessio Bortolotti, Victoria Nica, Luca Cirani, Eleonora Fontanesi, FedericoBertoli e tutti coloro che rendevano le pause più interessanti davanti allamacchinetta del caffè.

Ringrazio la mia costante voglia di migliorare

Bibliografia

[1] BUGSENG s.r.l.PPL: Parma Polyhedra Libraryhttp://bugseng.com/products/ppl/

[2] Boost DevelopersBoost: C++ Librarieshttp://www.boost.org/

[3] Austin GroupPOSIX Thread Programminghttps://computing.llnl.gov/tutorials/pthreads/

[4] GCC TeamGCC: GNU Compiler Collectionhttps://gcc.gnu.org/

[5] LLVM Teamclang: a C language family frontend for LLVMhttp://clang.llvm.org/

[6] Intel CompilersIntel C++ Compilerhttps://software.intel.com/en-us/c-compilers

[7] Steve Lewin-Berlin (Intel)Hot and Safe: a Beginner’s Guide to Multithreaded Libraries

[8] ISOC++ International Standard: Final Draft International StandardISO/IEC JTC1 SC22 WG21 N 3290, 11 Aprile 2011

BIBLIOGRAFIA 51

[9] Free Software FoundationGNU Development Tools, nmhttp://linux.die.net/man/1/nm

[10] C++ EnthusiastsC++ compiler supporthttp://en.cppreference.com/w/cpp/compiler_support

[11] Junio Hamanogithttp://git-scm.com/doc

[12] Free Software FoundationThe GNU Multiple Precision Arithmetic Libraryhttps://gmplib.org/manual/Reentrancy.html

[13] Anthony WilliamsC++ Concurrency In Action: Practical MultithreadingManning Publications Co., 2012.

[14] Free Software FoundationThe GNU Linear Programming Kithttps://www.gnu.org/software/glpk