Příklady paralelních algoritmů Studijní materiál Michal Černý, katedra ekonometrie, VŠE Praha 0. Úvod 0.1. Řada problémů může být řešena velmi efektivně, máme-li k disposici počítač, který disponuje více než jedním procesorem. Často je možné nějakým způsobem úlohu rozdělit na menší části, každou z těchto částí nechat zpracovat jednomu procesoru a poté z těchto částí poskládat řešení celé úlohy. Mohou-li jednotlivé procesory pracovat nezávisle na sobě, bude celkový čas zpracování úlohy roven maximu z časů, po které počítají jednotlivé procesory, plus čas potřebný na získání výsledku celkové úlohy z výsledků jednotlivých částí. 0.. Příklad. Uvažme počítač, který disponuje n procesory. Nechť jsou dány dva vektory x = (x 1, x,..., x n ) a y = (y 1, y,..., y n ) a naším úkolem je spočítat jejich skalární součin 1 xy T = x 1 y 1 + x y + + x n y n. Všimněme si, že všechny součiny x i y i je možné počítat nezávisle na sobě. Pokud bychom každému z n procesorů přidělili úkol spočítat jeden z těchto součinů, a všechny procesory mohly počítat nezávisle na sobě, bude celkový čas této operace roven max i m i, kde m i je čas potřebný k výpočtu x i y i (1 i n). Kdybychom však počítali stejnou úlohu na počítači s jediným procesorem, byl by potřebný čas m 1 + m + + m n, tedy obecně podstatně větší. Pro jednoduchost nadále předpokládejme, že jedna aritmetická operace (sčítání nebo násobení) trvá jednotkový čas. Pak bychom dostali, že výpočet všech součinů x i y i na paralelním počítači s n procesory trvá čas O(1), zatímco na sekvenčním počítači (tedy počítači s jediným procesorem) bychom potřebovali čas O(n). Předpokládejme pro jednoduchost, že číslo n je mocninou dvojky, a označme a i = x i y i. Abychom dostali číslo xy T, potřebujeme čísla a i posčítat. Na 1 Horní index T značí transposici. 1
paralelním počítači můžeme postupovat takto: použijeme n procesorů, které spočtou čísla b 1 = a 1 + a, b = a 3 + a 4,, b n = a n 1 + a n. Těchto n součtů lze spočítat paralelně, tedy v čase O(1). Dále postupujeme obdobně: budeme paralelně po dvou sčítat čísla b i, opět v čase O(1): c 1 = b 1 + b, c = b 3 + b 4,, c n 4 = b n 1 + b n. A tak bychom pokračovali dále, dokud bychom nezískali výslednou hodnotu xy T. Schematicky můžeme celý výpočet pro n = 8 znázornit následující pyramidou: xy T c 1 c b 1 b b 3 b 4 a 1 a a 3 a 4 a 5 a 6 a 7 a 8 Výpočet každého patra pyramidy trvá čas O(1), protože každý součet může být proveden nezávisle na ostatních součtech na patře. Všimněme si, že dokonce nevyužijeme ani všech n procesorů. Celkový čas na výpočet součtu je tedy roven počtu pater této pyramidy. Protože je v každém patře polovina členů oproti patru bezprostředně pod ním, je v našem příkladě počet pater roven log 8 = 3; obecně je výška pyramidy O(log n). Dostáváme tak, že na výpočet skalárního součinu xy T je třeba čas O(1) + O(log n) = O(log n), zatímco na sekvenčním počítači (s jediným procesorem) bychom potřebovali čas O(n). 0.3. Uvedený příklad ukazuje, že paralelisací problémů můžeme dosáhnout velmi podstatného zrychlení výpočtu (uvědomme si, že funkce log n roste oproti n velmi pomalu). Cena za toto zrychlení je však velká: potřebujeme k tomu velký počet procesorů. Dokonce vidíme, toto zrychlení výpočtu je
podmíněno tím, že s růstem velikosti úlohy potřebujeme rostoucí počet procesorů. 0.4. Všimněme si, že pomocí tohoto algoritmu můžeme provést i násobení matic. Máme-li vynásobit dvě matice A a B velikosti n n, můžeme číslo v matici AB na místě i, j spočítat jako skalární součin i-tého řádku matice A a j-tého sloupce matice B. Pokud bychom měli k disposici n 3 procesorů, můžeme všech n skalárních součinů spočítat paralelně a tak v čase O(log n) paralelně spočítat všechna čísla v matici AB. Upozorněme na jeden detail: složitost výpočtu obvykle měříváme jako funkci velikosti vstupního zadání. Uvědomme si, že v této úloze n není velikostí vstupu; velikost vstupu je řádově m = n. Proto náš algoritmus na násobení matic počítá v čase O( log m) (kde jsme předpokládali, že provedení jedné aritmetické operace trvá jednotkový čas) a použije k tomu m 3/ procesorů. Klasický sekvenční algoritmus pro násobení matic podle definice pracuje v čase O(m 3/ ). 1. Hledání maxima posloupnosti v konstantním čase 1.1. Mějme dánu poslounost čísel a 1, a,..., a n. Nalezení maxima na sekvenčním počítači (tj. na počítači s jediným procesorem) trvá čas Θ(n): budeme čísla a 1, a,..., a n postupně procházet a pamatovat si největší dosud nalezené. Není těžké si též při tomto průchodu posloupností označit pole, kde bylo maximum nalezeno (naprogramujte!). Tato procedura zřejmě trvá čas O(n) (předpokládáme-li, že porovnání dvou čísel lze provést v jednotkovém čase). 1.. Uvědomme si též, že kratší čas než O(n) tato procedura trvat nemůže; kdyby nějaký sekvenční program měl hledat maximum v čase o(n), neměl by čas ani přečíst všechna čísla a i. Je zřejmé, že by nemohl počítat správně: mohli bychom mu někam do nepřečteného úseku umístit obrovské číslo, které by program díky tomu, že tento úsek nečetl, nenašel. 1.3. Už v příkladu 0.. jsme viděli, že pro paralelní programy toto dolní omezení neplatí: díky tomu, že máme k disposici více procesorů, může zpracování úlohy trvat i kratší čas než O(n). 3
1.4. Ukažme nyní paralelní program, který nalezne maximum posloupnosti v konstantním čase: výpočet bude trvat vždy stejně dlouho, bez ohledu na to, jak dlouhá je vstupní posloupnost. 1.5. Předpokládejme, že máme k disposici počítač s n procesory. Uvažme tento výpočetní model: počítač má paměť s přímým přístupem, každá buňka paměti může obsahovat jedno číslo. Čtení i zápis čísla trvá čas O(1). Z paměti může více procesorů číst najednou. Zapisovat do paměti může též více procesorů najednou, ale pouze tehdy, zapisují-li všechny stejnou hodnotu; pokud by se pokusily dva procesory zároveň do některé buňky paměti zapsat různá čísla, dojde k chybě. 1.6. Předpokládejme, že máme v paměti vyhrazeno pole b ij, 1 i n, 1 j n, a procesory jsou označeny P ij. Výstupem bude pole m i, 1 i n takové, že m i = 1, je-li a i maximum, a m i = 0, není-li a i maximum. 1.7. V prvním kroku každý z procesorů P ij čte z paměti čísla a i a a j a provede instrukci if a i a j then zapiš b ij := 1 else zapiš b ij := 0. Tuto akci provede všech n procesorů paralelně; celkem tedy vyplnění pole b ij bude trvat čas O(1). V dalším kroku vezmeme n procesorů, řekněme P 11 až P 1n, a vyplníme s nimi pole m i jedničkami. Jinými slovy, každý z procesorů P 1i provede paralelně instrukci m i := 1. Tato akce také zřejmě trvá čas O(1). 1.8. Dále vezmeme opět všech n procesorů a každý procesor P ij provede instrukci if b ij = 0 then zapiš m i := 0. Uvědomme si, že číslo a k je maximum, právě když má matice b ij v k-tém řádku samé jedničky. V tom případě žádný z procesorů P k1, P k,..., P kn při provádění testu if b kj = 0 then... neuspěje, a proto instrukci zapiš m k := 0 ani jeden z nich neprovede. V buňce m k tak zůstane hodnota 1. V případě, kdyby a k maximum nebylo, bude k-tý řádek matice b ij obsahovat aspoň jednu nulu; řekněme, že je v l-tém sloupci. Proto procesor P kl při provádění testu if b kl = 0 then... uspěje a zapíše do m k hodnotu 0. 4
Všimněme se, že pokud k-tý řádek matice obsahuje více nul, pokouší se více procesorů zapsat do m k hodnotu 0; nikdy však nedojde k chybě, protože se nikdy dva procesory nepokoušejí do stejné paměťové buňky zapsat různá čísla. Opět je zřejmé, že tento krok trvá čas O(1). 1.9. Příklad. Uvažme posloupnost a 1 = 1, a = 3, a 3 =, a 4 = 9, a 5 = 0. Matice b ij a pole m i budou vypadat takto: b ij m i 1 0 0 0 1 0 1 1 1 0 1 0 1 0 1 0 1 0 1 1 1 1 1 1 0 0 0 0 1 0 1.10. Snadno už nyní v jednom kroku zapíšeme hodnotu maximálního prvku na výstup, či řekněme do paměťové buňky M. Použijeme n procesorů, řekněme P 11, P 1,..., P 1n a každý z procesorů P 1i provede instrukci if m i = 1 then zapiš M := a i.. Sčítání.1. V této kapitole ukážeme, že i tak jednoduchou operaci, jakou je sčítání dvou čísel zadaných jako posloupnost bitů (tedy v binárním zápise), je možné paralelisovat a dosáhnout tak velmi příznivého výpočetního času. Budeme k tomu potřebovat lineární počet procesorů; tyto procesory však budou provádět tak jednoduché operace (konjunkci, disjunkci bitů apod.), že je budeme moci nahradit hradly a sestavit logický obvod, který rychlé sčítání realisuje... Nechť jsou dána dvě n-místná čísla a a b jako posloupnost bitů: a = a n 1 n 1 + a n n + + a 1 1 + a 0, b = b n 1 n 1 + b n n + + b 1 1 + b 0 5
a máme určit bity s 0, s 1,..., s n, které tvoří jejich součet: a + b = s = s n n + s n 1 n 1 + + s 1 1 + s 0. (Všimněme si, že obecně může být součet dvou n-místných čísel o jedno místo delší.).3. Přímý algoritmus pro sčítání postupuje zprava doleva : nejprve provede operaci s 0 = a 0 b 0 a jsou-li oba tyto bity 1, zapamatuje si přenos jedničky do vyššího řádu c 0 = 1; jinak bude c 0 = 0. Dále se sčítá s 1 = a 1 b 1 c 0 a jsou-li alespoň dva z těchto sčítanců 1, zapamatuje si přenos jedničky c 1 = 1, jinak bude c 1 = 0. Dále se sčítá s = a b c 1 a jsou-li alespoň dva ze sčítanců 1, zapamatuje se přenos c = 1 atd. Je vidět, že paralelisaci sčítání komplikuje právě přenos jedničky do vyšších řádů. V následujícím textu popíšeme metodu, jak výpočet přenosů c i paralelisovat..4. Zaveďme pomocnou operaci : (g, p) (g, p ) = (g (p & g ), p & p ). Při paralelisaci s výhodou využijete toho, že tato operace je asociativní (viz část.10.):.5. Položme [(g, p) (g, p )] (g, p ) = (g, p) [(g, p ) (g, p )]. g i = a i & b i a p i = a i b i. Čísla g i a p i snadno paralelně spočteme ze zadaných čísel a a b. Zavedeme-li dále (G 0, P 0 ) = (g 0, p 0 ), (G i, P i ) = (g i, p i ) (G i 1, P i 1 ) pro 1 i n 1, platí důležitý vztah (viz část.11.): G i = c i. Operace je sčítání modulo, tedy: 0 + 1 = 1 + 0 = 1, 0 + 0 = 1 + 1 = 0. Operace a b je ekvivalentní s (a b); bývá proto také nazývána jako nonekvivalence. Někdy se místo a b používá symbolu a XOR b (zkratka z exclusive OR, vylučující nebo ). 6
To jinými slovy znamená, že přenosy bitů c 0, c 1, c atd. lze na základě g i a p i (připomeňme, že g i = a i & b i a p i = a i b i je možné spočítat paralelně) určit pomocí vztahů c 0 = první složka (G 0, P 0 ) = (g 0, p 0 ) = g 0, c 1 = první složka (G 1, P 1 ) = (g 1, p 1 ) (g 0, p 0 ), c = první složka (G, P ) = (g, p ) (g 1, p 1 ) (g 0, p 0 ), c 3 = první složka (G 3, P 3 ) = (g 3, p 3 ) (g, p ) (g 1, p 1 ) (g 0, p 0 ) atd..5. Nyní je již zřejmé, že asociativita operace zaručuje, že můžeme počítat mnoho výrazů současně. Například (G 3, P 3 ) je možné určit ve dvou krocích tak, že jeden procesor spočte (γ, λ) = (g 3, p 3 ) (g, p ), druhý procesor paralelně spočte (γ, λ ) = (g 1, p 1 ) (g 0, p 0 ) a ve druhém kroku se spočte (G 3, P 3 ) = (γ, λ) (γ, λ ). Tím se získá přenos bitů c 3 = G 3 (číslo P 3 již nepotřebujeme)..6. Přistupme nyní k popisu rychlého paralelního algoritmu na sčítání. Předpokládejme, že máme v paměti pole K 0, K 1,..., K n 1 a v každé buňce tohoto pole uchováváme dvojici bitů. Uvažme nejprve příklad, kdy sčítáme dvě osmibitová čísla a 7 a 6 a 0 a b 7 b 6 b 0. V prvním kroku spočteme paralelně všechna g i a p i a uložíme je do paměti: K 0 := (g 0, p 0 ), K 1 := (g 1, p 1 ),..., K 7 := (g 7, p 7 ). V druhém kroku paralelně spočteme K 1 := K 0 K 1, K 3 := K K 3, K 5 := K 4 K 5, K 7 := K 6 K 7. Ve třetím kroku paralelně spočteme K 3 := K 1 K 3, K 7 := K 5 K 7 a ve čtvrtém V této fázi platí: K 7 := K 3 K 7. K 0 = (G 0, P 0 ) = (g 0, p 0 ), K 1 = (G 1, P 1 ) = (g 1, p 1 ) (g 0, p 0 ), K 3 = (G 3, P 3 ) = (g 3, p 3 ) (g, p ) (g 1, p 1 ) (g 0, p 0 ), K 7 = (G 7, P 7 ) = (g 7, p 7 ) (g 6, p 6 ) (g 0, p 0 ), 7
a tedy první bit v buňce K 0 je bitový přenos c 0, první bit v buňce K 1 obsahuje bitový přenos c 1, první bit v buňce K 3 je bitový přenos c 3 a první bit v buňce K 7 je bitový přenos c 7. Tuto první fázi algoritmu můžeme zachytit následujícím schematem: symbol ij označuje, že v dané paměťové buňce je uložena informace (g j, p j ) (g j 1, p j 1 ) (g i, p i ), takže například 47 označuje (g 7, p 7 ) (g 6, p 6 ) (g 5, p 5 ) (g 4, p 4 ). Připomeňme, že důležité jsou pro nás informace (G i, P i ), neboť G i je hledaný bitový posun c i. Je-li v některém kroku spočetno (G i, P i ), odpovídá to v našem schematu označení 0i. Šipky označují, na které paměťové buňky se aplikuje operace. Zbývá dopočítat c, c 4, c 5, c 6. Toho docílíme ve druhé fázi výpočtu: v pátém kroku spočteme K 5 := K 3 K 5, čímž získáme c 5, a v šestém kroku paralelně K := K 1 K, K 4 := K 3 K 4, K 6 := K 5 K 6. V tomto okamžiku již buňky K, K 4 a K 6 obsahují požadované hodnoty c, c 4, c 6. Opět zachyťme šestý a sedný krok na obrázku; tučně jsou vyznačena místa, kde se v příslušných paměťových buňkách objeví požadované hodnoty c i. 8
Vodorovná čára odděluje první a druhou fázi programu. Nyní již snadno paralelně spočteme s 0 := a 0 b 0, pro 1 i 7 s i := a i b i c i 1, s 8 := c 7. Síla uvedeného algoritmu je v tom, že počet kroků je velmi malý; v tomto konkrétním příkladě jsme potřebovali jeden krok na paralelní výpočet všech (g i, p i ), log 8 = 3 kroky na první fázi výpočtu, log (8) 1 = kroky na druhou fázi výpočtu a jeden krok na spočtení všech s i..7. Postup z minulého paragrafu je možné zobecnit na libovolně dlouhá čísla, přičemž počet kroků, které potřebujeme na výpočet bitových posunů c i, zůstane logaritmický. Předpokládejme, že počet bitů vstupních čísel je mocninou dvojky; pokud ne, snadno je zleva doplníme nulami. Nejprve paralelně spočteme všechna K i := (g i, p i ) První fáze programu bude pracovat podle následujícího schematu: 9
s := ; while s n do for all i = s 1, s 1, 3s 1,..., n 1 do paralelně K i end do; s := s; end do; := K i s K i; Tím máme spočteny c 0, c 1, c 3, c 7, c 15, c 31, c 63, c 17 atd., tedy všechny c s indexem tvaru (mocnina dvojky) 1. Všimněme si, že počet opakování těla cyklu while je přesně log n, protože krok s se vždy zdvojnásobí, dokud nedosáhne hodnoty n. Druhá fáze pracuje podle následujícího schematu: s := n; while s do for all i = s 1, s 1, 3s 1,..., n s 1 do paralelně K i+ s end do; s := s ; end do; := K i K i+ s ; Po této fázi již každá z paměťových buněk K i obsahuje (G i, P i ) a tudíž první bit v každé buňce je c i. Druhá fáze trvá čas log (n) 1, protože krok s začíná na hodnotě n, a vždy se zmenší na polovinu, dokud nedosáhne hodnoty. Pak že stačí v jediném kroku paralelně spočítat s 0 := a 0 b 0, pro 1 i n 1 s i := a i b i c i 1, s n := c n 1..8. Pro názornost ještě ukažme schema výpočtu pro n = 16. 10
.9. Nyní jsme již jen krůček k sestavení efektivního logického obvodu pro sčítání. V našem algoritmu jsme potřebovali n procesorů; každý z procesorů však provádí vždy jen jedinou operaci, a to. Proto bychom mohli schemata, kterými jsme ilustrovali práci algoritmu, považovat přímo za logické obvody pro výpočet c i. Šipky lze chápat jako dvojice vodičů, které přenášejí informaci typu (g, p); tam, kde se šipky stýkají, je umístěno logické hradlo, které počítá operaci (připomeňme, že (g, p) (g, p ) = (g (p & g ), p & p )). Tím máme sestaven logický obvod, který je velmi malý a počítá bitové posuny c i. Pro ilustraci ukažme tento obvod pro n = 8; na vstupy 1 až 8 přivedeme dvojice (g i, p i ) (Obvody pro výpočet g i a p i jsou triviální, protože g i = a i & b i a p i = a i b i ). 11
Obvod pro sčítání je pak už velmi jednoduchý, stačí napojit výstupní vodiče nesoucí informaci c i na hradla pro :.10. Dodatek. Zbývá ukázat, že jsme nelhali, když jsme používali dvou důležitých vztahů: (1) operace je asociativní, () G i = c i. Ukažme nejprve asociativitu operace, tedy, že platí: [(g, p) (g, p )] (g, p ) = (g, p) [(g, p ) (g, p )]. 1
Využijme toho, že a & (b & c) = (a & b) & c, a (b c) = (a b) c a a (b & c) = (a b) & (a c). [(g, p) (g, p )] (g, p ) = = (g (p & g ), p & p ) (g, p ) = = ((g (p & g ) ((p & p ) & g ), (p & p ) & p ) = = (g ((p & g ) (p & (p & g ))), (p & p ) & p ) = = (g (p & (g (p & g ))), p & (p & p )) = = (g, p) (g (p & g ), p & p ) = = (g, p) [(g, p ) (g, p )]..11. Vztah G i = c i ukážeme indukcí. Pro i = 0 je G 0 = g 0 = a 0 & b 0 = c 0, protože přenos c 0 = 1, právě když obě čísla a 0 i b 0 jsou jedničky. Pro i > 1 je (G i, P i ) = (g i, p i ) (G i 1, P i 1 ) = (g i, p i ) (c i 1, P i 1 ) = = (g i (p i & c i 1 ), p i & P i 1 ) = (c i, p i & P i 1 ); stačí si rozmyslet, že výraz g i (p i & c i 1 ) = (a i & b i ) ((a i b i ) & c i 1 ) je jednička, právě když aspoň dva z výrazů a i, b i, c i 1 jsou jedničky (třeba tak, že se sestaví tabulka pravdivostních hodnot). 3. Maticové operace 3.1. V části 0.4. jsme ukázali paralelní algoritmus, který násobí dvě matice řádu n n v čase O(log n) za použití n 3 procesorů. V praxi má tento algoritmus jednu velkou nevýhodu: vyžaduje, aby se v jednom okamžiku uspokojilo mnoho žádostí o přístup do paměti, protože každý z procesorů čte vstupní matice někde jinde. Tento fakt činí mnoho paralelních algoritmů v praxi těžko použitelnými, protože obvykle není možné zajišťovat velmi rychle mnoho paralelních přístupů do paměti a případně dalších požadavků na komunikaci mezi procesory. Proto je snaha navrhovat paralelní algoritmy tak, aby jednotlivé procesory nepožadovaly mnoho různých přístupů do paměti. Ukažme příklad algoritmu, který bude násobit dvě matice řádu n n v čase O(n). Vstupní matice budou uloženy v jednoduchém typu paměti ve fron- 13
tách, ze kterých budou moci procesory číst postupným odebíráním prvků z jejich začátků. 3.. Uvažme nejprve matice : ( ) ( a11 a A = 1 b11 b, B = 1 a 1 a b 1 b Mějme k disposici čtyři procesory, každý má svoji vnitřní paměť, do které je možné uložit jedno číslo s. Každý procesor má dále dva vstupy x, y a dva výstupy ξ, υ: y x s ξ υ Nechť tento procesor počítá podle velmi jednoduchého programu: ). ξ := x; υ := y; s := s + xy; (1) Takové procesory sestavíme do sítě. Na začátku výpočtu budou mít všechny procesory vnitřní paměť s = 0; síť bude pracovat v taktech tak, že v každém taktu odebere z každého vstupního zásobníku jeden údaj a každý procesor provede svoji instrukci (1). Pokud některému procesoru nic nepřichází na vstup, nic nedělá. Když u některého procesoru není zakreslen výstup, není důležitý a je možné jej zapomenout. 0 a a 1 a 1 a 11 0 0 b 1 b 11 s 11 s 1 b b 1 0 s 1 s V prvním kroku se ze zásobníků odeberou čísla a 11, b 11, 0, 0 a po provedení instrukcí (1) bude platit s 11 = a 11 b 11, s 1 = 0, s 1 = 0, s = 0. 14
V dalších krocích pak s 11 = a 11 b 11 + a 1 b 1, s 1 = a 1 b 11, s 1 = a 11 b 1, s = a 1 b 1 ; s 11 = a 11 b 11 +a 1 b 1, s 1 = a 1 b 11 +a b 1, s 1 = a 11 b 1 +a 1 b, s = a 1 b 1 a s 11 = a 11 b 11 + a 1 b 1, s 1 = a 1 b 11 + a b 1, s 1 = a 11 b 1 + a 1 b, s = a 1 b 1 + a b. Je vidět, že ( s11 s 1 s 1 s ) = (AB) T, takže z vnitřních pamětí procesorů stačí matici AB pouze přečíst. 3.3. Tato úvaha se snadno zobecní i na větší matice než jen :... a 13 a a 31 a 1 a 1 0 a 11 0 0 b 31 b 1 b 11 s 11 s 1 s 13 b b 1 0 s 1 s s 3 b 13 0 0 s 31 s 3 s 33 3.4. Soustavy rovnic. Zastavme se ještě nad otázkou řešení soustavy lineárních rovnic Ax = b. Předpokládejme pro jednoduchost, že matice A je regulární řádu n n. Máme-li k disposici n procesorů, můžeme provést jordanovskou eliminaci v 15...
čase O(n). V prvním kroku upravíme matici A ekvivalentními úpravami na tvar, kde v prvním sloupci bude vektor (1, 0,..., 0) T. Všimněme si, že pomocí n procesorů lze v jediném kroku provést prohození dvou řádků či přičtení k- násobku jednoho řádku k jinému řádku. A též konstatujme, že provedení více úprav zároveň je taktéž možné provádět paralelně. Předpokládejme, že a 11 0. Pak stačí v čase O(1) přenásobit první řádek matice (A b) číselm 1 a 11 a poté paralelně pro i =, 3,..., n provést instrukci if a i1 0 then přičti ( a i1 )-násobek prvního řádku k i-tému řádku; Protože všechny tyto úpravy probíhají paralelně, vyžaduje celá akce čas O(1). Tím máme v prvním sloupci matice jedničku následovanou samými nulami. Kdyby náhodou bylo a 11 = 0, je třeba nejprve najít nějaký řádek, který má na prvním místě nenulové číslo, a tento řádek prohodit s prvním řádkem. To však můžeme učinit např. pomocí algoritmu popsaného v části : v konstantním čase najdeme maximum max a minimum min z čísel a 1, a 31,..., a n1. Alespoň jedno z čísel min a max musí být nenulové. Pokud si zapamatujeme, ve kterém řádku bylo toto nenulové číslo nalezeno, máme tak nalezen řádek, který se prohodí s prvním řádkem. Stále tak máme čas O(1). Je vidět, že na ekvivalentní úpravy, které vynulují jeden sloupec, potřebujeme čas O(1); na převedení matice do jordanovského tvaru (tj. do tvaru (1 b ), kde 1 je jednotková matice) potřebujeme čas O(n), protože musíme nulovat n sloupců. 3.5. Existují však i jiné postupy, jak paralelisovat výpočet soustavy rovnic. Předpokládejme, že matice A je řádu n n, kde n je mocninou dvojky. Můžeme postupovat takto: rozdělíme matici (A b) na horní polovinu (kterou tvoří řádky 1,,..., n) a dolní polovinu (řádky n +1, n +,..., n) a paralelně provedeme v obou částech ekvivalentní úpravy tak, abychom matici (A b) převedli do tvaru ( ) 1 F1 g 1 F 1 g (kde 1 označuje jednotkovou matici řádu n n ). Tím jsme původní soustavu převedli na ekvivalentní soustavu ( 1 F1 F 1 ) ( x1 x 16 ) = ( g1 g ).
( 1 F1 Přenásobme tuto soustavu zleva maticí F 1 ( 1 F1 F 1 ) ( 1 F1 F 1 Dostáváme tak soustavu ( 1 F1 F 0 0 1 F F 1 ) ( x1 x ) ( x1 ) = x ) : ( 1 F1 F 1 ) ( g1 F = 1 g g F g 1 ) ( g1 ). g ). Nyní již můžeme rekursivně řešit dvě soustavy rovnic o n proměnných: (1 F 1 F )x 1 = g 1 F 1 g, (1 F F 1 )x 1 = g F g 1. Tyto dvě soustavy jsou nezávislé, a tak je lze samozřejmě počítat paralelně. Tuto metodu lze dále rozpracovat tak, že dosáhneme výpočetního času O(log n), což je nejlepší dosud známý paralelní algoritmus pro řešení soustav lineárních rovnic. 4. Rekurentně zadané poslopnosti 4.1. Mějme dány dva vektory (a 1, a, a 3,..., a n ), (b 1, b,..., b n ), kde o n pro jednoduchost předpokládejme, že je mocninou dvojky, a a 1 = 0. Naším úkolem je spočítat vektor (y 1, y,..., y n ) takový, že y 1 = b 1 a y i = a i y i 1 + b i pro i =, 3,..., n. To je lineární rekurentně zadaná posloupnost. Úlohu můžeme na sekvenčním počítači řešit v čase O(n) přesně podle definice, tedy postupně počítat y 1, y, y 3 atd. podle uvedených vztahů. 4.. Na paralelním počítači však můžeme postupovat efektivněji. Spočteme dva pomocné vektory délky n α i = a i a i 1 i = 1,,..., n, β i = a i b i 1 + b i i = 1,,..., n. () 17
Nyní předpokládejme, že dokážeme vyřešit stejnou úlohu, ale o polovinu kratší : spočtěme vektor ω 1, ω,..., ω n splňující ω 1 = β 1, ω i = α i ω i 1 + β i pro i =, 3,..., n. (3) 4.3. Hodnoty y i lze získat na základě vztahů je-li i = 1, pak y 1 = b 1, je-li i sudé, pak y i = ω i, je-li i liché, i 1, pak y i = a i ω i 1 + b i. (4) 4.4. Náš algoritmus pak bude počítat podle následujícího schematu: (a) if n = 1 then y 1 := b 1 ; exit; (b) paralelně spočti (α 1, α,..., α n ) a (β 1, β,..., β n ) podle (); (c) rekursivně spočti (ω 1, ω,..., ω n ) splňující (3); (d) paralelně spočti y i pomocí vztahů (4). Je zřejmé, že máme-li k disposici n procesorů, řádky (a), (b) a (d) budou trvat O(1). Protože se rekurse (c) používá vždy na problém poloviční velikosti (nejprve n, další volání n, n atd.), bude počet rekursivních volání 4 8 O(log n). Dostáváme tak celkový výpočetní čas O(log n). 4.5. Dodatek. Zbývá ukázat, že vztahy (4) platí: tedy, že náš algoritmus počítá správně. Postupujme indukcí; pro i = 1 je to triviálně pravda. Ukažme, nejprve že pro i sudé platí y i = ω i. Číslo ω i bylo rekursí spočteno tak, aby platilo ω i = α i ω i 1 + β i, tedy podle () ω i = a i a i 1 ω i 1 + a ib i 1 + b i. Z indukčního předpokladu y i = ω i 1 dostáváme ω i = a i a i 1 y i + a i b i 1 + b i = a i (a i 1 y i + b i 1 }{{} =y i 1 ) + b i = a i y i 1 + b i = y i. Pro lichá čísla je situace velmi jednoduchá: je-li pro sudé i y i = ω i, pak pro i + 1 je y i+1 = a i ω i + b i, což je jen jiný zápis pro poslední vztah (4). 18
Cvičení Kapitola 1. Nechť je dána posloupnost a 1, a,..., a n. (a) Rozhodněte, jaký je determinant matice b ij definované v 1.7. (b) Rozhodněte, jaká je hodnost matice b ij. (c) Jaký význam mají řádkové a sloupcové součty matice b ij? (d) Použijte algoritmů z částí 0.3 a z kapitoly 1 a sestavte algoritmus na hledání mediánu posloupnosti v čase O(log n). 19