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 VC è 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è
xV : 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 (i1) 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