A Habanero Java programozási nyelv

Példaprogramok

Ebben a részben néhány működő példakód olvasható, melyek a Habanero Java egyes lehetőségeit villantják fel.

Iteratív átlagolás

A következő példa egy egydimenziós iteratív átlagoló eljárás, melyet a phaserekkel és accumulatorokkal foglalkozó cikkekben előszeretettel hoztak fel példaként a Habanero Project résztvevői. Itt nem a cikkbeli példakódok szerepelnek, mert azok szintaxisa nem megfelelő a jelenlegi fordítónak, valamint csak a program érdekes részét tartalmazzák. Itt működőképesre kiegészített és módosított, szellemükben az eredetieknek megfelelő megvalósítások szerepelnek.

Maga az eljárás annyit tesz, hogy adott tömb egymás melletti elemeit elkezdi átlagolni mindaddig, amíg az átlagolás során az elemek változásainak összege egy előre megadott érték alá nem csökken. Végül kiírja az elvégzett iterációk számát.

Most álljon itt a program kerete, melyet a cikkek nem tartalmaznak. A jelzett helyre kell beírni a következőkben bemutatott különböző implementációkat. Az egész és valós értékek megosztott használata miatt szükséges a wrapper osztályok definiálása. A következő kódot egy választott implementációval kiegészítve, és az egészet az Averaging.hj fájlba helyezve, egy lefordítható és működő Habanero Java programot kapunk.

import java.util.Random; import java.lang.Math; public class Averaging { class myInteger { public int i = 0; } class myFloat { public float f = 0.0f; } void averaging() { final int L = 10; final region dataRegion = [0:L]; final region compRegion = [1:L-1]; final float[.] oldA = new arrayView(new float[dataRegion.size()], 0, dataRegion); final float[.] newA = new arrayView(new float[dataRegion.size()], 0, dataRegion); final float[.] tmp = new arrayView(new float[compRegion.size()], 0, compRegion); final float[.] diff = new arrayView(new float[compRegion.size()], 0, compRegion); Random gen = new Random(); for(point[i] : dataRegion) { oldA[i] = gen.nextFloat() * L; } newA[dataRegion.low()] = oldA[dataRegion.low()]; newA[dataRegion.high()] = oldA[dataRegion.high()]; final float eps = 10e-2f; final myFloat delta = new myFloat(); final myInteger iters = new myInteger(); //implemetation of averaging } public static void main(String[] args) { new Averaging().averaging(); } }

Barrier

Az első implementáció a legkevesebbet használja ki a phaserek adta lehetőségekből.
Miután minden tevékenység befejezte az adott iterációs lépést, meg kell határozni az iterációban bekövetkezett változások összegét, valamint fel kell cserélni a régi és új elemeket tartalmazó tömböket, és persze az iteráció-számlálót inkrementálni kell. Az összegzést nyilván egy tevékenységnek kell végeznie. Az elemek cseréjét szintén csak akkor kezdhetjük el, ha már minden tevékenység befejezte az átlagolást, de ezt minden pozícióra megtehetné az illetékes tevékenység is a köztes fázisban. Azonban a szofisztikáltabb megoldásnál a cserét az összegzéssel együtt egy kitüntetett tevékenységnek kell végeznie, úgyhogy így szerepel itt is. A tevékenységek szinkronizálásához a forall ciklus által implicit létrehozott phasert használjuk.

delta.f = eps + 1.0f; iters.i = 0; forall(point[i] : compRegion) { while (delta.f > eps) { newA[i] = (oldA[i - 1] + oldA[i + 1]) / 2.0f; diff[i] = Math.abs(newA[i] - oldA[i]); next; //barrier if (i == 1) { delta.f = 0.0f; for(point[j] : diff) delta.f += diff[j]; iters.i++; for(point[j] : tmp) { tmp[j] = newA[j]; newA[j] = oldA[j]; oldA[j] = tmp[j]; } } next; //barrier } //while } //forall System.out.println("Iterations: " + iters.i);

Látható, hogy logikailag minden iteráció egy fázisnak tekinthető, azonban a fázisok közt elvégzendő műveletek megfelelő szeparálása miatt szükséges volt egy közbenső fázis bevezetése is, amelyben egy kitüntetett szál, jelen esetben az 1 pozícióhoz tartozó, elvégzi az összegzést, az iteráció-számláló növelését és a tömbelemek cseréjét.

Single statement

Kihasználva a phaserek által nyújtott single statement lehetőségét a következő alakra hozhatjuk az előzőleg két next utasítás közt szereplő fázisátmenethez tartozó műveleteket.

delta.f = eps + 1.0f; iters.i = 0; forall(point[i] : compRegion) { while (delta.f > eps) { newA[i] = (oldA[i - 1] + oldA[i + 1]) / 2.0f; diff[i] = Math.abs(newA[i] - oldA[i]); next single { delta.f = 0.0f; for(point[j] : diff) delta.f += diff[j]; iters.i++; for(point[j] : tmp) { tmp[j] = newA[j]; newA[j] = oldA[j]; oldA[j] = tmp[j]; } } //single statement } //while } //forall System.out.println("Iterations: " + iters.i);

Ebben az esetben is a szinkronizált tevékenységek egyike fogja elvégezni a fázisok közti műveleteket, de hogy pontosan melyik, azt a futtató környezet dönti el. Ez a megvalósítás már jól illeszkedik a megoldás logikai felépítéséhez, hiszen az összegzés, a csere és az inkrementálás a fázisátmenetek során hajtódik végre, és ez már a szintaxis szintjén is megjelenik.

Accumulator

Ilyenkor a fázisátmenet szűk keresztmetszetet jelenthet. Mivel az összegzés egészét a fázisátmenet során kell elvégezni, az egyébként elsőre pillanatszerűnek gondolható fázisátmenet elég hosszúra nyúlhat. Éppen ilyen esetekre találták ki az accumulatorokat, melyek lehetővé teszik, hogy az összegzés és a fázisműveletek átlapolódjanak, de természetesen megbízható módon.

Az accumulator létrehozásához szükségünk van egy explicit phaserre, így a forall ciklus használata okafogyottá válik, mivel az implicit phaserre nincs szükség. Ezért használjuk a foreach ciklust, ami viszont szükségessé teszi a ciklus beágyazását egy finish blokkba, hogy az eredmény kiírása csak az iterációk befejeződése után történjen meg.

finish { delta.f = eps + 1.0f; iters.i = 0; phaser ph = new phaser(); final accumulator ac = accumulator.factory.accumulator(accumulator.Operator.SUM, double.class, ph); foreach(point[i] : compRegion) { while(delta.f > eps) { newA[i] = (oldA[i-1] - oldA[i+1]) / 2.0f; diff[i] = Math.abs(newA[i] - oldA[i]); ac.send(diff[i]); next single { delta.f = ac.result().floatValue(); iters.i++; for(point[j] : tmp) { tmp[j] = newA[j]; newA[j] = oldA[j]; oldA[j] = tmp[j]; } } } //while } //for } //finish System.out.println("Iterations: " + iters.i);

Tehát ebben a megoldásban a fázisátmenet során az összegzés helyett csak az összeget kell elkérni az accumulatortól.

Egyesekben felvetődhet a gondolat, hogy akár még ez is elhagyható lehet a fázisátmenetből, ha a delta.f érték helyett az összeg lekérdezését írjuk a ciklusfeltételbe. A következő bekezdésben ezzel a felvetéssel kapcsolatos gondolatébresztő elmélkedés olvasható.
A accumulator-érték ciklusfeltételben történő lekérdezése a feltétel első kiértékelésétől eltekintve jó megoldás lenne. Az első fázisban azonban külön odafigyelést kíván ez a megoldás, mivel az accumulator létrehozásakor az egyébként nem is létező előző fázis eredményeként 0-t ad. Csak úgy lehetne megoldani, hogy az (eps + 1.0f) értéket adja a feltétel első kiértékelésekor, ha azt a foreach előtt elküldi neki a fő tevékenység. Azonban ezután szükség lenne az első ciklus elején az accumulator nullázására, mielőtt a változások összegzése elkezdődik. Ezt két módon lehetne megtenni: Egyrészt egy fázisátmenettel egyből a ciklusmag elején. Így azonban minden iterációnak egy fölösleges fázisátmenettel kéne kezdődnie. Esetleg ezt a fázisátmenetet egy elágazás belsejébe lehetne rakni, amely csak a ciklusmag első lefutásakor hajtódik végre. Így csak egy logikailag felesleges fázisunk keletkezne, de az elágazás feltételét a ciklusmag minden lefutásakor ki kellene értékelni. A másik lehetőség, hogy a ciklusmag első lefutásának elején az egyik szál (-1.0) * (eps + 1.0f) értéket küld az accumulatornak, így kinullázva az értékét. Ez a megoldás azonban csak bonyolítaná a kódot, legalább egy elágazás megjelenésével a ciklusmagban. Ráadásul ennek feltételét a ciklusmag minden egyes végrehajtásakor ellenőrizni fogja a program, minden tevékenységben. Mindent összevetve, bár technikailag megoldható az accumulator értékének használata a ciklusfeltételben, sokkal olvashatóbb, a feladat megoldásához logikailag jobban illeszkedő, és a lebegőpontos érték tárolásának tárigényétől eltekintve valószínűleg hatékonyabb programot is kapunk, ha a fenti kódot használjuk.

Mátrixszorzás

A párhuzamosítással kapcsolatban tipikus példa a mátrixszorzás, úgyhogy nem maradhat ki az implementációja a Habanero Java ismertetéséből sem. Két változata is szerepel a következőkben, hogy szembetűnő legyen, milyen apró a különbség a két implementáció között.

A mátrixokat kétdimenziós tömbnézetek reprezentálják, és a biztonságos használat érdekében először célszerű ellenőrizni, hogy a paraméterül kapott nézetek kétdimenziósak-e, illetve megfelelnek-e a mátrixok szorzására vonatkozó követelményeknek. Erre a következő segédmetódus szolgál, melyet a szorzás mindkét implementációja meghív.

void checkMatrices(double[.] A, double[.] B, double[.] C) throws Exception { if(A.region.rank() != 2 || B.region.rank() != 2 || C.region.rank() != 2) throw new Exception("Parameters must be matrices."); if(!A.region.rank(1).equals(B.region.rank(0))) throw new Exception("A and B are not multiplicable."); if(!A.region.rank(0).equals(C.region.rank(0)) || !B.region.rank(1).equals(C.region.rank(1))) throw new Exception("C's region is not the result region."); }

Szekvenciális

Először lássuk a szekvenciális megvalósítást. A szükséges intervallumokat egy-egy region típusú változóba tesszük, majd teljesen triviális módon végigiterálunk az eredménymátrix elemein, és a mátrixszorzás definíciójának megfelelően kiszámítjuk az egyes elemeket.

public static void MatMultSerial(double[.] A, double[.] B, double[.] C) throws Exception { checkMatrices(A, B, C); region rows = A.region.rank(0); region columns = B.region.rank(1); region inner = A.region.rank(1); for(point[i] : rows) for(point[j] : columns) { C[i, j] = 0.0; for(point[k] : inner) C[i, j] += A[i, k] * B[k, j]; } }

Feltűnhet, hogy a külső két ciklust akár egybe is lehetne olvasztani a [rows, columns] tartomány használatával. Azonban többdimenziós tartományt, csak egydimenziós tartományok segítségével lehet definiálni. Bár ez az intervallumokra igaz, de önmagában a region típus ezt nem biztosítja. Viszont lehetőség van a tartományok dimenziószámának explicit jelölésére. Ekkor a rank(int) metódus által visszaadott tartományokat castolni kell a megfelelő „altípusba”. (Idézőjelet használtam, mert igazából nem tudni, hogy a fordító hogyan kezeli ezt a jelölésmódot.) Így a következőhöz hasonló módon összevonhatók a ciklusok.

region(:rank==1) rows = (region(:rank==1)) A.region.rank(0); region(:rank==1) columns = (region(:rank==1)) B.region.rank(1); for(point[i, j] : [rows, columns]) Stmt; //inner loop

Ezt a fordító elfogadja, bár a castolás veszélyességére figyelmeztet. Azonban a generált bájtkódot nem lehet végrehajtani, mert a JVM VerifyErrorral elutasítja a végrehajtását. Sajnos nem ez az egyetlen eset, amikor VerifyErrort okoz egy Habanero Java 1.2 bájtkód, ugyanis a fordító bonyolultabb kifejezések kiértékelése esetén néha ilyen veszélyesnek ítélt műveletet tartalmazó kódot generál.
Tehát egyelőre kénytelenek vagyunk a kissé hosszabb első kódot használni.

Párhuzamos

Most pedig lássuk a párhuzamos megvalósítást. Itt a párhuzamosítás a sorok szerint történik, tehát az eredmény minden sorát külön tevékenység határozza meg. Az implementáció csupán annyiban különbözik a szekvenciális megvalósítástól, hogy a legkülső, sorokat bejáró ciklust forall kulcsszóval vezetjük be. Bár a forall implicit phaserére nincs szükségünk, az implicit finish miatt mégis ezt használjuk a foreach helyett, hogy csak az eredmény teljes meghatározása után haladjon tovább a vezérlés a fő tevékenységben.

void MatMultRowStripedAsync(final double[.] A, final double[.] B, final double[.] C) throws Exception { checkMatrices(A, B, C); final region rows = A.region.rank(0); final region columns = B.region.rank(1); final region inner = B.region.rank(0); forall(point[i] : rows) for(point[j] : columns) { C[i, j] = 0.0; for(point[k] : inner) C[i, j] += A[i, k] * B[k, j]; } }

Fibonacci

A következő példa a Habanero Java 1.2.0 hivatalos kiadása mellé csomagolt példák egyike, a future-ök használtatára mutat egy egyszerű példát.

A FibFuture osztály fib(int) metódusa az n-edik Fibonacci-számot határozza meg future-ök segítségével. Maga a program paraméterként vár egy n egész számot, és az n-edik Fibonacci-számot kiírja a képernyőre. A megvalósítás magáért beszél.

/* * RICE University * Habanero Team */ public class FibFuture { public FibFuture() { super(); } public int fib (final int n) { if ( n < 2) { return n; } final future<int> x = async<int> { return fib (n-1); }; final future<int> y = async<int> { return fib (n-2); }; return x.get() + y.get(); } public static void main (String[] args) { if( args.length != 1 ) { System.err.println("Usage: Fib <n>\n"); System.exit(0); } final int n = java.lang.Integer.parseInt(args[0]); final FibFuture f = new FibFuture(); final future<int> result = async<int> { return f.fib (n); }; System.out.println(result.get()); } }