Algoritmi paralleli di base su P-RAM Approfondimento alle lezioni del corso di ALGORITMI E STRUTTURE DATI a.a. 2000/2001 A cura di Costantini Massimo Pilotto Concetta 1 INTRODUZIONE In questo capitolo vengono introdotte due tecniche algoritmiche parallele che permettono di sviluppare soluzioni efficienti per alcuni problemi di base. La tesina consta di due sezioni, nella prima si descrive la tecnica della prima metà e nella seconda la tecnica del salto del puntatore. Ogni tecnica è suddivisa in tre fasi: progettazione, analisi ed applicazioni. In generale, una tecnica algoritmica è definita come una sequenza di passi da applicare all’input che, combinata a osservazioni aggiuntive diverse da caso a caso, permette di risolvere un’ampia “classe di problemi”. Per prima cosa è necessario fissare le ipotesi che individuano l’ambiente su cui una tecnica è applicata. Nella trattazione di queste due ci si pone su una P-RAM EREW, modello di macchina a memoria condivisa in cui non è permessa concorrenza né in lettura né in scrittura. Da notare che in alcune applicazioni delle due tecniche del capitolo si è preferito modificare il modello sottostante permettendo lettura concorrente.(Va da se che è possibile porsi sul modello più debole pagando però un tempo logaritmico aggiuntivo) Riguardo ai domini di applicazione si ha che la prima opera bene su problemi che hanno come input vettori e come output l’applicazione di un’operazione binaria associativa (op), o in generale di una funzione, ai suoi elementi; cioè, detto A il vettore di dimensione n, si vuole in output R = a1 op a2 op … op an Da osservare che in tale tipologia di problemi si cerca un’informazione che è globale rispetto al vettore, in quanto concorrono tutti gli elementi per ottenerla. Con la seconda si risolvono problemi che hanno come input liste e che restituiscono in output un vettore R di dimensione pari alla cardinalità della lista. R è tale che per ogni indice i, R[i] contiene il risultato dell’applicazione di una prefissata operazione agli elementi di una sottolista della lista in input selezionati in base alla politica adottata da tale tecnica. R1 R2 R3 R4 .. .. .. .. .. .. Rn-1 Rn Dove Ri = op (Li) = (l1 op l2 op … li … op lk-1op lk). In questo caso per ogni i si combina l’informazione di un sottoinsieme arbitrario di elementi della lista, ottenendo un vettore di informazioni, in un certo senso, locali. Per concludere, a nostro avviso questo capitolo non deve essere sottovalutato per la semplicità degli algoritmi proposti. Infatti, tali algoritmi applicati ad input “ad hoc” saranno chiamati a risolvere singoli passi di algoritmi molto più complicati. Per esempio, l’algoritmo 2 relativo al problema delle somme prefisse permette di assegnare un ID-numerico crescente (etichetta) agli elementi di un insieme. 3 TECNICA DELLA PRIMA METÀ Permette di trovare soluzioni efficienti per problemi molto semplici che operano sui vettori. Come già sottolineato nell’introduzione, i problemi appartenenti al dominio di applicazione di tale tecnica sono facilmente schematizzabili: hanno in input una sequenza di elementi (a1a2a3…an-1an), e restituiscono in output un singolo valore (r) ottenuto mediante l’applicazione di una qualche funzione (ricavabile dal problema) ai suoi elementi (r = f(a1a2a3…an-1an)). La sequenza è mantenuta in un vettore di dimensione n, con n potenza di due. Da notare che si parla di sequenza e non di insieme, permettendo, quindi, ripetizioni di elementi. In questo modo è possibile studiare la tecnica mantenendosi il più generale possibile. Inoltre, il tipo degli elementi, cioè l’insieme da cui sono selezionati, è arbitrario. Detto A questo insieme si ha che la f deve essere ben definita su esso; si considerano, infatti, f binarie associative ( f : A A A ), in quanto il risultato della loro applicazione alla sequenza deve essere indipendente dall’ordine con cui è analizzato l’input (la tecnica ne selezionerà uno in particolare). Da osservare che la notazione f(a1a2a3…an-1an) sta per l’applicazione di f agli elementi della sequenza, per esempio, f(f(…f(f(f(a1, a2), a3) …), an-1), an). Inoltre, per semplicità, nel seguito si tratterà A come un insieme numerico, su cui sono definite, tra l’altro, le operazioni di addizione e di confronto. La tecnica consiste nell’eseguire in sequenza un numero logaritmico di passi paralleli, ciascuno dei quali determina mediante il suo indice i processori da attivare e le locazioni di memoria a cui accedere. Si considera un numero di processori N dell’ordine di n (N = O(n)). Da notare che dietro la notazione asintotica c’è una corrispondenza biunivoca tra gli indici degli elementi del vettore e gli indici dei processori descritta tramite funzione di identità, ottenendo, quindi, che i processori sono numerati da 1 a n ed etichettati con P 1, P2, …, Pn. Detta k una generica iterazione (k=1..log2n), si ha che i primi nk processori (con nk = n/2k) prelevano dal vettore una coppia di valori, applicano ad essi la funzione f e scrivono i risultati nelle locazioni del vettore corrispondenti ai loro indici. I valori su cui ogni processore Pi opera sono i contenuti delle locazioni indicizzate con “i” rispettivamente nella prima e nella seconda metà del sottovettore Vk-1[1.. nk-1] di V. Da notare che n0 = n e che V0 = V. Dati i seguenti valori per V 1 2 5 6 3 3 4 5 6 7 8 9 7 11 8 2 1 10 10 11 4 9 12 14 13 2 14 6 15 0 16 3 4 al primo passo (k=1, nk=8) si ha che 1 2 3 4 5 5 6 3 7 11 P1 P2 6 7 8 2 8 9 10 11 12 13 14 15 16 1 10 4 9 14 2 6 0 3 P3 P4 P5 P6 P7 P8 Ottenendo 1 2 3 4 5 6 7 8 9 16 f(5, 10) f(6, 4) f(3, 9) f(7, 14) f(11, 2) f(8, 6) f(2, 0) f(1, 3) 10 ... ... 3 passando al secondo passo (k=2, nk=4) 1 2 3 4 5 6 7 8 9 f(5, 10) f(6, 4) f(3, 9) f(7, 14) f(11, 2) f(8, 6) f(2, 0) f(1, 3) 10 P1 P2 P3 16 ... ... 3 P4 si ottiene 1 2 3 4 5 f(f(5, 10), f(11, 2)) f(f(6, 4), f(8, 6)) f(f(3, 9), f(2, 0)) f(f(7, 14), f(1, 3)) f(11, 2) 16 ... 3 Quindi, ad ogni passo dimezza la dimensione del vettore portatore di informazioni. Dopo aver effettuato log2n passi, si ottiene che nel primo elemento è caricato il risultato di f(f(f(f(a1, a2), a3)… ,an-1), an) ovvero di f(V) anche se calcolato in un ordine diverso. È possibile formalizzare tale tecnica tramite un algoritmo a patto di fissare il modello di macchina sottostante (P-RAM EREW) e di selezionare i processori da usare (N = O(n)). Il passo parallelo eseguito da ciascun processore consiste, proprio, nel combinare le due informazioni: Passo k (k=1..log2n) FOR I = 1 TO n/2k PARDO PI: V[I] f(V[I], V[I + n/2k]) 5 Di seguito è riportata una possibile implementazione algoritmica per tale tecnica: ALGORITMO BASE FOR FOR J = 1 TO log2n DO I = 1 TO n/2J PARDO PI: V[I] f(V[I], V[I + n/2J]) Per quanto riguarda la stima del tempo di esecuzione è necessario porre delle limitazioni al tempo di computazione della f. Se la f è calcolabile in tempo costante (f = O(1)), allora l’algoritmo ha tempo di esecuzione O(log2 n); in caso contrario, detto O(w(n)) il tempo necessario alla macchina per determinare il risultato della f, si ha che il tempo di esecuzione dell’algoritmo è O(w(n) log2 n). Per quanto riguarda il costo una stima frettolosa, ottenuta moltiplicando il numero dei processori per il tempo di esecuzione, permette di ottenere come limite superiore O(n log2n). In realtà è possibile trovarne uno più stretto richiamando il significato del costo e osservando che ad ogni passo sequenziale il numero di istruzioni eseguite (ciascuna di tempo costante) coincide con il numero di processori attivi. Quindi è possibile scrivere la seguente sommatoria per stimarne il costo Costo = O( log2 n n/ 2 i ) = O(n) i 1 Tale sommatoria può essere riportata in termini di una geometrica (a = ½), la cui formula risolutiva per a<1 è a i 0 i = 1 1 a . Da notare che non è possibile trovare un limite superiore inferiore a questo in quanto il miglior algoritmo sequenziale impiega O(n) per estrarre da V un valore al cui calcolo concorrono tutti gli elementi. Si ottiene quindi che tale tecnica è efficiente (o ottimale) rispetto alla mole di lavoro eseguita. Per dimostrare, invece, che sono sufficienti lettura e scrittura esclusive, è necessario analizzare il comportamento dei processori rispetto all’istruzione V[I] f(V[I], V[I + n/2k]). Quest’ultima può essere suddivisa in più istruzioni elementari, reperimento delle informazioni in memoria globale, loro scrittura in memoria locale, applicazione della f ai dati e, infine, scrittura del risultato in memoria condivisa. Naturalmente, si devono considerare solo quelle istruzioni che accedono alla memoria globale, che sono la prima e la quarta. In entrambi i casi, non c’è bisogno di concorrenza. Infatti, durante il reperimento delle informazioni, tutti i processori si sincronizzano leggendo prima l’elemento ad essi corrispondente contenuto nella prima metà del vettore del passo precedente, e, poi quello contenuto nella seconda metà, senza sovrapposizioni. Durante la scrittura in memoria del risultato dell’applicazione di f, ogni processore scrive nella 6 locazione corrispondente al proprio indice, naturalmente senza sovrapposizioni (la corrispondenza indice processore - indice vettore è biunivoca). Da osservare che tale tecnica lavora sul vettore modificandone il contenuto. Un modo per arginare la perdita di informazioni consiste nel premettere all’algoritmo alcune linee di codice che implementano il metodo di “clone” del vettore per poi applicare l’algoritmo a quest’ultimo. L’algoritmo della copia è molto semplice, detto C il vettore di copia, si ha: FOR I = 1 TO n PARDO PI: C[I] V[I] L’aggiunta di tale passo non comporta un incremento in termini di tempo o di costo alla tecnica; infatti, è facile osservare che la procedura che realizza la copia impiega O(1) per caricare i dati nel nuovo vettore con un numero totale di passi elementari (semplici “move”) dell’ordine di O(n). Inoltre, è possibile rilassare le ipotesi fissate all’inizio della descrizione della tecnica (la dimensione del vettore di input è potenza di due, il numero dei processori assegnati è n). Si è deciso di affrontare i due problemi separatamente, in quanto indipendenti tra loro. Sarà possibile poi combinare le loro soluzioni per realizzare un algoritmo che permetta di applicare tale tecnica ad un vettore di dimensione arbitraria su una macchina con un numero qualsiasi di processori. Si supponga di avere in input una sequenza di elementi di lunghezza qualsiasi (n), e di avere a disposizione O(n) processori. In queste ipotesi si può, per esempio, premettere all’algoritmo già sviluppato un passo che trasformi, senza modificare le informazioni presenti, il vettore di input V in uno nuovo V’ di dimensione potenza di due, in modo da applicare l’algoritmo base a quest’ultimo. Da notare che la dimensione di V’ è dell’ordine di n, come sarà spiegato successivamente. L’idea è quella di prolungare V fino a un valore k, definito come min {d: d = 2s n d, d, s N}. Nonostante l’espressione a prima vista complessa, il calcolo di k è immediato se si osserva che il suo esponente coincide con log2n. Combinando questa osservazione con la catena di disuguaglianze log2n log2n log2n + 1, si ha che n k 2n e che, quindi, che k = O(n). Dal punto di vista logico, V’ è ottenuto concatenando V a un vettore C di dimensione k-n, i cui valori sono tutti uguali e sono tali che VC è equivalente a V, cioè f(V’) = f(V). Per esempio, se f è l’operazione di somma, C contiene tutti 0, se f è l’operazione di confronto e si cerca l’elemento massimo nel vettore, tutti gli elementi di C sono -. Se V è 1 2 3 4 5 6 7 8 9 10 11 5 6 3 7 11 8 2 1 10 4 9 7 V’ è così fatto 1 2 3 4 5 6 5 6 3 7 11 8 7 2 8 9 10 11 12 13 14 15 16 1 10 4 9 x x x x x V C dove n = 11 e k = 16. Si possono distinguere tre passi da premettere all’algoritmo base: calcolo di t, allocazione della memoria per V’, caricamento dei dati per i suoi elementi. Da osservare che V’ è allocato dinamicamente, in quanto la sua dimensione è calcolata a tempo di esecuzione, e che durante l’esecuzione dell’algoritmo si accede alla parte di V’ corrispondente a C solo al primo passo parallelo. Combinando le due osservazioni, ci si rende conto che si può fare a meno di allocare memoria per il nuovo vettore. Infatti, si può costruire un algoritmo simile a quello base da applicare a V i cui “cicli for” hanno gli indici calcolati rispetto alle dimensioni di V’. Ad ogni passo, per ogni processore è possibile controllare in tempo costante se il secondo dei due indici correnti (i, j) si riferisce a V (j n) o a C. Nel primo caso per i due indici c’è l’accesso alle locazioni corrispondenti (f(V[i], V[j]), come per l’algoritmo base; nel secondo caso per j non c’è accesso in memoria, ma ad esso corrisponde un valore costante (f(V[i], x)). Il codice di tale algoritmo è il seguente: ALGORITMO 1 P1: T log2n K 2T FOR FOR J = 1 TO T DO I = 1 TO K/2J PARDO J PI: IF (I + K/2 ) n THEN V[I] f(V[I], V[I + K/2J]) ELSE V[I] f(V[I], x) L’analisi di questo algoritmo è analoga a quella riportata per l’algoritmo base della tecnica, macchina sottostante EREW, tempo di esecuzione O(log2n), costo O(n). Si supponga, invece, di avere in input un vettore di dimensione n potenza di due, ma di avere a disposizione un numero N di processori molto più piccolo di n (N<<n), anch’esso potenza di due. Vengono proposte due possibili soluzioni. La prima sfrutta l’idea usata nella dimostrazione del teorema di Brent (cfr…). Nella seconda, dettagliata di seguito, si partiziona il vettore V in N sottovettori V1, V2, …, VN, tutti di dimensione n/N e si assegna ciascuno di essi a un processore. Naturalmente tale corrispondenza deve essere biunivoca, per semplicità è implementata tramite funzione di identità, in modo tale che Vi risulti assegnato a Pi. In generale Vi è il sottovettore di V a partire dall’indice (n/N(i-1)+1) fino 8 all’indice (n/N i), da notare che non ci sono sovrapposizioni di indici tra sottovettori diversi. 1 2 3 4 5 6 5 6 3 7 11 8 7 2 8 9 10 11 12 13 14 15 16 1 10 4 9 3 4 22 1 12 ... P1 P2 ... PN Ogni processore calcola in modo sequenziale la f sui suoi dati scrivendo il risultato in V nella locazione di memoria corrispondente al proprio indice. Si applica, quindi, l’algoritmo ai primi N elementi di V, in quanto solo queste locazioni contengono informazioni significative. L’algoritmo è il seguente ALGORITMO 2 FOR I=1 TO N PARDO PI: V[I] sequentialF(V[n/N (i-1)+1 .. (n/N i)]) FOR FOR J = 1 TO log2N DO I = 1 TO N/2J PARDO PI: V[I] f(V[I], V[I + N/2J]) Dove sequentialF() è l’applicazione sequenziale di f. Se si suppone che la singola applicazione di f è costante allora il tempo d’esecuzione di sequentialF() è lineare rispetto all’input (in questo caso O(n/N)). Si ottiene, quindi, che il tempo di esecuzione dell’intero algoritmo e’ O (n/N + log2N), il costo è O(n + N) e la macchina è EREW. Si possono rilassare i vincoli imposti sui valori assunti da n e N (n e N arbitrari con N << n), in modo da studiare il caso precedente nella sua generalità. Si assegnano ai primi N-1 processori n/N elementi (O(n/N)) e all’ultimo i rimanenti (O(n/N + N)). I < N VI V[(n/N (I-1)+1).. (n/N I)] I = N VN V[(n/N (N-1)+1).. n] L’algoritmo implementato è ALGORITMO 3 FOR I=1 TO N PARDO PI: IF (I < N) THEN V[I] sequentialF(V[n/N (i-1)+1 .. (n/N i)]) ELSE V[N] sequentialF(V[(n/N (N-1)+1).. n]) applica l’algoritmo 1 a V[1..N] 9 Si usa “l’algoritmo 1”, perché permette di applicare la tecnica a un vettore di dimensione qualsiasi. Quindi, il tempo di esecuzione dell’algoritmo è O(n/N + N) + O(log2N), il costo è O(n + N) e la macchina è EREW. Nel seguito si mostra l’applicazione di tale tecnica a due problemi: il calcolo della somma di n numeri e la ricerca del massimo tra n elementi. Un’altra applicazione della tecnica è presente nella tesina “Quintarelli-Gesomundo” (problema relativo alla simulazione della scrittura concorrente tramite quella esclusiva). Data la trattazione generale della tecnica si è deciso di descrivere ciascun problema solo per sommi capi. Inoltre, dopo le due applicazioni è stato inserito un altro problema (quello della ricerca) per mostrare come sfruttare tali algoritmi nella risoluzione di problemi più complessi. 10 SOMMA Sia a1a2…an una sequenza di numeri selezionati da A e memorizzati in un vettore V. Si vuole ottenere in un registro di memoria R la loro somma R= n a i 1 i = a1+ a2 +… + an. Il problema è risolto sfruttando la tecnica della prima metà, sostituendo ad ogni occorrenza della f un’occorrenza dell’operazione di somma. ALGORITMO BASE (n è potenza di due, si hanno a disposizione n processori) FOR I=1 TO log2n DO FOR j=1 TO n/2I PARDO PJ: V[J] V[J] + v[J + n/2I] P1 : R V[1] La somma di due elementi è calcolata in O(1), ottenendo che il tempo di esecuzione dell’algoritmo è O( log2N) e il costo O(n) . CALCOLO DEL MASSIMO Si supponga, ancora, di selezionare da A n numeri interi (a1a2…an) e di memorizzarli in un vettore V. Si vuole ottenere in un registro di memoria R il massimo del vettore, cioè xV : y V, y x y x. Si può risolvere il problema sfruttando la tecnica della prima metà. In questa applicazione della tecnica, la f è implementata con la funzione max che restituisce il massimo tra i due valori in input (V[J], V[J + n/2I]), tale funzione ha tempo computazionale costante. Per semplicità la funzione è stata esplicitata con un piccolo accorgimento che permette di scrivere nel vettore solo quando è strettamente necessario. Infatti, bisogna scrivere in V[J] solo se l’informazione restituita da max non è V[J]. 11 FOR I=1 TO log2n DO FOR j=1 TO n/2I PARDO PJ: IF(V[J] < V[J+n/2I]) THEN V[J] V[J+n/2I] P1 : R V[1] L’algoritmo ha tempo di esecuzione O( log2N) e costo O(n). È possibile anche trasformare l’output in maniera opportuna, ottenendo problemi analoghi a quello del calcolo del massimo. Per esempio, si potrebbe voler restituire l’indice dell’elemento massimo presente nel vettore. Anche questa formulazione del problema può essere risolta applicando la tecnica della prima metà. Si usano due vettori il primo per gli indici (B inizializzato a I per ogni I), il secondo per i valori (V). La tecnica è applicata al vettore degli indici, ma ha bisogno di accedere a quello dei valori per selezionare ad ogni passo l’indice corretto. L’algoritmo base risulta così trasformato ALGORITMO BASE FOR I=1 TO n PARDO PI: B[I] I FOR I=1 TO log2n DO FOR j=1 TO n/2I PARDO PJ: IF(V[B[J]] < V[B[J+n/2I]]) THEN B[J] B[J+n/2I] P1 : R B[1] Il tempo d’esecuzione, il costo e il modello di macchina sono gli stessi dell’algoritmo base della tecnica. Tutti gli algoritmi presentati fino a questo momento sono stati pensati per essere eseguiti su una P-RAM a lettura e scrittura esclusive. Si lavora su questo modello, perché è il più generale; infatti, un qualsiasi algoritmo scritto per questa macchina funziona anche su tutte le altre P-RAM senza dover apportare ulteriori modifiche. D’altra parte è noto che si può simulare la lettura (o la scrittura) concorrente tramite quella esclusiva in tempo logaritmico. Mettendo insieme le due informazioni, nasce l’idea di poter delineare un algoritmo di tempo costante su una CRCW P-RAM interpretando il tempo logaritmico dell’altro come conseguenza dei vincoli causati dalla lettura e dalla scrittura esclusive. Da osservare che è necessario fissare con precisione il modello di scrittura concorrente. Per l’algoritmo costruito, come sarà spiegato più avanti, basta porsi nel modello standard, cioè se più processori scrivono in contemporanea nella stessa locazione di memoria devono necessariamente scrivere la stessa informazione. 12 L’idea che viene implementata è quella di effettuare in parallelo i confronti tra tutte le possibili coppie di valori, riconoscendo, quindi, in tempo costante tutte le occorrenze dell’elemento massimo. In prima analisi, si può supporre di avere a disposizione un numero di processori almeno pari al quadrato della dimensione del vettore (si ha bisogno di un processore per ogni possibile coppia di elementi, N = O(n2)), successivamente questa ipotesi viene lasciata cadere in modo da studiare il problema nella sua generalità. Nel primo caso l’algoritmo è il seguente ALGORITMO FOR I=1 TO n PARDO FOR J=1 TO n PARDO PI,J: IF (V[I] V[J]) THEN M[I,J] 1 ELSE M[I,J] 0 FOR I=1 TO n PARDO PI: X[I] 1 FOR I=1 TO n PARDO FOR J=1 TO n PARDO PI,J: IF (M[I,J] = 0) THEN FOR I=1 TO n PARDO PI: IF (X[I] = 1) THEN R V[I] X[I] 0 Prima di tutto, si ha bisogno di stanziare memoria per una matrice M di dimensione n x n, destinata a contenere i risultati dei confronti. Ciascun processore Pi,j (i,j da 1 a n) scrive in mi,j 1 se A[i] A[j], 0 altrimenti. Tutti i confronti sono realizzati in parallelo sfruttando la lettura concorrente (da notare che tutti i processori associati a una stessa riga (o colonna) leggono contemporaneamente la stessa informazione). Ogni riga i può essere interpretata come il confronto dell’i-esimo elemento con tutti gli altri, derivando, quindi, che le righe corrispondenti alle occorrenze del massimo hanno tutti 1. Per ottenere in output il massimo, è necessario realizzare questo controllo, per esempio tramite un AND tra gli elementi di una stessa riga. Un modo per implementarlo in tempo costante sfruttando la scrittura concorrente si basa sull’uso di un vettore ausiliario X di dimensione n, i cui elementi sono inizializzati a 1. Per ogni riga si abilitano in scrittura concorrente tutti i processori della riga la cui corrispondente informazione nella matrice è uno zero, ottenendo che per ogni i da 1 a n il valore A[X[i]] è un’occorrenza del massimo se e solo se non ci sono zeri nell’i-esima riga se e solo se X[i] 0 . 13 Infine, tutti i processori, il cui valore di X è 1, sono attivati scrivendo in parallelo (tutti la stessa informazione) il massimo in R. Da osservare che non è possibile che nessun processore si attivi, perché il massimo esiste sempre. Il tempo di esecuzione è O(1) e il relativo costo è O(n2), perché si ha bisogno di un numero di processori dell’ordine O(n2). È facile osservare che l’algoritmo proposto è inefficiente ( fattore di inefficienza = O(n)/O(n2)). Se n2>N allora si hanno due possibili scelte: si comprime n fino ad avere dimensione N1/2 oppure assegnare più confronti a uno stesso processore. In entrambi i casi, il tempo di esecuzione non è costante, perché è necessario sequenzializzare delle operazioni (la ricerca del massimo nel primo caso, il numero di confronti nel secondo) ottenendo O(n/N1/2) e O(n2/N) con l’uso di N processori. RICERCA Sia a1a2..an una sequenza di elementi selezionati da e memorizzati in un vettore V di dimensione n; sia x un generico elemento di A passato in input. La formulazione più semplice del problema di ricerca consiste nel voler sapere se x è presente nel vettore, restituendo “uno” in caso affermativo, “zero” altrimenti. Si suppone di avere a disposizione un numero di processori N = O(n) e di permettere ripetizioni di elementi nel vettore. L’idea implementata usa applicazioni della tecnica della prima metà, ed è un esempio di programmazione astratta modulare. Si usa un vettore di appoggio X[] di dimensione n, i cui elementi sono inizializzati a zero. Ogni processore Pi controlla se l’elemento ad esso assegnato (V[i]) è uguale a x, in caso affermativo scrive un “uno” in X[i]. Da notare che c’è l’accesso concorrente a x da parte di tutti i processori. Per mantenere il modello EREW è necessario trasmettere precedentemente x a tutti, ciò è fatto attraverso l’applicazione dell’algoritmo di simulazione della lettura concorrente tramite quella esclusiva usando il vettore B per propagare l’informazione. L’elemento x è presente in V se e solo se X contiene almeno un “uno”, tale controllo può essere effettuato o sommando gli elementi del vettore (sum = 0 sse X contiene solo zeri) oppure cercando il suo massimo. ALGORITMO TRAMITE SOMMA R 0 FOR I = 1 TO n PARDO PI: X[I] 0 B[] algoritmo di trasmissione (x) 14 FOR I = 1 TO n PARDO PI: IF (V[I] = B[I] ) THEN X[I] 1 sum algoritmo di somma (X) P1: IF (sum 0) THEN R 1 ALGORITMO TRAMITE RICERCA FOR I = 1 TO n PARDO PI: X[I] 0 B[] algoritmo di trasmissione (x) FOR I = 1 TO n PARDO PI: IF (V[I] = B[I] ) THEN X[I] 1 P1: R algoritmo di massimo(X) Il tempo di esecuzione dei due algoritmi è O(log2n) e costo O(n). È possibile anche trasformare l’output in maniera opportuna, ottenendo problemi analoghi a quello della ricerca. Per esempio, si potrebbe voler restituire la posizione di x (o il vettore delle posizioni in caso di occorrenze multiple di x). Si studia direttamente il caso più generale, si suppone di avere a disposizione un numero di processori molto più piccolo della lunghezza dell’input (N<<n) e di permettere ripetizioni di elementi nella sequenza. Ad ogni processore sono assegnati n/N elementi, per semplicità n e N si considerano potenza di due, in modo che a Pi è assegnato il sottovettore V[n / N (i-1) + 1 .. n/ N i] di V. Ogni processore cerca in modo sequenziale le occorrenze di x nel vettore corrispondente, scrivendo le loro posizioni in memoria locale in una lista (li). Terminata la ricerca, i processori in parallelo inseriscono i valori trovati nel vettore dei risultati. Molto interessante è la gestione della scrittura dei risultati. Infatti, non essendo in grado di stabilire a priori il numero di occorrenze di x, bisogna predisporre un vettore R di dimensione n per contenerli. Un primo metodo usa l'algoritmo delle somme prefisse trattato in fondo al capitolo. Si considera un vettore ausiliario X di dimensione N in cui ogni processore PI scrive la cardinalità della propria lista (Pi: X[i] li). Si applica a X l’algoritmo delle somme 15 prefisse. In questo modo ogni processore conosce l’indice a partire dal quale scrivere la propria lista in R. P1 scrive a partire da R[1], Pi (i1) a partire da R[X[i-1] +1]. L’algoritmo di ricerca così implementato ha tempo di esecuzione (n/N + log2N). Inoltre, è possibile stabilire in O(log2N) l’assenza dell’elemento, basta calcolare la somma degli elementi di X. Un secondo metodo consiste nel partizionare anche R tra i processori, assegnando n/N locazioni ciascuno. I processori in parallelo scrivono la propria lista nel sottovettore corrispondente lasciando a 0 gli elementi di R non utilizzati. In questo modo durante la lettura del risultato se si incontra uno 0 si salta al successivo blocco (l’indice iniziale di ogni blocco è calcolato in tempo costante). Il tempo di esecuzione è (n/N). Questa implementazione è più veloce dell’altra. Anche se a differenza dell’altra sono necessari O(N) accessi a R per stabilire l’assenza dell’elemento. Si può scendere a O(log2N) utilizzando un vettore booleano di appoggio simile in funzionamento a quello presente nel problema relativo alla simulazione della scrittura concorrente (cfr…). 16 17 18