Prodotto Matrice - Vettore in MPI

Похожие документы
Somma di un array di N numeri in MPI

Pigreco in OpenMP. Facoltà di Ingegneria Corso di Studi in Ingegneria Informatica. Elaborato di Calcolo Parallelo. Anno Accademico 2011/2012

Prodotto Matrice - Vettore in OpenMP

Prodotto Matrice - Vettore in MPI II Strategia

Prodotto Matrice - Vettore in MPI - III Strategia

Si digita login e password e si clicca su accedi. Si apre la finestra di collegamento:

UNIVERSITA DEGLI STUDI DI NAPOLI FEDERICO II

L Allocazione Dinamica della Memoria

A.d'Alessio. Calcolo Parallelo. Esempi di topologie

Introduzione a Matlab

L'Allocazione Dinamica della Memoria nel linguaggio C

Calcolo Parallelo. Domanda. In particolare. Qual è l algoritmo parallelo. PROBLEMA: Prodotto Matrice-Vettore

Gestione dinamica della memoria

ERRATA CORRIGE. void SvuotaBuffer(void); void SvuotaBuffer(void) { if(getchar()!=10) {svuotabuffer();} }

Fondamenti di Informatica

Linguaggio C: le funzioni. Introduzione e sintassi

Introduzione alla programmazione Algoritmi e diagrammi di flusso. Sviluppo del software

Funzioni in C. Funzioni. Strategie di programmazione. Funzioni in C. Come riusare il codice? (2/3) Come riusare il codice? (1/3)

STRUTTURE DI CONTROLLO DEL C++

Corso sul linguaggio Java

Funzioni, Stack e Visibilità delle Variabili in C

MPI è una libreria che comprende:

Funzioni. Unità 1. Domenico Daniele Bloisi. Corso di Fondamenti di Informatica Ingegneria delle Comunicazioni BCOR Ingegneria Elettronica BELR

Sistemi lineari. Lorenzo Pareschi. Dipartimento di Matematica & Facoltá di Architettura Universitá di Ferrara

Linguaggio C++ 8. Matrici

Esercizi sulla macchina assembler, strutturazione a livelli, spazio di indirizzamento

C: panoramica. Violetta Lonati

Studio degli algoritmi

Unità Didattica 4 Linguaggio C. Vettori. Puntatori. Funzioni: passaggio di parametri per indirizzo.

Problema. Vettori e matrici. Vettori. Vettori

Università degli Studi di Napoli Parthenope. Corso di Calcolo Parallelo e Distribuito. Virginia Bellino Matr. 108/1570

Elaborato Shell. Elementi di architettura e sistemi operativi 2016/2017

Sistemi Web per il turismo - lezione 3 -

Correttezza (prima parte)

Esercizi di MatLab. Sommario Esercizi di introduzione a MatLab per il corso di Calcolo Numerico e Laboratorio, A.A

La Gestione della Memoria. Carla Binucci e Walter Didimo

Esercitazione 11. Liste semplici

Appunti su Indipendenza Lineare di Vettori

Problema: dati i voti di tutti gli studenti di una classe determinare il voto medio della classe.

Tipi di dati scalari (casting e puntatori) Alessandra Giordani Lunedì 10 maggio 2010

Linguaggio C: le funzioni. Introduzione e sintassi

I puntatori. Un puntatore è una variabile che contiene l indirizzo di un altra variabile. puntatore

SOLUZIONI DELLA PROVA SCRITTA DEL CORSO DI. NUOVO E VECCHIO ORDINAMENTO DIDATTICO 28 Settembre 2006

COMPLESSITÀ COMPUTAZIONALE DEGLI ALGORITMI

Esercizio 2 (punti 7) Dato il seguente programma C: #include <stdio.h> int swap(int * nome, int length);

Strutture dati e loro organizzazione. Gabriella Trucco

Evoluzione del FORTRAN 14/03/2016. LABORATORIO DI PROGRAMMAZIONE Corso di laurea in matematica 15 IL LINGUAGGIO FORTRAN

Esercizi di riepilogo (Fondamenti di Informatica 1 Walter Didimo)

Progetto Matlab N 2. Calcolo Numerico 6 CFU. Corso di Laurea in Ingegneria delle Comunicazioni 31/05/2014

Compitino di Laboratorio di Informatica CdL in Matematica 13/11/2007 Teoria Compito A

Lezione 21 e 22. Valentina Ciriani ( ) Laboratorio di programmazione. Laboratorio di programmazione. Lezione 21 e 22

Introduzione alla programmazione Esercizi risolti

Definizione di metodi in Java

Rappresentazione degli algoritmi

Fondamenti di Informatica 6. Algoritmi e pseudocodifica

Una Libreria di Algebra Lineare per il Calcolo Scientifico

Il Sistema Operativo

Matrici. Matrici.h Definizione dei tipi. Un po di esercizi sulle matrici Semplici. Media difficoltà. Difficili

Complementi ed Esercizi di Informatica Teorica II

Gli Array. Dichiarazione di un array

ESECUZIONE DI PROGRAMMI C SU MACCHINE REALI. Docente: Giorgio Giacinto AA 2008/2009. formalizzazione degli algoritmi in linguaggio C

IL PRIMO PROGRAMMA IN C

Esercizio 2: Algebra dei Puntatori e Puntatori a Puntatori

Alcuni strumenti per lo sviluppo di software su architetture MIMD

Unità F1. Obiettivi. Il linguaggio C. Il linguaggio C++ Linguaggio C. Pseudolinguaggio. Primi programmi

Fondamenti di Informatica T-1 Modulo 2

INTRODUZIONE ALLA PROGRAMMAZIONE AD ALTO LIVELLO IL LINGUAGGIO JAVA. Fondamenti di Informatica - D. Talia - UNICAL 1. Fondamenti di Informatica

2. Algoritmi e Programmi

Sistemi di Elaborazione delle Informazioni

3. Matrici e algebra lineare in MATLAB

ACSO Programmazione di Sistema e Concorrente

Il sistema C è formato dal linguaggio C, dal preprocessore, dal compilatore, dalle librerie e da altri strumenti di supporto.

Le basi del linguaggio Java

VERIFICA DI UNA RETE DI DISTRIBUZIONE INTERNA

SIMULAZIONE DELLA PROVA INTERMEDIA DEL CORSO DI CALCOLATORI ELETTRONICI

Non ci sono vincoli sul tipo degli elementi di un vettore Possiamo dunque avere anche vettori di

15 Riepilogo di esercizi in MATLAB

Транскрипт:

Facoltà di Ingegneria Corso di Studi in Ingegneria Informatica Elaborato di Calcolo Parallelo Prodotto Matrice - Vettore in MPI Anno Accademico 2011/2012 Professoressa Alessandra D Alessio Studenti

Definizione del Problema In quest elaborato ci concentreremo sull operazione di prodotto Matrice Vettore su un architettura parallela MIMD (Multiple Instruction Multiple Data) multi computer, in particolare ci avvarremo di quella a 64 CPU (16 x Intel Xeon E5410 quad-core@2.33ghz 64 bit) offerta dall Università degli Studi di Napoli Federico II attraverso l infrastruttura S.Co.P.E. L obiettivo è la valutazione dell efficienza dell algoritmo da noi implementato, valutazione a cui giungeremo raccogliendo i risultati dei diversi test eseguiti (che si differenziano per la dimensione della matrice su cui verrà effettuata l operazione e/o per il numero di processori coinvolti) per calcolare tre parametri fondamentali: tempo impiegato dall algoritmo parallelo con p processori T(p), speed-up S(p) ed efficienza E(p). Descriviamo in particolare l operazione che vogliamo realizzare. Vogliamo realizzare il calcolo del prodotto: Ax = y ove A è una matrice quadrata,, mentre x e y sono vettori della stessa dimensione della matrice; quindi Definita la matrice A: 2

E il vettore x: Si vuole ottenere il vettore risultato y così calcolato: Si può notare come questa operazione sia fortemente parallelizzabile osservando la natura stessa dei calcoli che l algoritmo dovrà effettuare. Infatti, ciascun elemento del vettore dei risultati y viene calcolato effettuando i prodotti scalari di ogni riga della matrice A per il vettore x. E quindi chiaro che i prodotti scalari possono essere effettuati in maniera indipendente gli uni dagli altri, è proprio questa l idea alla base della parallelizzazione di quest algoritmo. 3

Descrizione dell algoritmo Nome capitolo Esistono tre diverse strategie per il calcolo del prodotto di una matrice A (che supporremo essere sempre quadrata) con un vettore X, il cui comune denominatore è rappresentato dalla presenza delle seguenti fasi: - Suddivisione della matrice a blocchi: A seconda della strategia implementata viene scelto un modo di decomposizione della matrice A, le alternative sono le seguenti: decomposizione in blocchi di righe, decomposizione in blocchi di colonne o decomposizione a scacchiera. Verrà contestualmente effettuata anche l eventuale decomposizione del vettore x fra i vari processori. Tale divisione sarà necessaria o meno a seconda della strategia implementata. - Calcolo del vettore risultato parziale: operando sui blocchi ricevuti al passo precedente, ogni processore calcolerà un vettore risultato parziale. Le componenti di quest ultimo in base alla strategia adottata rappresenteranno o un sottoinsieme delle componenti definitive del vettore finale oppure un contributo al prodotto scalare che deve avere luogo tra ogni riga della matrice ed il vettore x affinché il prodotto matrice vettore sia calcolato correttamente. - Aggregazione: A seconda della strategia scelta, sarà utilizzato un approccio differente per aggregare i risultati parziali ottenuti al passo precedente. 4

Prima strategia (Algoritmo a blocchi di righe) In questo caso il processore designato come ROOT, possessore sia dell intera matrice A che del vettore x, provvederà a frammentare in blocchi di righe la matrice tra i diversi processori, il vettore x sarà invece trasmesso in broadcast a tutti i processori. Ciascun processore riceverà quindi almeno una riga della matrice A e tutto il vettore x, provvederà quindi ad eseguire il prodotto scalare tra ciascuna riga ricevuta e x. Indicando con y il vettore risultante dall operazione di prodotto tra la matrice ed il vettore, si avrà che ogni singolo processore calcolerà tanti elementi del vettore y quante sono le righe della matrice avute in consegna dal ROOT e saranno omologhi tra loro; ciò significa che se un processore ha ricevuto le prime tre righe determinerà i primi tre elementi del vettore risultante y. Alla fine del calcolo parziale, il processore ROOT provvederà a unire i diversi elementi del vettore y calcolati dai diversi processori, andando a formare il vettore risultato completo. Passiamo ora alla descrizione dell algoritmo implementato. In primis definiamo le variabili da utilizzare in seguito, in particolare Matrice ed y (dai nomi è intuibile quali elementi della strategia sopra descritta rappresentino) sono dei puntatori ad intero inizializzati a zero perché per i processori diversi da quello ROOT devono essere solo visibili affinché le funzioni di MPI da essi invocate non diano errori a tempo di compilazione; di fatto tali vettori non saranno mai allocati se non appunto dal processore ROOT. Successivamente inizializziamo l ambiente di esecuzione MPI tramite la funzione di libreria MPI_Init, ad ogni processore assegniamo nella variabile numproc il numero di processori coinvolti sfruttando la funzione di libreria MPI_Comm_size; mentre con la MPI_Comm_rank il processore chiamante potrà memorizzare nella variabile id il proprio identificativo. Una volta accertata la validità degli input inseriti tramite alcuni controlli (tali controlli vengono eseguiti da tutti i processori altrimenti in caso di esito negativo non tutti eseguirebbero la MPI_Finalize()); tutti i processori 5

allocano dinamicamente ed inizializzano i vettori SendCount, Displs, RcvCount e RcvDispls, i primi due sono utilizzati dalla funzione MPI_Scatterv, mentre gli ultimi due dalla MPI_Gatherv. La cardinalità di tali vettori è pari al numero di processori coinvolti mentre i valori memorizzati in ogni singola locazione saranno semanticamente simili ma numericamente diversi per SendCount e RcvCount e per Displs e RcvDispls. Infatti SendCount e RcvCount indicano rispettivamente il numero di elementi che il processore ROOT dovrà inviare/ricevere dai vari processori; nel primo caso gli elementi sono le righe della matrice mentre nel secondo sono componenti del vettore y. Displs e RcvDispls invece indicano lo spostamento all interno del vettore da inviare/ricevere. Le ulteriori variabili definite da tutti i processori sono dim, matloc e ypar in cui saranno memorizzati rispettivamente il numero di righe che ogni processore dovrà ricevere, la sottomatrice su cui ciascuno lavorerà localmente e il sottovettore di y prodotto. Il solo processore ROOT si occuperà del riempimento della matrice Matrice (allocata dinamicamente come un vettore per facilitarne l invio) e del vettore x. Si è scelto di riempire la matrice con una costante numerica unitaria per salvaguardasi da problemi legati all overflow. A questo punto è tutto pronto per la distribuzione delle righe ad ogni processore attraverso la funzione MPI_Scatterv, che abbiamo scelto di utilizzare per assicurare la distribuzione anche nel caso in cui il numero di righe non sia un multiplo del numero dei processori, e al broadcast del vettore x, che avviene mediante la funzione MPI_Bcast. Con la funzione MPI_Barrier siamo sicuri che il calcolo del tempo (come differenza di istanti) sarà avviato non appena tutti possederanno il materiale su cui poter operare per determinare il proprio sottovettore ypar. Quest ultimo, una volta calcolato da ciascun processore, sarà recapitato al processore ROOT attraverso l operazione collettiva MPI_Gatherv, funzione che permette appunto la raccolta di dati a carico del processore ROOT. Infine abbiamo pensato di inserire la funzione MPI_Barrier più per un motivo formale che pratico, ovvero per garantire la correttezza del tempo valutato, non solo limitatamente al caso in cui il calcolo sia compiuto dal processore ROOT. Infatti nel caso specifico di questo algoritmo, l ultimo processore che completerà l operazione di raccolta citata sarà 6

sempre e solo il processore ROOT in quanto depositario del risultato, mentre gli altri finiranno prima. Dato che il tempo viene calcolato dal processore ROOT, siamo sicuri che anche se non avessimo inserito la funzione di barriera il tempo calcolato da questo non sarebbe variato e sarebbe comunque stato relativo alla terminazione del calcolo per tutti i processori coinvolti; di contro, in assenza della barriera dopo la funzione di gatherv, il calcolo del tempo non sarebbe stato corretto nel caso questo fosse stato calcolato da un processore diverso dal ROOT. Infine, prima di eseguire la MPI_Finalize(), per rilasciare l ambiente di esecuzione MPI, sono deallocati tutti i puntatori. Seconda strategia (Algoritmo a blocchi di colonne) Al contrario del caso appena visto, la ripartizione della matrice tra tutti i processori qui avverrà per colonne, pertanto il processore ROOT non dovrà distribuire tutto il vettore x, ma ogni processore ne riceverà solo una parte. In particolare ognuno riceverà un numero di righe di x pari al numero di colonne della propria sottomatrice. Ciascuno parteciperà in parte al calcolo di tutti i prodotti scalari tra ogni singola riga ed il vettore x, pertanto la ricostruzione della y finale passerà attraverso la somma puntuale dei vettori prodotti localmente. Procediamo con la descrizione dell algoritmo, focalizzandoci solo sui punti per i quali questa strategia si differenzia dalla precedente, onde evitare ripetizioni inutili. La fasi di inizializzazione e controllo sono quasi identiche, ciò che cambia è l implementazione della distribuzione delle colonne della matrice. Per realizzare la distribuzione della matrice A, allocata dinamicamente come se fosse un vettore, abbiamo scelto di ricavarne la trasposta, anch essa istanziata come un vettore, per poi inviare le righe di quest ultima a ciascun processore. In questo modo abbiamo potuto semplificare la distribuzione per colonne, avendo di fatto realizzato una distribuzione per righe della matrice trasposta, molto più semplice in quanto si tratta della distribuzione di elementi 7

adiacenti del vettore. I blocchi ricevuti da ogni nodo del communicator MPI_COMM_WORLD coincideranno proprio con le colonne della matrice di partenza; anche qui è impiegata la funzione MPI_Scatterv, scelta al posto della semplice scatter per assicurare la corretta distribuzione della matrice anche nel caso in cui il numero di colonne non sia un multiplo del numero di processori. Per evitare di dover ricalcolare la versione non trasposta della matrice locale ad ogni processore prima di dover lavorare sulla stessa, abbiamo preferito operare su quest ultima invertendo gli indici di riga e colonna. Inoltre visto che l output locale ad ogni processore è un vettore della stessa cardinalità del vettore x, ma in cui saranno memorizzate solo le somme parziali di ogni prodotto scalare riga per colonna, si dovrà provvedere a sommare tali parziali, memorizzati in ypar. Per questo motivo usufruiremo della funzione di libreria MPI_Reduce. Prima di arrestare il conto del tempo, ottenuto come differenza di intervalli, è invocata da tutti i processori la funzione MPI_Barrier. Così come per la strategia vista in precedenza tale invocazione è superflua ai fini dei tempi, ma legittima da un punto di vista formale. Terza strategia (Algoritmo a blocchi quadrati) L ultimo approccio che vedremo può essere facilmente compreso se inquadrato come risultato dell incrocio tra le due precedentemente menzionate. Sulla base di quanto detto si può intuire che ogni processore non riceverà né colonne né righe complete, ma solo una sottoparte di queste. Infatti si ricorrerà alla decomposizione checkboard della matrice, per cui ogni processore si ritroverà a lavorare su un blocco quadrato della matrice iniziale. Dato che ognuno non riceverà mai righe complete della matrice A non dovrà neanche ricevere tutto il vettore x, ma solo la sottoparte di quest ultimo con righe pari in numero e in posizione alla parte di colonne della matrice A ricevute. 8

Ricordando la nostra ipotesi di partenza, ovvero che la matrice A deve essere quadrata, possiamo definire p blocchi quadrati, ciascuno dei quali con cardinalità pari a La risposta implementativa a questo approccio può variare nel modo in cui il vettore x è distribuito ai processori. Per facilitare il processo distributivo, i processori vengono organizzati in una topologia a griglia bidimensionale, la quale rappresenta un organizzazione virtuale dei suoi membri. Occorre però diversificare la modalità di distribuzione della matrice A da quella del vettore x. Nel primo caso il processore zero del MPI_COMM_WORLD in posizione (0,0) nella griglia suddividerà la matrice di partenza in sottomatrici non quadrate il cui numero di righe sarà pari a ed il numero di colonne sarà ancora uguale ad n. Ognuna di tali porzioni dovrà poi essere distribuita ai processori disposti lungo la stessa colonna del processore zero, ovvero la prima. Essi rappresenteranno i motori di distribuzione degli ulteriori frammenti in cui il sottoblocco di matrice ad essi assegnato dovrà essere ulteriormente scomposto. In particolare ciascun processore capo-riga (processore 0 di ogni riga) invierà ad ogni nodo del proprio communicator di riga, una sottomatrice quadrata di cardinalità. Per quanto concerne la distribuzione del vettore x, possiamo evidenziare i seguenti passi: Il processore zero (0,0) spezzetta il vettore x in sottovettori ed invia ogni distinta porzione ai processori del suo communicator di colonna. I processori sulla prima colonna eccetto il processore diagonale (0,0) inviano a quelli disposti sulla diagonale della griglia il frammento del vettore x in loro possesso. Da ogni processore diagonale si diffonde lungo il rispettivo communicator di colonna di appartenenza la sottoparte di x. 9

L ultima precisazione degna di nota è quella relativa alla ricomposizione dei risultati parziali di ogni nodo della griglia. Ricordiamo che ciascuno di essi, al termine delle operazioni locali, disporrà di un sottovettore che su ogni riga ospiterà la somma parziale del prodotto scalare relativo a ciascuna delle riga della matrice A ricevute in parte. Basterà che i processori zero su ogni communicator di riga raccolgano e sommino puntualmente i vettori localmente prodotti dagli altri; infine il processore in posizione (0,0) all interno della griglia riaggregherà in un unico vettore le porzioni della y di cui ciascun processore del suo communicator di colonna è depositario. Per quanto riguarda l'analisi del codice sviluppato si avrà che dopo la fase di inizializzazione e controllo degli input, il processore zero del MPI_COMM_WORLD invia in broadcast a tutti i processori il numero di righe/colonne di ogni blocco che riceveranno, così poi potranno conoscere lo spazio da dover allocare. Esso si occuperà inoltre dell'istanziazione e della relativa inizializzaione della matrice A (nel codice è la matrice M) e deil vettore x. Segue la parte di codice destinata alla creazione della griglia e dei communicator per ogni riga ed ogni colonna della rete topologica. Successivamente solo i processori disposti lungo la prima colonna della griglia (accomunati dall'avere la seconda componente delle coordinate topologiche pari a 0) saranno coinvolti nella chiamata di due MPI_Scatter; prima per ricevere la sottoparte della matrice A e poi per quella di x da parte del processore ROOT, ovvero zero, del MPI_COMM_WORLD. Abbiamo scelto di non utilizzare la MPI_Scatterv perché qui il numero di elementi da distribuire ad ogni processore sarà sempre uguale in numero. I processori considerati provvederanno a calcolare la versione trasposta M1T della matrice non quadrata ricevuta, in modo da lavorare suelementi adiacenti della stessa, allocata sotto forma di vettore, facilitando la distribuzione. Su ogni singolo communicator di riga verrà eseguita la 10

MPI_Scatter. Per quanto concerne la distribuzione della x sono state previste diverse soluzioni. La prima prevede che tale vettore venga distribuito fra tutti i processori della prima colonna e successivamente inviato dagli stessi ai vari processori appartenenti alla diagonale della griglia topologica; essa prevede l utilizzo di MPI_Send e MPI_Recv. Per identificare i processori sulla diagonale abbiamo notato che il loro rank all interno del rispettivo comunicato di riga coincide con il valore della prima coordinata del processore zero sulla medesima riga. La seconda strategia alternativa è basata sulla creazione di un nuovo communicator che includa al suo interno solo i processori disposti sulla diagonale. Tuttavia prima di arrivare a ciò abbiamo creato un gruppo con tutti i processori della griglia e poi un sottogruppo con quelli contenuti nella diagonale della griglia. Il vantaggio del nuovo communicator si esplica nella possibilità di applicare la MPI_Scatter al posto di più di una send. Nell ultima variante è solo il processore zero della griglia ad avere come destinatari quelli sulla diagonale tramite delle MPI_Isend abbinate con MPI_Recv. In questo modo il mittente può risparmiare tempo non dovendo aspettare che il destinatario precedente abbia ricevuto prima del successivo. In seguito tutti i processori della diagonale eseguiranno un broadcast di quanto precedentemente ottenuto sul rispettivo communicator di colonna. Si procederà quindi con il calcolo delle varie componenti del vettore delle soluzioni y. I risultati parziali verrano dapprima sommati mediante l'utilizzo della funzione MPI_Reduce su ciascun communicator di riga ed in seguito raccolte dal processore 0 del COMM_WORLD mediante MPI_Gather eseguita sul communicator della prima colonna. Lo stesso processore 0 calcolerà il tempo totale come differenza tra i due istante temporali a sua disposizione. L ultimo inevitabile passaggio consente di liberare i puntatori utilizzati. 11

Input, Output e Errori In questo capitolo descriveremo il modo di funzionamento dei tre programmi da noi sviluppati, corrispondenti alle tre possibili strategie implementative del prodotto matrice vettore. Parleremo dell interfaccia di questi programmi, andando a specificare gli input richiesti e gli output ottenibili. Per i tre programmi le interfacce sono coincidenti, quindi ne parleremo in generale. Procederemo inoltre alla descrizione delle situazioni d errore in cui è possibile incorrere nell utilizzo dei tre programmi da noi sviluppati. Parametri di Input I parametri forniti in ingresso al programma da noi sviluppato sono due: - n: Intero positivo. Dimensione della matrice quadrata di cui andremo a effettuare il prodotto con il vettore x. Tale vettore avrà la stessa dimensione della matrice. Inoltre, questa dimensione dovrà essere almeno pari al numero di processori su cui è richiesta l esecuzione, altro parametro d ingresso. Se omesso è settato al valore di default, n=20. - p: numero di processori a cui sarà sottomessa l esecuzione dell algoritmo. Il sistema su cui andiamo a lavorare è dotato di 64 core, quindi p dovrà essere necessariamente inferiore a tale numero. 12

Si noti che per utilizzare il programma relativo alla terza strategia realizzativa sarà necessario fornire i due input in maniera tale che n sia un multiplo della radice di p. Si noti, inoltre, che la matrice A è stata riempita, all interno del codice, da un valore numerico costante (1). Il vettore X, invece, è stato riempito da numeri progressivi i, con per poter controllare l effettiva correttezza dell algoritmo. Sono stati scelti questi valori semplici in quanto lo scopo principale di questo lavoro è quello di mostrare l efficienza dell algoritmo di prodotto matrice - vettore parallelo. Parametri di Output Il programma da noi implementato fornisce come output, mostrati a video al termine dell esecuzione, due parametri: - Il tempo impiegato per effettuare i calcoli, in maniera tale da poter fare delle valutazioni riguardanti l efficienza dell algoritmo. - Il vetore risultato Y, contenente il prodotto matrice vettore, calcolato in parallelo sul numero di processori indicati in input. I parametri di input vengono forniti al sistema mediante il file matvec.pbs e, all interno dello stesso file, viene stabilito che il sistema genererà al termine dell esecuzione due file di output: - matvec.out in cui verrà stampato l output del programma; - matvec.err che conterrà gli eventuali errori restituiti dal sistema; Il formato PBS (Portable Batch System) permette di comandare l esecuzione di un insieme di jobs su un architettura parallela. Abbiamo scritto il file matvec.pbs in maniera tale che esso fornisca in ingresso i parametri, evitando di dover modificare il file matvec.c per ogni esecuzione. Riportiamo qui di seguito il contenuto del file matvec.pbs : 13

#!/bin/bash ########################## # # # The PBS directives # # # ########################## #PBS -q studenti #PBS -l nodes=4 #PBS -N matvec #PBS -o matvec.out #PBS -e matvec.err ########################################## # # # Output some useful job information. # # # ########################################## NCPU=`wc -l < $PBS_NODEFILE` echo ------------------------------------------------------ echo ' This job is allocated on '${NCPU' cpu(s)' echo 'Job is running on node(s): ' cat $PBS_NODEFILE PBS_O_WORKDIR=$PBS_O_HOME/matvec echo ------------------------------------------------------ echo PBS: qsub is running on $PBS_O_HOST 14

echo PBS: originating queue is $PBS_O_QUEUE echo PBS: executing queue is $PBS_QUEUE echo PBS: working directory is $PBS_O_WORKDIR echo PBS: execution mode is $PBS_ENVIRONMENT echo PBS: job identifier is $PBS_JOBID echo PBS: job name is $PBS_JOBNAME echo PBS: node file is $PBS_NODEFILE echo PBS: current home directory is $PBS_O_HOME echo PBS: PATH = $PBS_O_PATH echo ------------------------------------------------------ echo "Eseguo: /usr/lib64/openmpi/1.4-gcc/bin/mpicc -o $PBS_O_WORKDIR/matvec $PBS_O_WORKDIR/matvec.c" /usr/lib64/openmpi/1.4-gcc/bin/mpicc -o $PBS_O_WORKDIR/matvec $PBS_O_WORKDIR/matvec.c echo "Eseguo: /usr/lib64/openmpi/1.4-gcc/bin/mpiexec -machinefile $PBS_NODEFILE -np $NCPU $PBS_O_WORKDIR/matvec" /usr/lib64/openmpi/1.4-gcc/bin/mpiexec -machinefile $PBS_NODEFILE -np $NCPU $PBS_O_WORKDIR/matvec 100000 Il numero di processori viene fornito mediante la direttiva #PBS -l nodes=x, mentre la dimensione n della matrice A (e del vettore X) viene sottomesso aggiungendolo in coda alla direttiva di esecuzione mpiexec. Strategia I: Situazioni d errore Elenchiamo qui di seguito le situazioni d errore che vengono restituite dal programma relativo alla prima strategia nel caso di un errato utilizzo dello stesso. I controlli effettuati per la restituzione di tali situazioni d errore riguardano i parametri di input forniti in ingresso dall utente. 15

Elenco delle situazioni d errore previste: 1) Errore dovuto all inserimento di più di un argomento. L unico, eventuale, argomento da fornire in input è n, dimensione della matrice quadrata di cui effettuare il prodotto con il vettore X della stessa dimensione. ERRORE! Sono stati inseriti troppi argomenti! Sintassi corretta: Matvec n_processori [dim_matrix] Il parametro [dim_matrix] e' opzionale ed e' settato a 20 di default. 2) Errore dovuto all inserimento di un valore non intero come dimensione della matrice ERRORE! La cardinalità della matrice deve essere un intero! Il programma terminerà. 3) Errore dovuto alla richiesta di esecuzione su un numero di core superiore a 64, ovvero il numero di core di cui dispone il sistema su cui il programma è stato testato. ERRORE! Il sistema dispone di 64 core. Non è possibile richiederne un numero superiore. Il programma terminerà. 4) Errore dovuto a una combinazione errata dei parametri di input. Il numero di righe della matrice deve essere almeno pari al numero di processori su cui è stata richiesta l esecuzione per assicurare una corretta distribuzione della matrice. ERRORE! La cardinalità della matrice non può essere minore del numero di processori utilizzati! Il programma terminerà. 16

Strategia II: Situazioni d errore Elenchiamo qui di seguito le situazioni d errore che vengono restituite dal programma relativo alla seconda strategia nel caso di un errato utilizzo dello stesso. I controlli effettuati per la restituzione di tali situazioni d errore riguardano i parametri di input forniti in ingresso dall utente. Elenco delle situazioni d errore previste: 1) Errore dovuto all inserimento di più di un argomento. L unico, eventuale, argomento da fornire in input è n, dimensione della matrice quadrata di cui effettuare il prodotto con il vettore X della stessa dimensione. ERRORE! Sono stati inseriti troppi argomenti! Sintassi corretta: Matvec n_processori [dim_matrix] Il parametro [dim_matrix] e' opzionale ed e' settato a 20 di default. NB. Il numero di processori p deve essere tale che la sua radice quadrata sia un intero E la [dim_matrix] deve a sua volta essere un multiplo di tale radice. 2) Errore dovuto all inserimento di un valore non intero come dimensione della matrice ERRORE! La cardinalità della matrice deve essere un intero! Il programma terminerà. 3) Errore dovuto alla richiesta di esecuzione su un numero di core superiore a 64, ovvero il numero di core di cui dispone il sistema su cui il programma è stato testato. ERRORE! Il sistema dispone di 64 core. Non è possibile richiederne un numero superiore. Il programma terminerà. 17

4) Errore dovuto a una combinazione errata dei parametri di input. Il numero di colonne della matrice deve essere almeno pari al numero di processori su cui è stata richiesta l esecuzione per assicurare una corretta distribuzione della matrice. ERRORE! La cardinalità della matrice non può essere minore del numero di processori utilizzati! Il programma terminerà. Strategia III: Situazioni d errore Elenchiamo qui di seguito le situazioni d errore che vengono restituite dal programma relativo alla terza strategia nel caso di un errato utilizzo dello stesso. I controlli effettuati per la restituzione di tali situazioni d errore riguardano i parametri di input forniti in ingresso dall utente. Elenco delle situazioni d errore previste: 1) Errore dovuto all inserimento di più di un argomento. L unico, eventuale, argomento da fornire in input è n, dimensione della matrice quadrata di cui effettuare il prodotto con il vettore X della stessa dimensione. ERRORE! Sono stati inseriti troppi argomenti! Sintassi corretta: Matvec n_processori [dim_matrix] Il parametro [dim_matrix] e' opzionale ed e' settato a 20 di default. 2) Errore dovuto all inserimento di un valore non intero come dimensione della matrice ERRORE! La cardinalità della matrice deve essere un intero! Il programma terminerà. 18

3) Errore dovuto alla richiesta di esecuzione su un numero di core superiore a 64, ovvero il numero di core di cui dispone il sistema su cui il programma è stato testato. ERRORE! Il sistema dispone di 64 core. Non è possibile richiederne un numero superiore. Il programma terminerà. 4) Errore dovuto alla richiesta di esecuzione su un numero di core che non rispetti le specifiche dettate dalla terza strategia. Infatti, il numero di processori deve essere tale che la sua radice quadrata sia un intero. ERRORE! Il numero di processori p deve essere tale che la sua radice quadrata sia un intero! Il programma terminerà. 5) Errore dovuto a una combinazione errata dei parametri di input. Infatti, la dimensione della matrice deve essere un multiplo della radice quadrata del numero di processori. ERRORE! La [dim_matrix] deve essere un multiplo della radice di p! Il programma terminerà. 19

Analisi del software Adesso concentreremo l attenzione sulle tabelle e i grafici relativi al calcolo del tempo di esecuzione T(p), dello speed-up S(p) e dell efficienza E(p). Strategia I Tempo di esecuzione T(p) Nella tabella che segue sono riportati i valori del tempo di esecuzione registrato al variare del numero di processori P e del numero di righe della matrice N. Quindi muovendoci lungo la colonna possiamo capire se c è un vantaggio o meno legato all aumento del numero di processori coinvolti. Tutti i valori del T(p) sono espressi in microsecondi, mentre ciascuno dei valori in tabella è stato ottenuto eseguendo la media sui campioni ottenuti eseguendo una quindicina di prove; un analogo discorso si estende anche ai valori posizionati nelle tabella dello speed-up S(p) e dell efficienza E(p). 20

N 100 500 1000 5000 10000 20000 P 1 84,590912 2194,833755 7557,964325 190684,4139 763561,4395 3086774,254 2 53,596497 1115,79895 4049,062729 95835,39963 382595,5391 1549240,912 4 41,484833 570,631027 2089,421583 48204,04053 192432,5943 775975,4281 8 51,403045 345,230103 1100,206375 24192,19017 96462,58354 393740,3625 Tabella 1 Tempo di esecuzione T(p) prima strategia Figura 2 T(p) al variare del numero di processori e del numero di righe della matrice: prima strategia Guardando la Figura 2 e la Tabella 1 possiamo notare che per N fissato non è sempre conveniente l utilizzo di un architettura parallela. In particolare, per N ridotto l utilizzo dell architettura parallela non è conveniente. Un esempio particolare è quello per N=100 nel quale, passando da p=4 a p=8 i tempi di esecuzione addirittura peggiorano. Possiamo notare che fino a N=1.000 è quasi indifferente l impiego di più processori, mentre iniziamo a scoprirne i vantaggi a partire da N=5.000 e ad avere dei tempi particolarmente soddisfacenti per N=10.000 ed N=20.000. Negli ultimi tre casi citati si ottengono dei tempi particolarmente convenienti per p=8. Si può quindi dedurre, anche osservando la 21

figura 2, ottenuta mediante il software MATLAB, che l utilizzo di un architettura parallela diventa efficace solo per N abbastanza grande. Invertendo il punto di vista, fissando quindi il numero di processori p e variando il valore di N, è possibile notare un ragionevole aumento dei tempi, dovuto alla necessità di effettuare calcoli su ordini di grandezza molto differenti tra loro. Strategia I Speed-up S(p) Nella tabella che segue sono riportati i valori dello speed-up relativi alla prima strategia. Ricordiamo che esso misura, a parità di n, la riduzione del tempo di esecuzione rispetto all algoritmo su 1 processore. Il valore è stato ottenuto a partire dal T(p) medio calcolato a partire da una quindicina di campioni. S( p) T(1) T( p) N 100 500 1000 5000 10000 20000 P 2 1,578291805 1,967051282 1,866596008 1,989707505 1,995740571 1,992443028 4 2,039080451 3,846327401 3,617251964 3,955776566 3,967942345 3,977927834 8 1,645640098 6,357596675 6,869587831 7,882064938 7,915622944 7,839618561 Tabella 2 Valori dello Speed-up S(p) al variare di N e di P prima strategia 22

Figura 3 Valori dello Speed-up S(p) al variare di N e di P prima strategia Il punto di riferimento per giudicare lo speed-up delle nostre esecuzioni è la linea rosa, che rappresenta il valore ideale. Questo grafico conferma ulteriormente le considerazioni fatte osservando i tempi, infatti per N di dimensione ridotta siamo molto lontani dalla linea ideale. Per N=100 e N=500 siamo ancora lontani dall approssimarci al miglior caso possibile, mentre valori nei dintorni di quelli ideali sono stati registrati per N=10.000 e N=20.000. Per quanto riguarda il numero di processori p, la vicinanza ai valori ottimi viene registrata per i valori di N su menzionati con p=2 e p=4, mentre con p=8 i benefici tratti dall uso dell architettura parallela non sono ottimi come nei casi precedenti. Strategia I Efficienza E(p) Nella tabella che segue sono riportati i valori dell efficienza, indicativa di quanto l algoritmo sfrutti il parallelismo del calcolatore. S( p) E( p) p 23

N 100 500 1000 5000 10000 20000 P 2 0,789145903 0,983525641 0,933298004 0,994853753 0,997870285 0,996221514 4 0,509770113 0,96158185 0,904312991 0,988944142 0,991985586 0,994481958 8 0,205705012 0,794699584 0,858698479 0,985258117 0,989452868 0,97995232 Tabella 3 Valori dell Efficienza E(p) al variare di N e p prima strategia Il valore di riferimento di E(p) è rappresentato nella Figura 4, in rapporto allo stesso scopriamo che i nostri test forniscono un risultato interessante per N=5.000 e N=10.000 con p=2 e con p=4, mentre sugli stessi processori per N=20.000 si ottengono valori quasi ideali. In tutti gli altri casi all aumentare di p l efficienza degrada, ciò in piena aderenza con le osservazioni suggeriteci dai grafici di T(p) ed S(p). Figura 4 Efficienza E(p) al variare di N e di p 24

Strategia II Tempo di esecuzione T(p) Nella tabella che segue sono riportati i valori del tempo di esecuzione del programma relativo alla seconda strategia registrati al variare del numero di processori P e del numero N di colonne della matrice. N 100 500 1000 5000 10000 20000 P 1 91,409683 2345,8004 11027,69318 369394,1593 1533705,378 6076872,969 2 58,841705 1252,746557 5797,383799 188659,811 772515,9168 3048968,557 4 55,217743 609,540939 2858,338455 95621,53816 391941,3567 1528295,002 8 62,417984 350,618363 1575,500168 51485,96764 207952,404 816149,8833 Tabella 4 Tempo di esecuzione T(p) seconda strategia Figura 5 T(p) al variare del numero di processori e del numero di colonne della matrice: seconda strategia 25

Guardando la Figura 5 e la Tabella 4 possiamo notare che le considerazioni da fare sono analoghe a quelle fatte per la strategia I. Anche in questo caso è possibile notare che per N ridotto l utilizzo dell architettura parallela non è conveniente. Un esempio particolare è quello per N=100 nel quale, aumentando esponenzialmente il numero di processori su cui è eseguito il programma, i tempi di esecuzione addirittura peggiorano. Possiamo notare che fino a N=1.000 è quasi indifferente l impiego di più processori, mentre iniziamo a scoprirne i vantaggi a partire da N=5.000 e ad avere dei tempi particolarmente soddisfacenti per N=10.000 ed N=20.000. Negli ultimi tre casi citati si ottengono dei tempi particolarmente convenienti per p=8. Come già fatto per la prima strategia, si può quindi dedurre, anche osservando la figura 2, ottenuta mediante il software MATLAB, che l utilizzo di un architettura parallela diventa efficace solo per N abbastanza grande. Invertendo il punto di vista, fissando quindi il numero di processori p e variando il valore di N, è possibile notare un ragionevole aumento dei tempi, dovuto alla necessità di effettuare calcoli su ordini di grandezza molto differenti tra loro. Strategia II Speed-up S(p) Nella tabella che segue sono riportati i valori dello speed-up relativi alla seconda strategia. Ricordiamo che esso misura, a parità di n, la riduzione del tempo di esecuzione rispetto all algoritmo su 1 processore. N 100 500 1000 5000 10000 20000 P 2 1,553484608 1,872525921 1,902184426 1,957990721 1,985338223 1,993091386 4 1,655440408 3,848470627 3,858078163 3,863085309 3,913099119 3,976243435 8 1,464476696 6,690466466 6,999487147 7,174657024 7,375271206 7,445780601 Tabella 5 Valori dello Speed-up S(p) al variare di N e di P seconda strategia 26

Figura 6 Valori dello Speed-up S(p) al variare di N e di P seconda strategia Il punto di riferimento per giudicare lo speed-up delle nostre esecuzioni è la linea tratteggiata, che rappresenta il valore ideale. Questo grafico conferma ulteriormente le considerazioni fatte osservando i tempi, infatti per N di dimensione ridotta siamo molto lontani dalla linea ideale. A differenza di quanto accadeva per la prima strategia, però, il valore dello speed-up si attesta intorno a quello ideale già per N=500. Come nella prima strategia, valori nei dintorni di quelli ideali sono stati registrati per N=10.000 e N=20.000. Per quanto riguarda il numero di processori p, la vicinanza ai valori ottimi viene registrata per i valori di N su menzionati con p=2 e p=4, mentre con p=8 i benefici tratti dall uso dell architettura parallela non sono ottimi come nei casi precedenti. Strategia II Efficienza E(p) Nella tabella che segue sono riportati i valori dell efficienza, indicativa di quanto l algoritmo sfrutti il parallelismo del calcolatore. 27

N 100 500 1000 5000 10000 20000 P 2 0,776742304 0,93626296 0,951092213 0,97899536 0,992669112 0,996545693 4 0,413860102 0,962117657 0,964519541 0,965771327 0,97827478 0,994060859 8 0,183059587 0,836308308 0,874935893 0,896832128 0,921908901 0,930722575 Tabella 6 Valori dell Efficienza E(p) al variare di N e p seconda strategia Il valore di riferimento di E(p) è rappresentato nella Figura 4, in rapporto allo stesso scopriamo che i nostri test forniscono un risultato interessante per N=5.000 e N=10.000 con p=2 e con p=4, mentre sugli stessi processori per N=20.000 si ottengono valori quasi ideali. In tutti gli altri casi all aumentare di p l efficienza degrada, ciò in piena aderenza con le osservazioni suggeriteci dai grafici di T(p) ed S(p). Figura 7 Efficienza E(p) al variare di N e di p 28

Strategia III Tempo di esecuzione T(p) Nella tabella che segue sono riportati i valori del tempo di esecuzione del programma relativo alla terza strategia registrati al variare del numero di processori P e del numero N di colonne della matrice. N 240 1200 7200 15000 20000 P 1 127,102737 12552,494049 715639,770031 3252777,957916 5503644,18 4 45,018402 3356,883774 186690,040588 816229,057312 1378042,97 9 75,026932 2128,134720 88568,952560 368141,893387 617454,206 16 131,018402 1894,122925 54903,863907 206875,829697 346566,162 Tabella 7 Tempo di esecuzione T(p) terza strategia Figura 8 T(p) al variare del numero di processori e del numero di righe della matrice: terza strategia 29

Guardando la Figura 8 e la Tabella 7 la prima cosa da notare è che, rispetto ai casi precedenti, è stato necessario testare l esecuzione del programma su input differenti. Ad ogni modo, le considerazioni da fare sono analoghe a quelle fatte per le altre strategie. Anche in questo caso è possibile notare che per N ridotto l utilizzo dell architettura parallela non è conveniente. Per N=240 si può notare come il tempo d esecuzione per p=16 è peggiore rispetto a p=1. Questo è possibile spiegarlo a causa dei ritardi dovuti alla elevata comunicazione MPI che, però, risulta non utile a causa del ridotto ordine di grandezza dei calcoli da effettuare. Anche in questo caso, iniziamo a scoprire i vantaggi dell uso di un architettura parallela a partire da N=7.200 e ad avere dei tempi particolarmente soddisfacenti per N=15.000 e N=20.000. Negli ultimi tre casi citati si ottengono dei tempi particolarmente convenienti per p=16. Come già fatto per la prima strategia, si può quindi dedurre, anche osservando la figura 8, ottenuta mediante il software MATLAB, che l utilizzo di un architettura parallela diventa efficace solo per N abbastanza grande. Invertendo il punto di vista, fissando quindi il numero di processori p e variando il valore di N, è possibile notare un ragionevole aumento dei tempi, dovuto alla necessità di effettuare calcoli su ordini di grandezza molto differenti tra loro. Strategia III Speed-up S(p) Nella tabella che segue sono riportati i valori dello speed-up relativi alla terza strategia. Ricordiamo che esso misura, a parità di n, la riduzione del tempo di esecuzione rispetto all algoritmo su 1 processore. 30

N 240 1200 7200 15000 20000 P 4 2,823350704 3,739329358 3,833304486 3,985128842 3,99381174 9 1,694094822 5,898354992 8,080029732 8,835663684 8,91344511 16 0,970113626 6,627074665 13,03441541 15,72333492 15,8805007 Tabella 8 Valori dello Speed-up S(p) al variare di N e di P: terza strategia Figura 9 Valori dello Speed-up S(p) al variare di N e di P: terza strategia Il punto di riferimento per giudicare lo speed-up delle nostre esecuzioni è la linea tratteggiata, che rappresenta il valore ideale. Questo grafico conferma ulteriormente le considerazioni fatte osservando i tempi, infatti per N di dimensione ridotta siamo molto lontani dalla linea ideale. Come nelle altre strategie, valori nei dintorni di quelli ideali sono stati registrati per input abbastanza elevati come N=15.000 e N=20.000. Per quanto riguarda il numero di processori p, la vicinanza ai valori ottimi viene registrata per i valori di N su menzionati con p=4 e p=9, in paticolare con N=20.000 notiamo un aderenza quasi ottimale alla linea del valore ideale per tutti i processori con i quali abbiamo testato il programma. 31

Strategia III Efficienza E(p) Nella tabella che segue sono riportati i valori dell efficienza, indicativa di quanto l algoritmo sfrutti il parallelismo del calcolatore. N 240 1200 72000 15000 20000 P 4 0,705837676 0,93483234 0,958326121 0,99628221 0,99845293 9 0,188232758 0,655372777 0,897781081 0,981740409 0,99038279 16 0,060632102 0,414192167 0,814650963 0,982708433 0,99253129 Tabella 9 Valori dell Efficienza E(p) al variare di N e p terza strategia Il valore di riferimento di E(p) è rappresentato nella Figura 10, in rapporto allo stesso scopriamo che i nostri test forniscono un risultato interessante per N=7.200 con p=4 e con p=9, mentre per =15.000 ed N=20.000 si ottengono valori quasi ideali per qualsiasi valore di p. In tutti gli altri casi all aumentare di p l efficienza degrada, ciò in piena aderenza con le osservazioni suggeriteci dai grafici di T(p) ed S(p). Figura 10 Efficienza E(p) al variare di N e di p 32

Esempi d uso e codice In questo capitolo verranno illustrati degli esempi d uso degli algoritmi da noi implementati. Mostreremo in che modo verrà richiamato il programma e come esso verrà mostrato a video. Inoltre, mostreremo qui il codice dei programmi da noi sviluppati, il quale sarà correlato da una documentazione interna che ne spiegherà il funzionamento. Esempio d uso Mostriamo un esempio del risultato che si ottiene a video andando a eseguire uno dei tre programmi da noi implementati. Abbiamo deciso di mostrare un solo esempio d uso in quanto per le tre strategie l output è identico. Forniremo come input una cardinalità della matrice pari 25 e richiederemo che l algoritmo venga eseguito su p=2 processori. Come illustrato nel capitolo dedicato a Input, Output e Situazioni d Errore, tali input saranno forniti al sistema mediante il file matvec.pbs. 33

Figura 2 Esempio esecuzione con N=25 Si nota che vengono mostrate a video le informazioni sugli input forniti, n e p, il tempo totale impiegato per il calcolo del prodotto matrice-vettore ed il relativo risultato dell elaborazione. Il risultato verrà mostrato sottoforma di vettore ed è stato validato confrontando tale vettore con i risultati ottenuti in Matlab; è stato scelto di utilizzare una matrice semplice costituita da tutti 1 in quanto il nostro obiettivo è quello di ottenere informazioni significative riguardo i tempi di esecuzione, essendo già stata provata la correttezza dell algoritmo sviluppato. 34

Strategia I: Codice del programma Riportiamo qui di seguito il codice da noi scritto per la realizzazione di questo programma. Abbiamo utilizzato il linguaggio di programmazione C. Il codice è stato corredato di una documentazione interna che spiega le varie istruzioni utilizzate. /*Algoritmo per il calcolo del prodotto matrice - vettore: strategia I. Autori: Giuffrida Serena M63/239 Lampognana Francesca M63/144 Mele Gianluca M63/145 */ #include <stdio.h> #include <mpi.h> #include "malloc.h" #define ROOT 0 int main(int argc, char **argv) { /***Definizione delle variabili***/ int id, numproc; int dimmatrix=20; // Valore di default relativo alla cardinalità della matrice int dim; int i,j; double tin,tfin,tempotot; int* Matrice=0; // Tali puntatori vengono utilizzati ed allocate solo dal processore 0 int* y=0; // ma devono essere visti da tutti per le varie funzioni offerte da MPI /***Inizializzazione***/ MPI_Init(&argc, &argv); /***Numero processori***/ MPI_Comm_size(MPI_COMM_WORLD, &numproc); /***Individuazione ranks***/ MPI_Comm_rank(MPI_COMM_WORLD, &id); // Controlli sugli input // E' possibile richiamare il programma specificando uno o due argomenti, rispettivamente numero di processori // e cardinalità della matrice if(argc==2) // nel caso in cui viene specificata una cardinalità diversa da quella di default tramite il PBS dimmatrix=atoi(argv[1]); 35

else if(argc>2){ // controllo su eventuale inserimento di un numero eccessivo di argomenti if(!id){ printf("errore! Sono stati inseriti troppi argomenti!\n"); printf("\nsintassi corretta:\n"); printf("matvec n_processori [dim_matrix]\n\n"); printf("il parametro [dim_matrix] e' opzionale ed e' settato a 20 di default.\n"); MPI_Finalize(); return -1; // Controllo sulla cardinalità della matrice che deve essere un intero if((dimmatrix-(int)dimmatrix)!=0){ if(!id){ printf("errore! La cardinalità della matrice deve essere un intero!\n"); printf("il programma terminerà.\n"); MPI_Finalize(); return -1; // Controllo che il numero di righe della matrice sia almeno pari al numero di processori associati per // una corretta distribuzione della stessa if(dimmatrix<numproc){ if(!id){ printf("errore! La cardinalità della matrice non può essere minore del numero di processori utilizzati!\n"); printf("il programma terminerà.\n"); MPI_Finalize(); return -1; // Controllo sull'eventuale richiesta di esecuzione su un numero eccessivo di processori if(numproc>64){ if(!id){ printf("errore! Il sistema dispone di 64 core. Non è possibile richiederne un numero superiore.\n"); printf("il programma terminerà.\n"); MPI_Finalize(); return -1; // Fine controlli sugli input // Istanzio dinamicamente i vettori necessari alla ScatterV e GatherV int* SendCount=(int*)calloc(numproc,sizeof(int)); int* Displs=(int*)calloc(numproc,sizeof(int)); int* RcvCount=(int*)calloc(numproc,sizeof(int)); int* RcvDispls=(int*)calloc(numproc,sizeof(int)); // Inizializzo il vettore SendCount e RcvCount for(i=0;i<numproc;i++){ SendCount[i]=(int)(dimmatrix/numproc); 36

RcvCount[i]=SendCount[i]; if(dimmatrix%numproc>0) // Caso in cui la matrice non sia equidistribuibile fra i vari processori for(i=0;i<dimmatrix%numproc;i++){ SendCount[i]=SendCount[i]+1; RcvCount[i]=RcvCount[i]+1; for(i=0;i<numproc;i++) SendCount[i]=SendCount[i]*dimmatrix; // Inizializzo il vettore Displs e RcvDispls Displs[0]=0; RcvDispls[0]=0; for(i=1;i<numproc;i++){ RcvDispls[i]=RcvDispls[i-1]+RcvCount[i-1]; Displs[i]=Displs[i-1]+SendCount[i-1]; // Ogni processore avrà nella variabile dim il numero di righe assegnategli dim=(int)(dimmatrix/numproc); if(id<(dimmatrix%numproc)) dim=dim+1; // Alloco la matrice parziale che l'id-esimo processore riceverà in base alla I strategia ed il vettore // in cui inserire i risultati parziali int* matloc=(int*)calloc(dim*dimmatrix,sizeof(int*)); int* ypar=(int*)calloc(dim,sizeof(int)); int* x=(int*)calloc(dimmatrix,sizeof(int)); // Il processore ROOT va ad allocare ed istanziare la Matrice,il vettore x ed il vettore delle soluzioni finali y if(!id){ Matrice=(int*)calloc(dimmatrix*dimmatrix,sizeof(int*)); y=(int*)calloc(dimmatrix,sizeof(int)); printf("numero di processori: %d.\n",numproc); printf("dimensione matrice: %d.\n",dimmatrix); for(i=0;i<dimmatrix;i++){ x[i]=i; // definito come sequenza dei primi dimmatrix-1 numeri for(j=0;j<dimmatrix;j++) Matrice[j+i*dimmatrix]=1; // utilizzeremo una matrice costituita da tutti 1 // Distribuzione delle varie matrici parziali e del vettore x a tutti i processori MPI_Scatterv(Matrice,SendCount,Displs,MPI_INT,matloc,dim*dimmatrix,MPI_INT,ROOT, MPI_COMM_WORLD); MPI_Bcast(x,dimmatrix,MPI_INT,ROOT,MPI_COMM_WORLD); MPI_Barrier(MPI_COMM_WORLD); // Aspettiamo che tutti abbiano ricevuto i vari elementi // Inizio valutazione dei tempi if(!id) tin=mpi_wtime(); 37

// Ogni processore effettua il calcolo e andrà a definire la sua porzione del vettore delle soluzioni for(j=0;j<dim;j++){ for(i=0;i<dimmatrix;i++) ypar[j]=ypar[j]+x[i]*matloc[i+j*dimmatrix]; // La gatherv andrà a comporre il vettore finale delle soluzioni raccogliendo i vari risultati // parziali da tutti i processori coinvolti MPI_Gatherv(ypar,dim,MPI_INT,y,RcvCount,RcvDispls,MPI_INT,ROOT,MPI_COMM_WORLD); MPI_Barrier(MPI_COMM_WORLD); if(!id){ tfin=mpi_wtime(); // Fine valutazione dei tempi tempotot=1.e6*(tfin-tin); // il tempo impiegato viene calcolato come differenza degli istanti tfin e tin printf("tempo totale impiegato: %f.\n",tempotot); printf("algoritmo PARALLELO\n"); printf("calcolo su %d processori.\n",numproc); for(i=0;i<dimmatrix;i++) printf("y[%d] = %d\n",i,y[i]); // Deallocazione delle variabili dinamiche istanziate all'interno del programma. free(matrice); free(x); free(y); free(ypar); free(matloc); free(sendcount); free(displs); free(rcvcount); free(rcvdispls); MPI_Finalize(); return 0; 38

Strategia II: Codice del programma Riportiamo qui di seguito il codice da noi scritto per la realizzazione di questo programma. Abbiamo utilizzato il linguaggio di programmazione C. Il codice è stato corredato di una documentazione interna che spiega le varie istruzioni utilizzate. /*Algoritmo per il calcolo del prodotto matrice - vettore: II strategia. Autori: Giuffrida Serena M63/239 Lampognana Francesca M63/144 Mele Gianluca M63/145 */ #include <stdio.h> #include <mpi.h> #include "malloc.h" #define ROOT 0 int main(int argc, char **argv) { /***Definizione delle variabili***/ int id, numproc; int dimmatrix=20; // Valore di default relativo alla cardinalità della matrice int dim; int i,j; double tin,tfin,tempotot; int* Matrice=0; // Tali puntatori vengono utilizzati ed allocate solo dal int* x=0; // processore 0 ma devono essere visti da tutti per le varie int* y=0; // funzioni offerte da MPI int* MTrasposta=0; /***Inizializzazione***/ MPI_Init(&argc, &argv); /***Numero processori***/ MPI_Comm_size(MPI_COMM_WORLD, &numproc); /***Individuazione ranks***/ MPI_Comm_rank(MPI_COMM_WORLD, &id); // Controlli sugli input // E' possibile richiamare il programma specificando uno o due argomenti, rispettivamente numero di processori // e cardinalità della matrice if(argc==2) // nel caso in cui viene specificata una cardinalità diversa da quella di default tramite il PBS dimmatrix=atoi(argv[1]); else if(argc>2){ // controllo su eventuale inserimento di un numero eccessivo di argomenti 39

if(!id){ printf("errore! Sono stati inseriti troppi argomenti!\n"); printf("\nsintassi corretta:\n"); printf("matvec2 n_processori [dim_matrix]\n\n"); printf("il parametro [dim_matrix] e' opzionale ed e' settato a 20 di default.\n"); MPI_Finalize(); return -1; // Controllo sulla cardinalità della matrice che deve essere un intero if((dimmatrix-(int)dimmatrix)!=0){ if(!id){ printf("errore! La cardinalità della matrice deve essere un intero!\n"); printf("il programma terminerà.\n"); MPI_Finalize(); return -1; // Controllo che il numero di righe della matrice sia almeno pari al numero di processori associati per // una corretta distribuzione della stessa if(dimmatrix<numproc){ if(!id){ printf("errore! La cardinalità della matrice non può essere minore del numero di processori utilizzati!\n"); printf("il programma terminerà.\n"); MPI_Finalize(); return -1; // Controllo sull'eventuale richiesta di esecuzione su un numero eccessivo di processori if(numproc>64){ if(!id){ printf("errore! Il sistema dispone di 64 core. Non è possibile richiederne un numero superiore.\n"); printf("il programma terminerà.\n"); MPI_Finalize(); return -1; // Fine controlli sugli input // Istanzio dinamicamente i vettori necessari alla ScatterV e GatherV int* SendCount=(int*)calloc(numproc,sizeof(int)); int* Displs=(int*)calloc(numproc,sizeof(int)); int* RcvCount=(int*)calloc(numproc,sizeof(int)); int* RcvDispls=(int*)calloc(numproc,sizeof(int)); // Inizializzo il vettore SendCount e RcvCount for(i=0;i<numproc;i++){ SendCount[i]=(int)(dimmatrix/numproc); RcvCount[i]=SendCount[i]; 40

if(dimmatrix%numproc>0) // Caso in cui la matrice non sia equidistribuibile fra i vari processori for(i=0;i<dimmatrix%numproc;i++){ SendCount[i]=SendCount[i]+1; RcvCount[i]=RcvCount[i]+1; for(i=0;i<numproc;i++) SendCount[i]=SendCount[i]*dimmatrix; // Inizializzo il vettore Displs e RcvDispls Displs[0]=0; RcvDispls[0]=0; for(i=1;i<numproc;i++){ RcvDispls[i]=RcvDispls[i-1]+RcvCount[i-1]; Displs[i]=Displs[i-1]+SendCount[i-1]; // Ogni processore avrà nella variabile dim il numero di colonne assegnategli dim=(int)(dimmatrix/numproc); if(id<(dimmatrix%numproc)) dim=dim+1; // Alloco la matrice parziale che l'id-esimo processore riceverà in base alla II strategia, il vettore // delle x assegnategli ed il vettore in cui inserire i risultati locali int* matloc=(int*)calloc(dim*dimmatrix,sizeof(int*)); int* xloc=(int*)calloc(dim,sizeof(int)); int* ypar=(int*)calloc(dimmatrix,sizeof(int)); // Il processore ROOT va ad allocare ed istanziare la Matrice, il vettore x ed il vettore delle soluzioni finali y, // esso utilizzerà anche una matrice ausiliaria definita come la trasposta della Matrice; tale matrice faciliterà // la fase di distribuzione della matrice, così come richiesto per la II strategia if(!id){ Matrice=(int*)calloc(dimmatrix*dimmatrix,sizeof(int*)); MTrasposta=(int*)calloc(dimmatrix*dimmatrix,sizeof(int*)); x=(int*)calloc(dimmatrix,sizeof(int)); y=(int*)calloc(dimmatrix,sizeof(int)); printf("numero di processori: %d.\n",numproc); printf("dimensione matrice: %d.\n",dimmatrix); for(i=0;i<dimmatrix;i++){ x[i]=i; // definito come sequenza dei primi dimmatrix-1 numeri for(j=0;j<dimmatrix;j++) Matrice[j+i*dimmatrix]=1; // utilizzeremo una matrice costituita da tutti 1 // Calcolo della matrice trasposta for(i=0;i<dimmatrix;i++) for(j=0;j<dimmatrix;j++) MTrasposta[j+i*dimmatrix]=Matrice[i+j*dimmatrix]; // Distribuzione delle varie matrici parziali e del vettore x a tutti i processori 41

MPI_Scatterv(MTrasposta,SendCount,Displs,MPI_INT,matloc,dim*dimmatrix,MPI_INT,RO OT,MPI_COMM_WORLD); MPI_Scatterv(x,RcvCount,RcvDispls,MPI_INT,xloc,dim,MPI_INT,ROOT,MPI_COMM_WORLD); MPI_Barrier(MPI_COMM_WORLD); // Aspettiamo che tutti abbiano ricevuto i vari elementi // Inizio valutazione dei tempi if(!id) tin=mpi_wtime(); // Ogni processore effettua il calcolo e andrà a definire la sua porzione del vettore delle soluzioni for(j=0;j<dimmatrix;j++){ for(i=0;i<dim;i++) ypar[j]=ypar[j]+xloc[i]*matloc[j+i*dimmatrix]; // avendo utilizzato la trasposta della Matrice iniziale // La funzione Reduce effettua la riunione dei risultati calcolati da ogni processore, calcolando la somma totale. // Il risultato complessivo verrà calcolato dal processore ROOT nella variabile y MPI_Reduce(ypar,y,dimmatrix,MPI_INT,MPI_SUM,ROOT,MPI_COMM_WORLD); MPI_Barrier(MPI_COMM_WORLD); if(!id){ tfin=mpi_wtime(); // Fine valutazione dei tempi tempotot=1.e6*(tfin-tin); // il tempo impiegato viene calcolato come differenza degli istanti tfin e tin printf("tempo totale impiegato: %f.\n",tempotot); printf("algoritmo PARALLELO\n"); printf("calcolo su %d processori.\n",numproc); for(i=0;i<dimmatrix;i++) printf("y[%d] = %d\n",i,y[i]); // Deallocazione delle variabili dinamiche istanziate all'interno del programma. free(matrice); free(mtrasposta); free(x); free(y); free(ypar); free(xloc); free(matloc); free(sendcount); free(displs); free(rcvcount); free(rcvdispls); MPI_Finalize(); return 0; 42

Strategia III: Codice del programma Riportiamo qui di seguito il codice da noi scritto per la realizzazione di questo programma. Abbiamo utilizzato il linguaggio di programmazione C. Il codice è stato corredato di una documentazione interna che spiega le varie istruzioni utilizzate. /*Algoritmo per il calcolo del prodotto Matrice - Vettore: III Strategia. Autori: Giuffrida Serena M63/239 Lampognana Francesca M63/144 Mele Gianluca M63/145 */ #include <stdio.h> #include <mpi.h> #include "malloc.h" #include "math.h" #define ROOT 0 int main(int argc, char **argv) { /***Definizione delle variabili***/ int id,id_griglia, nprocs; int dimmatrix=24; // Valore di default relativo alla cardinalità della matrice int nlocal; int j,i; double tin,tfin,tempotot; int* M=0; // Tali puntatori vengono utilizzati ed allocate solo dal int* x=0; // processore 0 ma devono essere visti da tutti per le varie int* y=0; // funzioni offerte da MPI int* ypar=0; int* M1T=0; // Questi due puntatori invece saranno utilizzati unicamente dai processori int* M1=0; // della prima colonna della griglia per la distribuzione della matrice int dim[2],period[2],coord_griglia[2]; // Variabili utilizzate nella creazione della int free_cord[2]={1,0; // topologia e delle sottotopologie MPI_Comm comm_grid,comm_col,comm_row,comm_diag; MPI_Group grp_aus,grp_diag; MPI_Status stato; MPI_Request rqst; // Communicator ausiliari /***Inizializzazione***/ 43

MPI_Init(&argc, &argv); /***Numero processori***/ MPI_Comm_size(MPI_COMM_WORLD, &nprocs); /***Individuazione ranks***/ MPI_Comm_rank(MPI_COMM_WORLD, &id); // Controlli sugli input // E' possibile richiamare il programma specificando uno o due argomenti, rispettivamente numero di processori // e cardinalità della matrice if(argc==2) // nel caso in cui viene specificata una cardinalità diversa da quella di default tramite il PBS dimmatrix=atoi(argv[1]); else if(argc>2){ //controllo su eventuale inserimento di un numero eccessivo di argomenti if(!id){ printf("errore! Sono stati inseriti troppi argomenti!\n"); printf("\nsintassi corretta:\n"); printf("matvec3 n_processori [dim_matrix]\n\n"); printf("il parametro [dim_matrix] e' opzionale ed e' settato a 20 di default.\n"); printf("nb. Il numero di processori p deve essere tale che la sua radice quadrata sia un intero"); printf(" e la [dim_matrix] deve a sua volta essere un multiplo di tale radice.\n"); MPI_Finalize(); return -1; // Controllo che il numero di processori richiesti rispetti le specifiche, il numero di processori deve essere // tale che la sua radice quadrata sia un intero if(((int)(sqrt(nprocs))-sqrt(nprocs))!=0){ if(!id){ printf("errore! Il numero di processori p deve essere tale che la sua radice quadrata sia un intero!\n"); printf("il programma terminerà.\n"); MPI_Finalize(); return -1; // Controllo che la dimensione assegnata alla matrice rispetti le specifiche, la cardinalità della matrice // deve essere un multiplo della radice del numero di processori if(dimmatrix%(int)(sqrt(nprocs))>0){ if(!id){ printf("errore! La cardinalità della matrice deve essere un multiplo della radice del numero di processori!\n"); printf("il programma terminerà.\n"); MPI_Finalize(); return -1; // Controllo sull'eventuale richiesta di esecuzione su un numero eccessivo di processori 44

if(nprocs>64){ if(!id){ printf("errore! Il sistema dispone di 64 core. Non è possibile richiederne un numero superiore.\n"); printf("il programma terminerà.\n"); MPI_Finalize(); return -1; // Fine controlli sugli input // Come da specifiche P0 legge dimmatrix, calcola nlocal if(!id) nlocal=dimmatrix/sqrt(nprocs); // E lo manda in broadcast a tutti i processori MPI_Bcast(&nlocal,1,MPI_INT,ROOT,MPI_COMM_WORLD); // Il processore ROOT va ad allocare ed istanziare la Matrice, il vettore x ed il vettore delle soluzioni finali y if(!id){ y=(int*)calloc(dimmatrix,sizeof(int)); x=(int*)calloc(dimmatrix,sizeof(int)); M=(int*)calloc(dimmatrix*dimmatrix,sizeof(int)); printf("numero di processori: %d.\n",nprocs); printf("dimensione matrice: %d.\n",dimmatrix); for(j=0;j<dimmatrix;j++){ x[j]=j; // definito come sequenza dei primi dimmatrix-1 numer for(i=0;i<dimmatrix;i++) M[i+j*dimmatrix]=1; // utilizzeremo una matrice costituita da tutti 1 // Tutti i processori allocano la matrice parziale che l'id-esimo processore riceverà in base // alla III strategia, il vettore delle x assegnategli ed il vettore in cui inserire i risultati parziali int* Mloc=(int*)calloc(nlocal*nlocal,sizeof(int)); int* xloc=(int*)calloc(nlocal,sizeof(int)); int* yloc=(int*)calloc(nlocal,sizeof(int)); // Fase di definizione delle variabili necessarie per la creazione della topologia dim[0]=dim[1]=sqrt(nprocs); // numero di righe e colonne della griglia period[0]=period[1]=0; // imponiamo l'assenza di periodicità // Andiamo a definire la griglia bidimensionale MPI_Cart_create(MPI_COMM_WORLD,2,dim,period,0,&comm_grid); // Ad ogni processore sarà assegnato un nuovo id nella topologia creata MPI_Comm_rank(comm_grid,&id_griglia); // Ogni processore sarà identificato nella griglia con delle coordinate cartesiane MPI_Cart_coords(comm_grid,id_griglia,2,coord_griglia); // Andiamo a definire delle sottogriglie formate dalle varie colonne // della griglia precedentemente definita MPI_Cart_sub(comm_grid,free_cord,&comm_col); 45

// Come richiesto nelle specifiche, i processori della prima colonna della griglia riceveranno dal processore 0 // un determinato numero di righe della matrice (pari a nlocal), che andranno poi a distribuire // tra i vari processori della propria riga if(coord_griglia[1]==0){ // Qui accedono solo i processori della prima colonna M1=(int*)calloc(nlocal*dimmatrix,sizeof(int)); // Il processore 0 distribuisce la matrice ed il vettore x ai processori della prima colonna MPI_Scatter(M,nlocal*dimmatrix,MPI_INT,M1,nlocal*dimmatrix,MPI_INT,ROOT,comm_col ); MPI_Scatter(x,nlocal,MPI_INT,xloc,nlocal,MPI_INT,ROOT,comm_col); // Tale Scatter è relativa alla strategia prevista dalle specifiche M1T=(int*)calloc(nlocal*dimmatrix,sizeof(int)); for(j=0;j<dimmatrix;j++) for(i=0;i<nlocal;i++) M1T[i+j*(int)nlocal]=M1[j+i*(int)nlocal]; // Calcolo la traposta della matrice ricevuta in modo da semplificare // Andiamo a definire delle sottogriglie formate dalle varie righe // della griglia precedentemente definita free_cord[0]=0; free_cord[1]=1; MPI_Cart_sub(comm_grid,free_cord,&comm_row); // E procediamo con la distribuzione della matrice lungo le righe MPI_Scatter(M1T,nlocal*nlocal,MPI_INT,Mloc,nlocal*nlocal,MPI_INT,0,comm_row); // STRATEGIA PREVISTA DALLE SPECIFICHE // Distribuzione lungo i processori della diagonale del rispettivo sottovettore di x if(coord_griglia[1]==0 && coord_griglia[0]!=0){ MPI_Send(xloc,nlocal,MPI_INT,coord_griglia[0],coord_griglia[0],comm_row); if(coord_griglia[0]==coord_griglia[1] && coord_griglia[0]!=0) MPI_Recv(xloc,nlocal,MPI_INT,0,coord_griglia[0],comm_row,&stato); /* COMM DIAGONALE // Prima strategia alternativa per la distribuzione del vettore delle x lungo i processori della diagonale; // Essa consiste nel creare un nuovo communicator contenente unicamente i processori della diagonale, così da // facilitare ed ottimizzare il processo di comunicazione e scambio dati MPI_Comm_group(MPI_COMM_WORLD,&grp_aus); // Definiamo un gruppo di processori int* ranks=(int*)calloc(sqrt(nprocs),sizeof(int)); // in cui aggiungo i vari for(i=0;i<sqrt(nprocs);i++) // processori appartenenti ranks[i]=i*sqrt(nprocs)+i; MPI_Group_incl(grp_aus,sqrt(nprocs),ranks,&grp_diag); // della griglia bidimensionale // all 46

MPI_Comm_create(MPI_COMM_WORLD,grp_diag,&comm_diag); MPI_Barrier(MPI_COMM_WORLD); // Si procede infine con la distribuzione del vettore if(coord_griglia[0]==coord_griglia[1]) MPI_Scatter(x,nlocal,MPI_INT,xloc,nlocal,MPI_INT,ROOT,comm_diag); */ /* STRATEGIA SEND ASINCRONA // Seconda strategia alternativa per la distribuzione del vettore delle x lungo i processori della diagonale; // Essa prevede una Send asincrona da parte del processore 0 a tutti i processori della diagonale if(!id){ int* xaus=(int*)calloc(nlocal,sizeof(int)); for(i=0;i<sqrt(nprocs);i++){ for(j=0;j<nlocal;j++) xaus[j]=x[i*nlocal+j]; MPI_Isend(xaus,nlocal,MPI_INT,i*(sqrt(nprocs))+i,i,comm_grid,&rqst); // Ogni processore della diagonale andrà ad effettuare quindi una receive dal processore 0, // acquisendo così la propria porzione di x if(coord_griglia[0]==coord_griglia[1]) MPI_Recv(xloc,nlocal,MPI_INT,0,coord_griglia[0],comm_grid,&stato); */ // Una volta che i processori della diagonale hanno la propria porzione del vettore x, // lo inviano in broadcast lungo la propria colonna MPI_Bcast(xloc,nlocal,MPI_INT,coord_griglia[1],comm_col); // Il processore 0 di ogni riga dovrà raccogliere i risultati locali calcolati dai // processori della propria riga, allocherà quindi un vettore ausiliario if(coord_griglia[1]==0) ypar=(int*)calloc(nlocal,sizeof(int)); MPI_Barrier(MPI_COMM_WORLD); // Inizio valutazione dei tempi if(!id) tin=mpi_wtime(); // Ogni processore andrà a calcolarsi la sua porzione di soluzione for(j=0;j<nlocal;j++) for(i=0;i<nlocal;i++) yloc[j]=yloc[j]+mloc[j+i*(int)nlocal]*xloc[i]; // Si procederà con l'operazione di raccolta e somma per ogni riga, sempre a carico del processore 0 // della riga stessa MPI_Reduce(yloc,ypar,nlocal,MPI_INT,MPI_SUM,ROOT,comm_row); // Il processore 0 della griglia si occuperà invece di raccogliere tali vettore per riunirli in un // unico vettore delle soluzioni y if(coord_griglia[1]==0) MPI_Gather(ypar,nlocal,MPI_INT,y,nlocal,MPI_INT,ROOT,comm_col); 47

MPI_Barrier(MPI_COMM_WORLD); if(!id){ tfin=mpi_wtime(); // Fine valutazione dei tempi tempotot=1.e6*(tfin-tin); // il tempo impiegato viene calcolato come differenza degli istanti tfin e tin printf("tempo totale impiegato: %f.\n",tempotot); printf("algoritmo PARALLELO\n"); printf("calcolo su %d processori.\n",nprocs); for(i=0;i<dimmatrix;i++) printf("y[%d] = %d\n",i,y[i]); //Deallocazione delle variabili dinamiche istanziate all'interno del programma. free(m); free(x); free(y); free(ypar); free(yloc); free(xloc); free(mloc); free(m1); free(m1t); MPI_Finalize(); return 0; 48