View
4
Download
0
Category
Preview:
Citation preview
Tim Bruneel
parallellisatieEen raamwerk voor softwarematige speculatieve
Academiejaar 2008-2009Faculteit IngenieurswetenschappenVoorzitter: prof. dr. ir. Jan Van CampenhoutVakgroep Elektronica en informatiesystemen
Master in de ingenieurswetenschappen: computerwetenschappen Masterproef ingediend tot het behalen van de academische graad van
Begeleiders: dr. ir. Hans Vandierendonck, ir. Sean RulPromotor: prof. dr. ir. Koen De Bosschere
Dankwoord
Hierbij wens ik iedereen te bedanken die heeft bijgedragen tot het tot stand komen van
deze masterproef. Bijzondere dank gaat uit naar mijn promotor Koen De Bosschere
die het mogelijk maakte om rond dit onderwerp te werken, en de begeleiders Hans
Vandierendonck en Sean Rul voor de ondersteuning. Ik wens ook mijn familie en vrien-
den te bedanken voor de steun gedurende het jaar.
i
Toelating tot bruikleen
De auteur geeft de toelating deze masterproef voor consultatie beschikbaar te stellen
en delen van de masterproef te kopieren voor persoonlijk gebruik.
Elk ander gebruik valt onder de beperkingen van het auteursrecht, in het bijzonder met
betrekking tot de verplichting de bron uitdrukkelijk te vermelden bij het aanhalen van
resultaten uit deze masterproef.
20/08/2009
ii
Een raamwerk voor softwarematige speculatieve parallellisatie
door
Tim Bruneel
Promotor: prof. dr. ir. Koen De Bosschere
Thesisbegeleiders: dr. ir. Hans Vandierendonck, ir. Sean Rul
Afstudeerwerk ingediend tot het behalen van de graad van Master in de ingenieurswe-
tenschappen: computerwetenschappen
Academiejaar 2008–2009
Universiteit Gent
Faculteit Ingenieurswetenschappen
Vakgroep Elektronica en Informatiesystemen
Voorzitter: prof. dr. ir. J. Van Campenhout
Samenvatting
Steeds vaker bezitten processoren meerdere rekenkernen. De meeste programma’s
worden echter sequentieel geschreven, waardoor ze daar niet optimaal gebruik van ma-
ken. Daarom zijn er codetransformaties en parallellisatiemechanismen nodig die compi-
lers kunnen toepassen om programma’s te parallelliseren. Vanwege data afhankelijkhe-
den en alias analyse beperkingen kunnen echter niet alle parallellisatiemogelijheden bij
compilatie gedetecteerd worden. Speculatieve parallellisatie biedt daar een oplossing
voor, door toe te laten om mogelijks conflicterende code toch parallel uit te voeren, en
eventuele conflicten tijdens de uitvoering te detecteren en te herstellen. In deze mas-
terproef wordt onderzocht wat de mogelijkheden zijn van een softwarematig raamwerk
voor het speculatief parallel uitvoeren van code.
Trefwoorden: speculatieve parallellisatie, data afhankelijkheden, compilers, specula-
tieve uitvoering
iii
A framework for software speculativemulti-threading
Tim Bruneel
Supervisors: prof. dr. ir. Koen De Bosschere, dr. ir. Hans Vandierendonck and ir. Sean Rul
Abstract— Parallelization of programs is becoming increasingly important with the rise of multi-core processors. However not all parallelization options can be detected at compile time, since there are some data dependencies that can only be resolved at runtime. Speculative multi-threading allows to parallelize code witch may contain dependencies, and detects and corrects them at runtime. This thesis discusses the development of a software based framework for speculative multi-threading. It describes and tests the use of a framework that would allow compilers introduce speculative multi-threading in an automated fashion.
Keywords— Speculative multi-threading, data dependencies, compilers
I.INTRODUCTION
The increasing performance of single core processors led to very complex architectures, which makes further improving them increasingly more difficult. Therefore, processor manufacturers started producing multi-core processors to gain further performance improvements. However, most programs are written sequentially, and do not benefit from multiple cores. Therefore, automatic parallelization techniques are an important research issue, as multi-core processors contribute an increasingly larger share of the market while most programmers still write sequential code.
The problem with sequential programs is that they contain data dependencies. For example, instructions that read and write the same address, can't be executed in parallel, because the result would be undefined, and dependent on which instruction finishes first and which last. When automatically parallelizing a program, the compiler has to detect code without such dependencies, to determine which code can be executed in parallel and which must be executed sequentially. However, not all dependencies can be resolved at compile time, which means some possible parallelization opportunities are lost because the compiler can't be sure whether a dependency will occur or not. Speculative multi-threading allows to execute code with unresolved dependencies in parallel, monitors the executed operations at runtime to detect any dependency violations that may occur and provides a recovery mechanism to restore a consistent program state if a dependency is violated. This thesis discusses the development of a software framework for speculative multi-threading that doesn't require a big knowledge of or many changes to the parallelized code.
II.SPECULATIVE CONTROL TECHNIQUES
For speculative execution, there's need for a framework that monitors memory operations, detects conflicts and can execute rollbacks when conflicts occur. Such a framework can be implemented with hardware support, which can minimize code
overhead by implementing speculative control techniques in the hardware.[1][2] Good speedups can be achieved this way. However the possibilities are limited, because the dependence on the hardware doesn't leave much room to differentiate the control framework for different types of speculative code. For that, a software framework can be useful. In this master test we will consider a software framework for speculative multi-threading, which could be used by compilers for automatic parallelization of code. Other software frameworks have been designed.[3][4] We use methods derived from those frameworks and try to implement them in a fashion that allows speculative parallelization without much knowledge of the parallelized code.
III. THE FRAMEWORK
To implement a software-based speculative multi-threading framework we considered two main approaches. In the following, when we speak of lower threads, we mean threads that execute code that comes first in the sequential version of the program, higher threads means threads that execute code that comes later in the sequential order.
A.Shared memory
With shared memory, we mean that the threads can execute operations on original memory locations of the variables. So if different threads use the same variables, they read and write to the same memory locations.
1) Possible conflictsIf the threads work with the same data, we can be faced with
three types of conflicts:- Read after write conflicts : if a higher thread reads an
address that is later written by a lower thread, the wrong value has been read by the higher thread: the instruction in the higher thread read the original value, while it should have read the value written by the lower thread.
- Write after read conflicts : if a higher thread writes to a memory location which is later read by a lower thread, the lower thread reads the wrong data: it reads the modified data while it should have read the original data.
- Write after write conflicts : if a lower thread writes to a location after a higher thread has written to that location, the location has the value of the earliest write instruction, while it should have the one of the latter.
2) Conflict DetectionTo detect conflicts, each read and write operation is
redirected to the framework, which logs the time for each operation. After execution of the speculative code, or after a configurable amount of speculative iterations, the threads compare their logged operations with each other to find if there were any dependencies and if they were violated. Write after write conflicts can be eliminated during execution by only writing to a location if no higher threads have already written to it.
3) Conflict recoveryWhen executing write operations, the framework backs up
the original value of the address if it is the first time that address is written. That way there are copies for all the original values from memory locations that are written during the speculative execution. When a conflict is detected, these backups are used to restore the memory to the state it was before the speculative execution, and the code will be executed sequentially.
4) SynchronizationWhen no conflicts are detected, there is no synchronization
needed. The threads executed their operations on the original data locations, which now have the correct values
B.Local memory
In this approach, each thread has it's own local copy of the memory locations it writes. All the write operations are executed on the local copies.
1) Possible conflictsThe threads can't influence each other with their memory
operation, so we can only be faced with read after write conflicts: if a higher thread reads a location written by a lower thread, it will read the original data instead of the data written by the lower thread. As for write after read conflicts: since threads execute their write operations on their own copy, a write operation by one thread can't influence what is read by another thread, so write after read dependencies can't be violated. The same goes for write after write dependencies: when different threads have written copies of the same location, the copy of the highest thread will be selected at the synchronization, so the location will get the right value.
2) Conflict DetectionTo detect the read after write dependencies between the
threads, each thread keeps track of the original addresses of the locations that have been written and the locations that have been read before they were written. After execution of the speculative code, or after a configurable amount of speculative iterations, each thread compares addresses that were read before they were written with the addresses written by previous threads. If there's a match, there's a read after write dependency and it's violated because the thread read the original value of the address instead of the copy written by the previous thread. This doesn't require the framework to log each memory operation. The speculative code just has to ask the framework for a pointer to a local copy of each used location once, and can directly access it afterwards.
3)Conflict recoveryWhen there's a conflict, the local copies of the thread in
which the conflict was detected are discarded, along with the local copies of all higher threads. If there are no conflicts in the previous threads, they can be synchronized, since their execution was correct. That way we obtain a consistent memory status which corresponds to the beginning of the conflicting thread, and we can continue execution from there.
4)SynchronizationWhen there are no conflicts, we need to synchronize the
local copies of the different threads to get the right values in the original memory locations. Each thread receives a mapping that maps the original memory locations on a local copy from the previous thread, and adds to that its own mapping for locations that aren't in the map yet, and replaces the local copy address for values it has written that are already in the map. In the end, the map will contain all written locations and a pointer to the live-out written value of it. These are copied to the original locations, so all original addresses get the right live-out values.
IV. RESULTS
To compare the different approaches, we tested the framework on a simple example loop. The results indicated that the overhead of working with shared memory locations is way too big. The speculative code with shared memory performed a lot slower than the original. This is due to the fact that the framework has to be contacted for each memory operation. The fact that each memory operation must be logged together with the time it took place does not allow to cut back on control code overhead. Working with local copies doesn't have that restriction, since the framework doesn't need to know the time of the memory operations, that way it can perform much better by reducing the amount of times the framework is called. This led to a considerable speedup, especially when the number of iterations grew.
Other test results indicated that the use of smaller intervals after which to synchronize seems to be profitable. Conflicts are detected more quickly, less invalid iterations are executed.
We also parallelized the mcf benchmark from the SPEC 2006 benchmark suite.[5] This didn't result in a speedup, which indicates that there's need for combining the framework with code transformations when parallelizing complex loops with a lot of pointer operations.
V.CONCLUSIONS
The results indicate that working with local copies for each thread and synchronizing regularly are the best methods for implementing a software speculative multi-threading framework. The results with the mcf benchmark indicate that for parallelization of complex code with a lot of pointer operations, there is need for code transformations to be able to generate an efficient speculative version.
ACKNOWLEDGEMENTS
The authors would like to thank prof. dr. ir. Koen De Bosschere for the opportunity to work on this subject, and dr. ir. Hans Van Dierendonck and ir. Sean Rul for guidance.
REFERENCES
[1] H. Zhong, M. Mehrara, S. Lieberman, and S. Mahlke. Uncovering hidden loop level parallelism in sequential applications. In 14th International Symposium on High Performance Computer Architecture, pages 290–301. IEEE Computer Society, 2008.
[2] M. Bridges, N. Vachharajani, Y. Zhang, T. Jablin, and D. August. Revisiting the sequential programming model for multi-core. In Proceedings of the 40th Annual IEEE/ACM International Symposium on Microarchitecture, pages 69–84. IEEE Computer Society, 2007.
[3] Peter Rundberg and Per Stenström. An all-software thread-level data dependence speculation system for multiprocessors. Journal of Instruction-Level Parallelism, 3:2002, 2001.
[4] C. Oancea and A. Mycroft. Software thread-level speculation: an optimistic library implementation. In Proceedings of the 1st international workshop on Multicore software engineering, pages 23–32, 2008.
[5] Standard Performance Evaluation Corporation. Spec cpu 2006. http://www.spec.org/cpu2006/.
Inhoudsopgave
Dankwoord i
Toelating tot bruikleen ii
1 Inleiding 1
1.1 Situering . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.1.1 Parallellisatie van sequentiele programma’s . . . . . . . . . . . . 1
1.1.2 Limieten van niet-speculatieve parallellisatie . . . . . . . . . . . 2
1.1.3 Speculatieve parallellisatie . . . . . . . . . . . . . . . . . . . . . 4
1.2 Overzicht . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
2 Speculatieve parallellisatietechnieken 6
2.1 Conflicten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
2.2 Speculatieve parallellisatie met hardware ondersteuning . . . . . . . . . 9
2.3 Softwarematig . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
3 Softwarematige speculatieve uitvoering 11
3.1 Geheugentoegang . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
3.1.1 Gedeelde data . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
3.1.2 Lokale data . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
vi
3.1.3 Analyse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
3.2 Granulariteit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
4 Het ontworpen raamwerk 26
4.1 Interface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
4.2 Interne werking . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
4.2.1 Gedeelde data . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
4.2.2 Lokale kopieen . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
4.2.3 Beperkingen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
4.3 Resultaten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
4.3.1 Gedeelde data . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
4.3.2 Lokale data . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
4.3.3 Conclusie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
5 Parallellisatie van de mcf benchmark 49
5.1 MCF benchmark . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
5.2 Parallellisatie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
5.2.1 Het a-blok . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
5.2.2 Het b-blok . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
5.2.3 De primal net simplex functie . . . . . . . . . . . . . . . . . . . 52
5.2.4 Resultaat . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
5.3 Optimalisaties in de speculatieve uitvoering . . . . . . . . . . . . . . . 52
5.4 Resultaat . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
6 Besluit 56
vii
Hoofdstuk 1
Inleiding
1.1 Situering
1.1.1 Parallellisatie van sequentiele programma’s
De toenemende prestatie van processoren zorgt voor een steeds complexere architectuur,
wat het verder opdrijven van de prestatie van processoren met een enkele kern steeds
moeilijker maakt. Daarom ontwerpt de industrie steeds vaker processoren met meerdere
kernen, en ook in de mainstream computermarkt maken deze processoren een steeds
groter deel van de markt uit.
Hoewel sommige applicaties die uit meerdere draden bestaan hiervan kunnen profi-
teren, levert dit voor traditionele sequentiele applicaties echter geen voordeel op. The-
orethisch gezien kan een processor met bijvoorbeeld twee kernen wel dubbel zoveel
instructies uitvoeren, maar het grootste deel van de applicaties is sequentieel geschre-
ven, en maakt dus helemaal geen gebruik van die mogelijkheden. Ook voor applicaties
die vandaag de dag nog geschreven worden geldt dat ze voornamelijk sequentieel zijn,
aangezien het expliciet parallel programmeren heel wat extra inspanning van de pro-
grammeur vraagt. Omdat hiermee veel potentiele snelheidswinst verloren gaat, vormen
methodes om sequentiele programma’s automatisch te parallelliseren een belangrijk on-
derzoeksthema. Een compiler kan dan parallellisatiemogelijkheden gaan detecteren in
1
de code en die stukken parallel maken, zonder dat de programmeur daar aandacht aan
moet besteden.
1.1.2 Limieten van niet-speculatieve parallellisatie
Het probleem met sequentieel geprogrammeerde code is dat ze veel data afhankelijkhe-
den bevat. Opeenvolgende instructies die eenzelfde variabele bijvoorbeeld respectieve-
lijk schrijven en lezen kunnen niet parallel uitgevoerd worden.
De maximale snelheidswinst die geboekt kan worden door parallellisatie is afhanke-
lijk van de grootte van het sequentiele deel van het programma dat niet geparallelliseerd
kan worden. De wet van Amdahl zegt immers dat de maximale snelheidswinst van een
programma op een processor met N kernen als volgt berekend kan worden:
1
(1− P ) + PN
waarbij P de proportie van het programma is dat parallel uitgevoerd wordt, dus (1-P)
het gedeelte is dat sequentieel uitgevoerd wordt. Het resultaat hiervan wordt weergege-
ven in figuur 1.1, voor enkele programma’s met verschillende grootte van het parallelle
deel. Dit illustreert duidelijk het belang van het maximaliseren van het parallelle ge-
deelte van een programma. Bij een laag aantal processorkernen heeft het verhogen van
het aantal kernen nog een aanzienlijke invloed op de versnelling, maar naarmate het
aantal hoger wordt, wordt die invloed vrijwel nihil. De invloed van de grootte van het
parallelle deel neemt echter steeds toe, en zal dus in de toekomst een steeds belang-
rijkere rol spelen in het boeken van snelheidswinst op processoren met een toenemend
aantal kernen.
Bij het parallelliseren van code is het ontdekken en mogelijks elimineren van data
afhankelijkheden dus zeer belangrijk om het sequentieel uit te voeren deel te verklei-
nen. Geavanceerde alias analyse methodes om afhankelijkheden te detecteren, en code
transformaties om afhankelijkheden te elimineren, kunnen erin slagen stukken sequen-
tiele code te parallelliseren.
Een probleem is echter dat niet alle data afhankelijkheden met honderd procent
zekerheid tijdens de compilatie bepaald kunnen worden. Er zijn vaak stukken code
2
Figuur 1.1: De wet van Amdahl voor programma’s met verschillende groottes van de
parallelle delen
3
waarvan niet met zekerheid gezegd kan worden of ze tijdens de uitvoering wel of niet
een data afhankelijkheid zullen bevatten. Dit betekent dat die stukken code niet op
de gewone manier geparallelliseerd kunnen worden, aangezien er niet ten allen tijde
een correct resultaat gegarandeerd kan worden. Compilers moeten dus conservatief te
werk gaan bij het invoeren van klassieke parallellisatie, en alle mogelijks conflicterende
stukken ongemoeid laten. Hierbij gaan veel parallellisatie mogelijkheden verloren, aan-
gezien er vaak stukken code zijn waar de compiler geen uitsluitsel over kan geven, die
in een zeer groot percentage van de uitvoeringen toch geen afhankelijkheden bevatten.
Speculatieve parallellisatie biedt daar een oplossing voor.
1.1.3 Speculatieve parallellisatie
De techiek van het speculatief parallelliseren bestaat erin stukken code die mogelijks
conflicteren, maar in de meerderheid van de uitvoeringen geen conflicten opleveren, toch
te parallelliseren, en extra controlemechanismen in te bouwen om tijdens de uitvoering
te gaan controleren of er data afhankelijkheden zijn en of die geschonden worden. Zo
kan het parallelliseerbare deel van de code vergroot worden tot boven de limieten van de
klassieke parallellisatie, weliswaar ten koste van een extra overhead voor het detecteren
van conflicten en het in stand houden van herstelmechanismen.
In deze masterproef wordt het ontwerp van een puur softwarematig raamwerk on-
derzocht dat weinig kennis en verandering vereist van de te parallelliseren code.
1.2 Overzicht
Hier volgt een korte toelichting bij de structuur van de tekst.
Hoofdstuk 2 geeft een overzicht van verschillende technieken voor speculatieve pa-
rallellisatie, de literatuur daaromtrend en wat deze thesis daaraan toevoegt.
Hoofdstuk 3 geeft een overzicht van softwarematige speculatieve uitvoeringsmecha-
nismen, hun voor en nadelen, en ontwerpsbeslissingen die de prestatie beınvloeden.
In hoofdstuk 4 wordt het gebruik en de werking van het ontworpen raamwerk toe-
4
gelicht.
Hoofdstuk 5 beschrijft de speculatieve parallellisatie van de MCF benchmark uit de
SPEC CPU 2006 benchmark suite.
Hoofdstuk 6 formuleert een besluit over het ontworpen raamwerk, de resultaten, en
wat er in de toekomst mogelijk is.
5
Hoofdstuk 2
Speculatieve
parallellisatietechnieken
Voor het speculatief parallel uitvoeren van code is er nood aan een raamwerk dat tijdens
de uitvoering de geheugentoegangen gaat controleren, eventuele geschonden afhankelijk-
heden gaat detecteren en incorrecte uitvoeringen kan ongedaan maken. In dit hoofdstuk
wordt eerst beschreven welke conflicten er kunnen voorkomen, waarna wordt beschreven
welke mogelijkheden er zijn om een raamwerk te ontwerpen om ze te detecteren.
2.1 Conflicten
Het controlemechanisme moet volgende conflicten kunnen detecteren:
• Read after write: Een draad voert een instructie uit die data naar een geheugen-
locatie a schrijft, terwijl een andere draad een instructie uitvoert die data van
die geheugenlocatie leest. Wanneer in de sequentiele uitvoering de schrijfoperatie
voor de leesoperatie komt, maar door de parallelle uitvoering die volgorde om-
wisselt, levert de leesinstructie een verkeerd resultaat op. Dit wordt geıllustreerd
met een voorbeeld in figuur 2.1.
• Write after read: De omgekeerde situatie: de ene draad schrijft data waarna de
6
Figuur 2.1: illustratie van een read after write conflict
andere die leest, terwijl dit in de sequentiele uitvoering omgekeerd was. Dit wordt
geıllustreerd met een voorbeeld in figuur 2.2.
• Write after write: Wanneer twee draden schrijfoperaties naar dezelfde lokatie
uitvoeren die in de originele uitvoering in omgekeerde volgorde gebeurden, staat
na uitvoering het verkeerde resultaat in het geheugen. Dit wordt geıllustreerd
met een voorbeeld in figuur 2.3.
Er zijn verschillende aanpakken mogelijk om een omgeving te implementeren die
deze conflicten detecteerd.
7
Figuur 2.2: illustratie van een write after read conflict
Figuur 2.3: illustratie van een write after write conflict
8
2.2 Speculatieve parallellisatie met hardware onder-
steuning
Om de extra overhead die gepaard gaat met speculatieve parallellisatie te beperken, kan
gebruikt gemaakt worden van speciale hardware die bepaalde taken op zich neemt. Zo
beschrijven Zhong et al. [1] speculatieve parallellisatie met gebruik van een hardware
transactioneel geheugen en een communicatienetwerk tussen kernen om informatie door
te geven en bepaalde afhankelijkheden te omzeilen.
Bridges et al. [2] ontwikkelen een speculatief raamwerk met ondersteuning van ge-
avanceerde compiler en hardware technieken om te kunnen parallelliseren met weinig
overhead. Men maakt daarbij bovendien gebruik van een uitbreiding van het program-
meermodel, met een extra verantwoordelijkheid voor de programmeur in het specifieren
van bepaalde parallellisatie mogelijkheden. De hardwareondersteuning betreft een ge-
heugen met verschillende versies en een communicatienetwerk tussen de processorker-
nen. Bovendien veronderstelden ze in hun resultaten geen extra kost voor misspeculatie.
Balakrishnan en Sohi [3] onderzoeken het idee om methodes uit te voeren voor ze
effectief opgeroepen worden maar wanneer vermoed wordt dat hun juiste input reeds
beschikbaar is, maar ook hier wordt hardware ondersteuning verondersteld: een uit-
breiding van de instructieset, speciale cache vlaggen, een speciale hardware buffer, en
communicatiemiddelen tussen die buffer, de cache en het geheugen.
Hardware ondersteunde speculatieve parallellisatie kan mooie resultaten behalen,
maar is gebonden aan de specifieke hardware, en dus vaak beperkt tot een specifieke
methode.
2.3 Softwarematig
Naast controlemechanismen in de hardware, kan er ook een softwarematige omgeving
gecreeerd worden voor speculatieve parallellisatie. Een softwarematige omgeving heeft
als voordeel dat het onafhankelijk is van de hardware, en er dus met verschillende
technieken geexperimenteerd kan worden, waar je in een hardware mechanisme vaak
9
aan een techniek gebonden bent. Softwarematige oplossingen zijn dus interessant om de
gebruikte methodes te varieren aan de hand van de eigenschappen van de uitgevoerde
code. Het grootste nadeel van een puur softwarematig raamwerk is de extra code
overhead die gepaard gaat met het controleren van geheugentoegangen. Het komt er
dan ook op aan die zo veel mogelijk te beperken.
Papadimitriou en Mowry [4] ontwerpen een softwarematig mechanisme om afhan-
kelijkheden te detecteren aan de hand van de paging hardware van het geheugen. Het
pagina niveau blijkt echter te grote granulariteit te hebben, waardoor er te veel valse
conflicten gedetecteerd worden.
Prabhu en Olukotun [5] proberen op een puur softwarematige manier speculatieve
parallellisatie in te voeren in verscheidene SPEC CINT 2000 benchmarks, door middel
van uitgebreide manuele code transformaties. Dit verkent de limieten van wat theore-
tisch mogelijk is, maar komt neer op een zeer programma-specifieke optimalisatie, die
niet geautomatiseerd op grote schaal kan toegepast worden.
In deze masterproef wordt een puur softwarematig raamwerk voor het speculatief
uitvoeren van code besproken. Het is de bedoeling dat het raamwerk breed toepasbaar
is, en de transformatie van de programmacode eenvoudig is, en dus geautomatiseerd
door een compiler kan gebeuren, zonder complexe aanpassingen waarvoor menselijke
ingrepen in de code en een te grote kennis van het te parallelliseren programma nodig
zijn. De geımplementeerde mechanismen zijn gebaseerd op technieken die in ande-
re raamwerken gebruikt zijn [6, 7], maar geımplementeerd op een manier die minder
ingrepen en minder kennis van de te parallelliseren code vereist.
10
Hoofdstuk 3
Softwarematige speculatieve
uitvoering
Voor het speculatief uitvoeren van code moeten op verschillende vlakken beslissingen
genomen worden over de toegepaste methodes in het speculatieve raamwerk. De pres-
tatie van deze keuzes hangt af van de eigenschappen en het geheugengebruik van de
speculatief uit te voeren code. Verscheidene keuzes worden in dit hoofdstuk theoretisch
beschreven, in hoofdstuk 4 worden ze in de praktijk geımplementeerd en getest. We
beschouwen twee manieren om het geheugen te lezen en te schrijven in de speculatieve
code, met daarbij horend de mogelijke manieren om conflicten te detecteren, mogelijke
herstelmechanismes om in geval van een conflict toch een correcte uitvoering te kunnen
garanderen en synchronisatiemethodes om een correct resultaat na de speculatieve uit-
voering te verzekeren. Daarnaast kijken we ook naar keuzes rond de granulariteit om
het geheugen te controleren.
3.1 Geheugentoegang
We beschouwen twee methodes voor geheugentoegang in speculatief parallelle draden.
De eerste manier is de draden naar het geheugen laten lezen en schrijven van en naar
de originele geheugenlocaties. Dat noemen we het werken met gedeelde data, omdat de
11
draden tegelijk dezelfde geheugenlocaties kunnen gebruiken. De tweede methode is de
draden tijdens de speculatieve uitvoering laten lezen en schrijven naar een eigen kopie
van de data. Dit noemen we het werken met lokale data, omdat de draden tijdens het
uitvoeren van de speculatieve code elkaars data niet kunnen beınvloeden.
3.1.1 Gedeelde data
Bij het werken met gedeelde data lezen en schrijven de draden van en naar de origi-
nele geheugenlocaties van de variabelen. Als verschillende draden dezelfde variabelen
gebruiken lezen en schrijven ze dus naar dezelfde geheugenlocaties.
Mogelijke conflicten
Bij het werken op de originele data kunnen zowel read after write, write after read
als write after write afhankelijkheden geschonden worden. Omdat de draden naar de-
zelfde geheugenlocaties kunnen schrijven kunnen ze elkaars werking op elk moment
beınvloeden.
Conflictdetectie
• Achteraf : we kunnen in elke draad bijhouden welke lokaties er gelezen en ge-
schreven worden, om deze data na het voltooien van de speculatieve uitvoering
met elkaar te vergelijken. Zo kunnen we achteraf de afhankelijkheden detecteren,
en als er zich een voordoet, veronderstellen dat hij geschonden is. Wordt een adres
gelezen in de ene draad, en geschreven in een andere, dan is er een afhankelijkheid.
Deze methode kan in sommige situaties echter nadelig zijn, omdat er afhanke-
lijkheden kunnen voorkomen tussen de draden die toch niet geschonden worden,
bijvoorbeeld wanneer een draad die eerdere code uitvoert eerst op een adres schrijft
waarna een draad die latere code uitvoert op dat adres leest. Er is dan een read
after write afhankelijkheid tussen de draden, maar die wordt hier niet geschonden.
In dat geval kan het nuttig zijn om niet alleen bij te houden welke adressen welke
draad gelezen of geschreven heeft, maar ook wanneer. Door de timestamps te
12
vergelijken kan zo een onderscheid gemaakt worden tussen afhankelijkheden die
wel en niet geschonden zijn. Dit heeft op zijn beurt echter als nadeel dat dit extra
overhead oplevert.
• Interval: Als we de conflictdetectie helemaal op het einde van de speculatieve
uitvoering doen, dan kan er veel uitvoeringstijd verloren zijn indien zich in het
begin een conflict voordeed. Daarom kan het voordelig zijn om de speculatieve
uitvoering in intervallen te verdelen, en na elk interval te gaan controleren of er
conflicten waren, met dezelfde methode als uit de vorige paragraaf.
• Live: In plaats van achteraf kunnen we ook bij elke geheugentoegang zelf contro-
leren of er een afhankelijkheid geschonden wordt. Om dit te kunnen doen moet
er gebruik gemaakt worden van gedeelde controledata tussen de draden. Dit kan
door voor een gedeelde datastructuur bij te houden die voor elke geheugenlocatie
bijhoudt welke de hoogste draad is die er reeds naar geschreven heeft, en welke
de hoogste draad is die het gelezen heeft.
Bij het uitvoeren van een schrijfoperatie kan dan gekeken worden welke de hoogste
draad is die dat adres gelezen heeft, is die hoger dan de huidige draad, dan is er
een read after write conflict: een latere leesoperatie heeft een locatie gelezen die
door een eerdere schrijfoperatie nog moet geschreven worden.
Bij het uitvoeren van een leesoperatie wordt gekeken of er reeds een hogere draad
op dat adres geschreven heeft, is dit het geval dan is er een write after read
afhankelijkheid geschonden.
Write after write conflicten kunnen geelimineerd worden door voordat de data
weggeschreven wordt, te gaan kijken of er reeds een hogere draad naar dat adres
geschreven heeft, en niet te schrijven indien dit het geval is.
Het voordeel van achteraf controleren is dat er hierbij minder overhead is bij de
geheugentoegangen zelf, het nadeel is dat eventuele conflicten pas achteraf gedetecteerd
worden.
We kunnen ook de verschillende methodes combineren. Zo is het aangewezen om,
ook wanneer er pas achteraf op conflicten gedetecteerd wordt, op het moment van het
13
schrijven zelf te controleren op write after write afhankelijkheden, omdat die conflicten
zo vermeden kunnen worden. Dit zorgt ervoor dat code waarin verschillende draden
naar dezelfde adressen schrijven toch speculatief parallelliseerbaar is.
Herstelmechanisme
In geval er een conflict gedetecteerd is, moet er actie ondernomen worden om toch
een correcte uitvoering van de code te kunnen garanderen. Als er een afhankelijkheid
geschonden wordt, zijn meestal wijzigingen aan de orgininele data aangebracht die
ongedaan gemaakt moeten worden. Daarom is er een mechanisme nodig om terug
te keren naar een consistente geheugenstatus om vanaf daar de uitvoering te hervatten.
• Undo-log: bij het werken met een undo-log wordt bij elke operatie gelogd wat ze
aan het geheugen verandert, zodat in geval van een conflict de laatste operaties
ongedaan gemaakt kunnen worden tot op een punt waar de uitvoering nog correct
was.
• Kopie van origineel: wanneer een geheugenlocatie voor het eerst geschreven
wordt, kan er een kopie genomen worden van de originele data. In geval van een
conflict kan dan alle originele data hersteld worden.
Het werken met een undo-log heeft als voordeel dat de uitvoering in geval van een
conflict niet helemaal opnieuw vanaf het begin van de speculatieve uitvoering moet her-
beginnen. Er kan teruggekeerd worden tot op het punt van het conflict en de uitvoering
kan vanaf daar hervat worden. Dit is dus voordelig voor lange speculatieve uitvoerin-
gen die naar het einde toe vaak een conflict geven, of bijvoorbeeld lussen waarvan het
aantal iteraties vooraf niet gekend is, en er dus op het einde een aantal iteraties teveel
uitgevoerd zijn die ongedaan gemaakt moeten worden. In de meeste gevallen is deze
methode echter nadelig omwille van de extra overhead bij elke schrijfoperatie. Bij het
werken met een kopie van het origineel is er een stuk minder overhead: enkel bij de
eerste schrijfoperatie naar elke locatie is er extra werk voor het kopieren van de origine-
le data. Bovendien vraagt het werken met een kopie van het origineel een veel kleiner
14
geheugengebruik, vooral in uitvoeringen waar veel schrijfoperaties naar hetzelfde adres
worden gedaan.
Synchronisatie
Bij het werken op de originele data is er geen synchronisatie nodig. Indien er geen
conflicten waren bevindt het geheugen zich vanzelf al in dezelfde status als na het
uitvoeren van de originele, sequentiele code.
3.1.2 Lokale data
Een tweede mogelijkheid voor speculatieve geheugentoegang is het werken met lokale
data. Hierbij zal elke speculatieve draad zijn wijzigingen uitvoeren op een eigen kopie
van de data, die dan achteraf met elkaar gesynchroniseerd worden.
Mogelijke conflicten
Aangezien de verschillende draden elk naar andere locaties in het geheugen lezen en
schrijven, kunnen ze elkaar tijdens hun uitvoering niet beınvloeden. Dit betekent dat er
geen write after read en write after write afhankelijkheden geschonden kunnen worden.
Er kunnen wel read after write conflicten voorkomen: wanneer er een read after write
afhankelijkheid bestaat tussen twee instructies die beide in een andere draad uitgevoerd
worden, zal het conflict altijd optreden: aangezien beide instructies op een andere kopie
van de data worden uitgevoerd zal de leesinstructie nooit de data weggeschreven door
de schijfinstructie lezen, zelfs al vond de schrijfinstructie eerder plaats. Dit wordt
geıllustreerd in figuur 3.1. Bij het lezen van de variabele a zal de tweede draad eerst
kijken of hij zelf al een lokale kopie heeft, is dit niet het geval, dan haalt hij de data op
van de originele locatie, die nog ongewijzigd is.
15
Figuur 3.1: Illustratie van een read after write afhankelijkheid tussen draden bij het
werken met lokale kopieen van de data
Conflictdetectie
Er moet gedetecteerd worden of er read after write afhankelijkheden zijn tussen de
draden. Is dit het geval, dan zal die afhankelijkheid hoe dan ook geschonden zijn, zoals
hierboven beschreven. We kunnen deze afhankelijkheden detecteren door voor elke
draad bij te houden op welke adressen hij leest en schrijft, en die data te vergelijken:
als er in een draad die latere code uitvoert een adres gelezen wordt dat geschreven wordt
in een draad die eerdere code uitvoert, dan is er een conflict. Ook hier kunnen we, net
zoals bij het werken met gedeelde data, op verschillende manieren te werk gaan:
• live: we kunnen controledata tussen de draden delen om conflicten onmiddellijk
te kunnen detecteren. Dit zorgt er echter voor dat er opnieuw met locks gewerkt
moet worden, en de draden dus afhankelijk van elkaar worden en af en toe moeten
wachten, wat het grote voordeel van werken met lokale data deels teniet doet.
• achteraf : elke draad kan bijhouden welke variabelen hij gelezen en geschreven
heeft en die data achteraf doorgeven, zodat elke draad de gelezen variabelen kan
vergelijken met welke variabelen eerdere draden geschreven hebben om zo ge-
schonden afhankelijkheden te detecteren. Let wel: enkel de adressen die gelezen
16
Figuur 3.2: Vergelijken van controledata voor detecteren van een conflict
worden wanneer ze door die draad nog niet geschreven zijn, komen in aanmer-
king, want anders zijn ze afhankelijk van een schrijfoperatie in de eigen draad,
en niet van schrijfoperaties uit vorige draden. Beschouw het voorbeeld in figuur
3.2: draad 1 leest variabele a en draad 2 schrijft zijn eigen kopie van variabele a.
Dit is geen probleem, want draad 2 voert latere code uit en door het werken met
een lokale kopie kan die de data gebruikt door draad 1 niet beınvloeden. Draad
1 schrijft echter ook een lokale kopie van variabele b terwijl draad 2 variabele b
leest. Dit is wel een afhankelijkheid, en die wordt gedetecteerd doordat zowel de
schrijfdata van draad 1 als de leesdata van draad 2 variabele b bevatten.
• interval: net zoals bij het werken met gedeelde data geldt ook hier dat tussentijds
synchroniseren voordelig kan zijn. Het laat toe om conflicten sneller te detecteren
en zo minder ongeldige iteraties uit te voeren.
Herstelmechanisme
Aangezien alle wijzgingen weggeschreven worden naar lokale kopieen, is de originele data
van voor de speculatieve uitvoering nog onaangetast, dus is er geen herstelmechanisme
nodig om naar die toestand terug te keren. Aangezien elke draad zijn eigen versie van
de data heeft is het bovendien mogelijk om alle wijzigingen door te voeren tot aan de
draad waar het conflict zich voordeed, om zo niet de volledige uitvoering opnieuw te
moeten doen. Dit wordt geıllustreerd in figuur 3.3: de uitvoering van draden 1 en 2,
17
Figuur 3.3: Gedeeltelijke sychronisatie bij herstellen van een conflict
die code uitvoeren die in de sequentiele versie eerder kwam dan de code uit draad 3,
is correct, dus kan die data gesynchroniseerd worden, om vervolgens de uitvoering te
kunnen hervatten vanaf het begin van draad 3.
Synchronisatie
Na een correcte speculatieve uitvoering, moet de data van de verschillende lokale kopieen
gesynchroniseerd worden en gecommit worden naar de originele geheugenlocaties. Voor
elke geheugenlocatie dient de data geschreven te worden van hoogste draad die de data
geschreven heeft. Met de hoogste draad wordt de draad bedoelt die code uitvoert die
het laatst kwam in de sequentiele versie van de code.
• Een eerste mogelijkheid is om een mapping door te geven tussen de draden. De
draad die de code uitvoert die eerst kwam in de sequentiele versie geeft een da-
tastructuur door met alle geheugenadressen waarop hij geschreven heeft, samen
met de adressen van de lokale kopieen ervan. Hij geeft dit door aan de draad
die het daaropvolgende stuk code uitvoerde. Deze laatste voegt daaraan zijn ge-
schreven adressen toe, en overschrijft daarbij adressen die zowel door hem als de
voorgaande draad geschreven zijn, want zijn versie van de data is de juiste live-out
18
waarde gezien zijn code later kwam in de sequentiele uitvoering. Hij geeft dan op
zijn beurt de datastructuur door aan de volgende draad, enzoverder. De laatste
draad voegt er ook zijn data aan toe, en gaat dan de datastructuur overlopen
en voor alle adressen die erin voorkomen de lokale data naar het originele adres
kopieren. Deze doorgegeven data kan meteen ook gebruikt worden als log van de
uitgevoerde schrijfoperaties van vorige draden bij het detecteren van conflicten
(de schrijfdata uit figuur 3.2).
Voorbeeld we beschouwen twee speculatieve draden, draad 1 en draad 2, waar-
bij draad 1 code uitvoert die in de sequentiele uitvoering voor de code van draad
2 komt. Stel dat draad 1 op een adres a en b schrijft, dan bezit hij lokale kopieen
van die locaties zoals aangegeven in punt (a) op figuur 3.4. Stel dat draad 2 enkel
op een adres b schrijft, dan bevat draad 2 een lokale kopie van die locatie zoals
aangegeven in punt (b). Beide draden bevatten een mapping van de originele
locaties met de lokale kopieen die ze geschreven hebben. Bij de synchronisatie
wordt de mapping van draad 1, weergegeven in punt (c) doorgegeven aan draad
2. Draad 2 voegt daar zijn gegevens aan toe, in dit geval de pointer naar zijn
lokale kopie van adres b. Hiermee overschrijft hij de entry van draad 1 in de
mapping. De uiteindelijke mapping ziet er dan uit zoals in punt (d). Deze wordt
gebruikt om de originele geheugenlocaties aan te passen, zoals aangegeven in (e).
• Een tweede mogelijkheid is om tijdens de synchronisatie een gedeelde datastruc-
tuur bij te houden, met voor elke locatie welke draad ernaar geschreven heeft.
Elke draad overloopt alle adressen waarvan hij een lokale kopie geschreven heeft
en controleert in de gedeelde datastructuur welke draad er naar die locatie ge-
schreven heeft. Is dat een draad die code uitvoert die later komt in de sequentiele
versie, dan gooit de draad zijn kopie gewoon weg. Is dit niet het geval, dan schrijft
de draad zijn kopie weg naar het originele adres en past de gedeelde datastructuur
aan.
Voorbeeld We beschouwen opnieuw hetzelfde voorbeeld, maar nu met de twee-
de synchronisatiemethode, zoals weergegeven in figuur 3.5 Stel dat draad 1 eerst
19
Figuur 3.4: Synchronisatie met behulp van een doorgegeven mapping
begint te synchroniseren en zijn kopie van adres a wil wegschrijven. De gedeelde
datastructuur bevat nog niks, dus schrijft hij zijn kopie weg, en maakt een entry
aan in de gedeelde datastructuur met het adres a en zijn identificatie. Draad 2
ziet deze versie van de datastructuur wanneer hij zijn data wil synchroniseren.
Vooraleer hij zijn kopie van b wegschrijft zoekt hij in de map naar de entry voor
dat adres, en stelt vast dat die er nog niet is. Hij schrijft dus zijn kopie weg,
en maakt een entry aan in de datastructuur voor adres b met zijn identificatie.
Vervolgens wil draad 1 nog zijn kopie van b synchroniseren. Wanneer hij de entry
in de gedeelde datastructuur opzoekt, ziet hij dat draad 2 reeds adres b heeft
geschreven. Aangezien dat een hogere draad is, schrijft draad 1 zijn versie van b
niet weg.
Voordeel van de tweede manier van werken is dat de draden het werk verdelen,
terwijl bij de eerste methode al het werk doorgeschoven werd naar 1 draad die dan alle
data moet gaan kopieren. Nadeel van deze manier is echter dat een deel van de operaties
20
Figuur 3.5: Synchronisatie door wegschrijven en bijhouden controledata
nutteloos zijn (bijvoorbeeld wanneer een lagere draad eerst zijn data wegschrijft, om
vervolgens overschreven te worden door een hogere draad), en dat er locks genomen
moeten worden voor het wegschrijven ervan, met als gevolg dat het kan dat er gewacht
moet worden op een draad die data wegschrijft die misschien niet eens weggeschreven
moest worden. Bovendien moeten de draden voordien toch nog op elkaar wachten om
controledata door te geven voor het detecteren van conflicten, vooraleer ze data kunnen
wegschrijven. Bij de eerste methode kunnen die twee gecombineerd worden. In het
raamwerk is er gekozen voor het combineren van de conflictdetectie en synchronisatie, en
wordt enkel de eerste methode geımplementeerd. De prestatieverschillen met de tweede
methode zullen voor aanzienlijke speculatieve uitvoeringen minimaal zijn vanwege het
kleine aandeel van de synchronisatie in de prestatie.
3.1.3 Analyse
Hier worden de theoretische voor en nadelen van werken met gedeelde data en werken
met lokale data kort vergeleken. Praktijkresultaten volgen in het volgende hoofdstuk.
21
Het werken met gedeelde data op de originele geheugenlocaties heeft als voordeel
dat er in geval van correcte speculatie geen synchronisatie nodig is. Nadeel is dat er
meer overhead is bij het uitvoeren van lees en schrijf operaties, omdat er locks geno-
men moeten worden op de geheugenlocaties. Bij het werken met een undo-log moet
bovendien nog elke keer de vorige inhoud gekopieerd worden, wat dus telkens voor twee
schrijfoperaties zorgt in plaats van een. Bovendien moet bij elke operatie het tijdstip
bijgehouden worden. Dit zorgt ervoor dat voor elke lees- of schrijfoperatie het specu-
latieve raamwerk opgeroepen moet worden. Bovendien is het herstellen van conflicten
weinig efficient: bij het werken met een undo-log moeten heel wat operaties uitgevoerd
worden om terug te keren naar een consistente status, terwijl bij het werken met een
kopie van het origineel teruggekeerd moet worden naar het begin van de speculatieve
uitvoering. Bij het werken met een undo-log kan het geheugengebruik ook hoog oplopen
wanneer er veel schrijfoperaties plaatsvinden.
Bij het werken op lokale data is er een stuk minder overhead tijdens de uitvoering:
enkel bij de eerste schrijfoperatie naar een adres moet de data gekopieerd worden en de
operatie geregistreerd worden, bij daaropvolgende operaties is er geen overhead meer
nodig. Ook het herstellen van conflicten kan efficienter: er kan gesynchroniseerd worden
tot voor code van de draad waar het conflict optrad, alle data van daaropvolgende
draden kan simpelweg genegeerd worden en de uitvoering kan vanaf de conflictdraad
hervat worden. Een nadeel van werken met lokale kopieen van de data is dat er na de
uitvoering gesynchroniseerd moet worden, en dat er bij het werken met veel draden veel
geheugen vereist is omdat elke draad een eigen kopie van zijn geschreven data heeft.
Verder kan nog opgemerkt worden dat speculatieve uitvoering met lokale data in
meer gevallen toepasbaar is dan met gedeelde data, vanwege het elimineren van write
after read en write after write afhankelijkheden.
3.2 Granulariteit
Een belangrijke vraag is hoe groot de verschillende geheugenlocaties zijn die we be-
schouwen. Als er bijvoorbeeld een datastructuur met verschillende elementen gebruikt
wordt, kunnen we elk element als een afzonderlijke locatie beschouwen, of de data-
22
Figuur 3.6: Conflicten detecteren met grote granulariteit
structuur op zijn geheel als 1 geheugenlocatie beschouwen. Beide methodes hebben in
verschillende situaties hun voor en nadelen.
Beschouwen we de datastructuur als 1 geheel, dan is er minder overhead bij het
uitvoeren van de operaties, omdat we operaties naar de verschillende elementen kunnen
beschouwen als 1 geheel, met de overhead van 1 operatie, zoals geıllustreerd in figuur
3.6. Dit veroorzaakt echter een minder nauwkeurige conflictdetectie, wat kan leiden
tot het detecteren van zogenaamde valse conflicten: er wordt een conflict gedetecteerd
waar er geen is, zoals geıllustreerd wordt in het voorbeeld dat volgt.
Beschouwen we de elementen van de datastructuur als afzonderlijke geheugenlo-
caties, dan hebben we voor het gebruik van elk van de elementen een overhead en
afzonderlijke controledata, zoals geıllustreerd in figuur 3.7. Het gevolg is wel dat we
de detectie van valse conflicten ten gevolge van operaties naar verschillende elementen
elimineren.
Voorbeeld Beschouw het volgende voorbeeld: draad 1 schrijft naar een element van
een array a, terwijl draad 2 een ander element van die array leest. Als we de da-
tastructuur als 1 adres beschouwen, zoals in figuur 3.8, dan wordt er een vals conflict
23
Figuur 3.7: Conflicten detecteren met fijne granulariteit
gedetecteerd, omdat beide draden hetzelfde adres, namelijk de plaats van datastructuur
a, lijken te lezen en schrijven. Het gevolg is dat de speculatieve uitvoering onterecht
gestopt zal worden en naar een vroeger punt teruggekeerd zal worden, waardoor er
heel wat tijd verloren gaat. Beschouwen we de elementen van a als afzonderlijke adres-
sen, zoals in figuur 3.9, dan wordt er geen conflict gedetecteerd en wordt er wel verder
uitgevoerd.
24
Figuur 3.8: Conflicten detecteren met grote granulariteit
Figuur 3.9: Conflicten detecteren met fijne granulariteit
25
Hoofdstuk 4
Het ontworpen raamwerk
4.1 Interface
Als voorbeeld beschouwen we volgend codefragment met een lus die we speculatief
willen uitvoeren:
int x[1000], *y, z, i;
...
for(i=0; i < 1000; i++) {
x[i] = *y + z;
}
We hebben een array x, een onbekende pointer y die naar een element uit x kan wij-
zen, en een variabele z. Als y naar een element uit x wijst, dan is er een afhankelijkheid
tussen de iteratie die dat element schrijft en alle daarop volgende iteraties. Dit is dus
een lus die we speculatief kunnen parallelliseren.
De luscode wordt in een nieuwe functie geplaatst en aangepast voor de speculatieve
uitvoering. Deze functie zal worden opgroepen door het speculatieve raamwerk, en
krijgt als parameter een structuur met controledata gebruikt door het raamwerk, en
een startwaarde is en een eindwaarde ie van de iteratievariabele. De functie moet
de iteraties van is tot en met ie - 1 uitvoeren. Om zinloze iteraties te vermijden
26
wordt er bij elke iteratie een controlevariabele gecontroleerd die aangeeft of er in een
eerdere draad reeds een conflict gevonden is of een breakstatement in de lus uitgevoerd
is waardoor de iteraties van deze draad ongeldig zijn. Een break statement in de code
wordt vervangen door het aanroepen van de stop functie van het raamwerk, die zal de
nodige controlevariabelen instellen om ervoor te zorgen dat eerdere draden hun iteraties
nog uitvoeren en latere draden stoppen. Er zijn verschillende manieren om de code aan
te passen met behulp van de functies van het raamwerk, gedefineerd door de volgende
interface:
void write(params_struct* params, void* address, void* data, int size);
int read(params_struct* params, int* address, int size);
void* getReadOnlyPtr(params_struct* params, void* address, int size);
void* getWriteFirstPtr(params_struct* params, void* address, int size);
void* getReadAndWritePtr(params_struct* params, void* address, int size);
We kunnen de lees- en schrijfoperaties omleiden langs het raamwerk door het aan-
roepen van de read of write functie met de nodige parameters. Bij elke operatie geven
we de controlestructuur mee, het (originele) geheugenadres van de operatie, de grootte
van de geheugenregio waarop de operatie betrekking heeft, en in geval van een schrijf-
operatie de weg te schrijven data. De speculatieve luscode ziet er dan als volgt uit:
params_struct* specloop(params_struct* params, int is, int ie) {
int tmp;
for (int i = is; i<ie && *params->global_brk_thread > params->id;
i += params->ss) {
tmp = read(params, yptr, sizeof(*y)) + z;
write(params, x[i], &tmp, sizeof(x[i]));
}
}
Om de overhead te beperken kunnen we echter ook een pointer opvragen aan ons
raamwerk naar die adressen, die we dan bij meerdere lees- en/of schrijfoperaties kunnen
27
gebruiken. Dit kunnen we doen met behulp van de ptr-functies van het raamwerk. Er
zijn verschillende types pointers voorzien, die het raamwerk helpen bij het beperken van
de mogelijke conflicten. Als een pointer enkel gebruikt wordt om het adres te lezen, dient
de getReadOnlyPtr methode gebruikt te worden, zo wordt aan het raamwerk duidelijk
gemaakt dat de pointer niet voor schrijfoperaties gebruikt zal worden. Wordt het adres
geschreven, waarna eventueel de weggeschreven data terug gelezen wordt, dan dient de
getWriteFirstPtr methode gebruikt te worden. Voor toegangspatronen die daar niet
aan voldoen is er de getReadAndWritePtr methode, dit zijn dus adressen die zowel
gelezen als geschreven worden, met als eerste een leesoperatie. Er mogen meermaals
verschillende types pointers opgevraagd worden naar dezelfde adressen, het raamwerk
zal ervoor zorgen dat ze naar dezelfde lokale kopie wijzen om een consistente uitvoering
binnen eenzelfde draad te garanderen. De enige vereiste die voldaan moet worden is
dat de pointers enkel gebruikt worden volgens de vereisten van de functie waarmee
ze opgevraagd zijn. Wordt een pointer opgevraagd met de getReadOnlyPtr functie
bijvoorbeeld ook gebruikt voor schrijfoperaties, dan kunnen er conflicten ontstaan die
het raamwerk niet zal detecteren.
In principe kan men voor elk adres een pointer opvragen met de getReadAndWritePtr
methode om zeker geen restricties te schenden, maar dit zal leiden tot het detecteren
van heel wat valse conflicten, omdat alle gebruikte adressen dan als zowel gelezen als
geschreven beschouwd worden. Zowel voor de efficientie als voor de correcte werking
van het raamwerk dient dus met de juiste pointers gewerkt te worden. Gezien het
raamwerk de bedoeling heeft om door een compiler gebruikt te worden, en die laatste
de juiste functie-oproepen automatisch toevoegd, zal dit geen problemen opleveren. De
functie met de speculatieve luscode ziet er dan als volgt uit:
params_struct* specloop(params_struct* params, int is, int ie) {
for (int i = is; i<ie && *params->global_brk_thread > params->id;
i += params->ss) {
xptr = getWriteFirstPtr(params, x[i], sizeof(*x));
yptr = getReadOnlyPtr(params, y, sizeof(*y));
*xptr = *yptr + z;
}
28
}
De pointerfuncties laten toe de granulariteit van operaties te vergroten om zo het
aantal oproepen naar het raamwerk nog te verkleinen, en dus de overhead te beperken.
Zeker in geval van lussen waar over alle elementen geıtereerd wordt komt dit van pas,
omdat zo heel veel oproepen geelimineerd kunnen worden, en door het feit dat elk ele-
ment gebruikt wordt leidt het niet tot de detectie van valse conflicten. In ons voorbeeld
hebben we zo’n situatie: we kunnen het opvragen van de verschillende gebruikte ele-
menten van de x array groeperen en in een keer opvragen buiten de lus. Ook kunnen we
het opvragen van de pointer naar de y variabele verplaatsen naar buiten de lus, zodat
dat maar een keer gedaan wordt:
params_struct* specloop(params_struct* params, int is, int ie) {
xptr = getWriteFirstPtr(params, x[is], sizeof(*x) * (ie - is)
/ params->ss);
yptr = getReadOnlyPtr(params, y, sizeof(*y));
for (int i = is; i<ie && *params->global_brk_thread > params->id;
i += params->ss) {
*xptr = *yptr + z;
}
}
De pointerfuncties zijn alleen compatibel met het werken met lokale kopieen in het
raamwerk, omdat er dan enkel geweten moet zijn welke adressen gelezen en geschreven
worden, zonder het tijdstip van elke operatie te kennen. Bij het werken met gedeelde
data is dit niet het geval, dan moet het raamwerk wel elke operatie registreren, waardoor
elke geheugentoegang dus naar de read of write functie van het raamwerk moet worden
omgeleid.
Het oproepen van het raamwerk hoeft niet voor alle geheugentoegangen, enkel voor
locaties die mogelijk afhankelijkheden tussen de iteraties introduceren. In het voorbeeld
zijn dat zowel de array als het adres waar de y pointer naar wijst. De z variabele wordt
met zekerheid in geen enkele iteratie geschreven dus kan die geen conflict opleveren en
mag dus gewoon gebruikt worden.
29
Op de plaats waar de originele lus stond doen we een oproep van de execute_loop
functie van het raamwerk, met als parameters de startwaarde, eindwaarde, en stap-
grootte van de iteratievariabele, de gewenste intervalgrootte tussen synchronisaties, het
aantal gewenste speculatieve draden, en een functiepointer naar de speculatieve luscode.
In dit geval dus:
execute_loop(0,1000,1,1000,2,&specloop);
Deze functie garandeert een correcte uitvoering van de code, dus na deze oproep
kan verdergegaan worden met het verder uitvoeren van het programma.
4.2 Interne werking
In deze sectie bekijken we de interne werking van het raamwerk om een zicht te krijgen
op de overhead die het met zich meebrengt. We bekijken afzonderlijk de overhead van
het werken met gedeelde data en van het werken met lokale data. Voor het aanmaken
en opstarten van draden maakt het raamwerk gebruik van POSIX threads. [8]
4.2.1 Gedeelde data
Controledata
Er wordt een map met locks voor verschillende adressen gedeeld tussen de draden.
Telkens een nieuw adres gebruikt wordt, wordt er een nieuwe lock toegevoegd. De locks
worden genomen wanneer er operaties met dat adres uitgevoerd worden.
Elke draad bezit een map met voor elk gebruikt adres informatie over het tijdstip
van schrijf en leesoperaties, en een kopie van de originele data indien deze draad de
eerste was die het adres geschreven heeft.
30
Geheugenoperaties
De read functie
1. Er wordt bepaald of er in de controlestructuur al een element zit voor dit adres,
met andere woorden of dit de eerste keer is dat deze draad dat adres gebruikt.
2. Is er nog geen controledata, dan wordt die aangemaakt en wordt er gecontroleerd
of er al een lock voor dit adres bestaat.
3. Bestaat er nog geen lock voor het adres, dan betekent dit dat dit de allereerste
toegang naar dit adres is door alle draden samen. Het adres bevat dus nog de ori-
ginele waarde van voor de speculatieve uitvoering, deze waarde wordt gekopieerd
naar de controledata. Er wordt ook een lock aangemaakt voor dat adres, die door
de verschillende draden gedeeld wordt.
4. Er wordt een lock op het adres genomen, de data wordt uit het adres gelezen, en
het tijdstip wordt vastgelegd, waarna de lock weer vrijgegeven wordt.
5. Het tijdstip wordt bewaard in het last read attribuut van de controlestructuur,
en in het first read attribuut als die nog geen tijdstip bevat, aangezien dit dan de
eerste leesoperatie naar dit adres is. Op die manier wordt verzekerd dat voor elk
adres waarvan deze draad gelezen heeft, de tijdstippen van zowel de eerste als de
laatste leesoperatie beschikbaar zijn.
6. De gelezen data wordt teruggeven als returnwaarde.
De write functie
1. De write operatie controleert of er in de controlestructuur al een element zit voor
dit adres.
2. Is er nog geen element, dan wordt dat gealloceert en geınitialiseerd, en wordt er
gecontroleerd of er al een lock voor dit adres bestaat.
31
3. Bestaat er nog geen lock voor het adres, dan betekent dit dat dit de allereerste
toegang naar dit adres is door alle draden samen. Het adres bevat dus nog de ori-
ginele waarde van voor de speculatieve uitvoering, deze waarde wordt gekopieerd
naar de controledata. Er wordt ook een lock aangemaakt voor dat adres, die door
de verschillende draden gedeeld wordt.
4. De bij het adres horende lock wordt genomen, de data wordt naar het adres
geschreven, en het tijdstip wordt vastgelegd, waarna de lock weer vrijgegeven
wordt.
5. Het tijdstip wordt bewaard in het last write attribuut van de controlestructuur,
en in het first write attribuut als die nog geen waarde bevat, aangezien dit dan
de eerste schrijfoperatie naar dit adres is. Op die manier wordt verzekerd dat
voor elk adres waarnaar deze draad geschreven heeft, de tijdstippen van zowel de
eerste als de laatste schrijfoperatie beschikbaar zijn.
Het vastgelegde tijdstip is geen absolute tijd maar logische tijd. Er wordt een teller
gedeeld tussen de draden. Het bepalen van het tijdstip van een operatie gebeurt door die
teller te verhogen en de waarde op te slaan bij elke operatie, in een atomische operatie.
Zo kunnen we met absolute zekerhied de volgorde van de operaties achterhalen.
Bij het werken op gedeelde data kunnen de functies voor het opvragen van poin-
ters aan het raamwerk niet geımplementeerd worden. Het raamwerk moet immers het
tijdstip van elke lees en schrijfopertie kunnen vastleggen, waarvoor bij elke operatie het
raamwerk gecontacteerd moet worden. Bij het werken met pointers is dit niet het geval,
en voert de speculatieve code operaties uit zonder dat het raamwerk weet wanneer die
precies gebeuren.
Synchronisatie van de draden
Elke draad voert zijn toegekend aantal iteraties uit van de speculatieve loopcode door
het aanroepen van de speculatieve loopfunctie met de start en stop iteratie.
32
Draad 0:
1. De draad geeft zijn map met gebruikte adressen en bijhorende controledata door
aan draad 1.
2. De draad wacht op het voltooien van de synchronisatie door de andere draden.
Andere draden:
1. Er wordt gewacht op de sychronisatiedata van de vorige draad.
2. Er wordt gecontroleerd of synchronisatie nog nodig is. Wanneer er door een
lagere draad reeds een conflict is gedetecteerd, heeft dit geen zin meer. In dat
geval worden de originele waarden van geheugenlocaties die deze draad als eerste
gebruikt heeft hersteld.
3. Is er wel synchronisatie nodig, dan wordt naar conflicten gezocht door het verge-
lijken van de eigen controledata met de data van vorige draden, en wordt de eigen
data daaraan toegevoegd voor de volgende draden.
4. De volgende draad wordt wakker gemaakt en er wordt doorgegeven of er al dan
niet een conflict gedetecteer is.
4.2.2 Lokale kopieen
Controledata
Informatie over geschreven adressen en hun lokale kopieen wordt bijgehouden in een set
die instanties bevat van de controlestructuur. Die houdt bij wat het orginele adres is,
wat het adres van de lokale kopie is, en hoe groot de variabele is.
Er wordt ook een set bijgehouden van adresregio’s die door de draad gelezen zijn
vooraleer ze geschreven werden.
Er is een synchronisatieset die gedeeld wordt tussen de draden, om hun controle-
structuren van geschreven locaties door te geven.
33
Geheugenoperaties
De getReadOnlyPtr functie: figuur 4.1
1. Er wordt bepaald of de gevraagde regio reeds lokaal bestaat.
2. Is dit het geval, dan wordt er berekend waar in de lokale data zich de kopie precies
bevindt, en wordt een pointer naar dat adres teruggegeven.
3. Is dit nog niet het geval, dan wordt er controledata geınitialiseerd om deze geheu-
gentoegang op te slaan als in de lijst van gelezen adressen die mogelijk een read
after write afhankelijkheid met een vorige draad kunnen schenden.
4. Er wordt gekeken of er reeds lokale kopieen bestaan van delen van de gevraagde
regio. Als die er zijn, dan wordt er nieuwe ruimte gealloceerd voor de volledige
gevraagde regio, en wordt de inhoud van de partiele lokale kopieen naar de juiste
plaats erin gekopieerd alvorens die te verwijderen. Dit is noodzakelijk omdat de
recentste versie van de hele gevraagde regio waarnaar een pointer wordt gegeven
contigu in het geheugen moet zitten, om pointerberekeningen binnen die regio toe
te laten. Het adres wordt bovendien geregistreerd als gelezen alvorens geschreven
te zijn, en dus mogelijk een afhankelijkheid schendend met een schrijfoperatie
ernaar in een vorige draad, vanwege de stukken eruit waarvan nog geen lokale kopie
bestond. Er wordt een pointer naar het begin van de nieuwe kopie teruggegeven.
5. Bestaan er nog geen pariele kopieen, dan is de regio in deze draad nog niet gewij-
zigd, en wordt er een pointer teruggegeven naar het originele adres vanwaar de
data gelezen mag worden. Ook hierbij wordt het adres geregisteerd als gelezen.
Deze functie bevat heel wat code, maar het grootste codeblok, blok 4, moet slechts
uitgevoerd worden wanneer er eerder al kleinere stukken van de opgevraagde regio
geschreven zijn. Dit komt niet voor wanneer er met een vaste granulariteit gewerkt
wordt, en het moet maximaal 1 keer uitgevoerd worden voor een regio, want er wordt
een grote regio van gemaakt die de volgende keer onmiddelijk teruggegeven kan worden.
34
Figuur 4.1: Control flow van de getReadOnlyPtr functie bij het werken met lokale
kopieen
35
Figuur 4.2: Control flow van de getWriteFirstPtr functie bij het werken met lokale
kopieen
36
De getWriteFirstPtr functie: figuur 4.2
1. Er wordt bepaald of er reeds een lokale kopie bestaat van een regio waarin de
gevraagde regio volledig valt.
2. Bestaat die, dan wordt er berekend waar de gevraagde regio begint, en wordt er
een pointer naar teruggegeven.
3. Bestaat die niet, dan wordt die samen met de nodige controledata aangemaakt,
en worden reeds bestaande partiele kopieen van de regio verwijderd. De locatie
wordt geregistreerd als geschreven, en dus een afhankelijkheid creerend indien een
later draad dit adres leest. Een pointer naar de lokale kopie wordt teruggegeven.
Ook hier geldt dat het grootste codestuk maximaal een keer uitgevoerd wordt, en
enkel wanneer er met een niet-consistente granulariteit gewerkt wordt.
De getReadAndWritePtr functie: figuur 4.3
1. Er wordt gezocht naar een lokale kopie van een regio waarin het gevraagde adres
valt.
2. Bestaat er reeds een lokale kopie, dan wordt er een pointer daarnaar teruggegeven.
3. Bestaat er nog geen lokale kopie, dan wordt die aangemaakt
4. Bestaan er reeds lokale kopieen van stukken van de opgevraagde regio, dan worden
die naar de nieuwe grote kopie gekopieerd en zelf verwijderd
5. De regio wordt geregistreerd als gelezen, en dus mogelijk afhankelijk van schrijf-
operaties in voorgaande draden, en ook als geschreven, en dus mogelijk een af-
hankelijkheid creerend met leesoperaties in volgende draden.
Ook in deze functie wordt het grootste codefragment maximum een keer uitgevoerd
voor een bepaalde regio, bij daaropvolgende oproepen wordt enkel code in blok 1 en 2
uitgevoerd.
37
Figuur 4.3: Control flow van de getReadAndWritePtr functie bij het werken met lokale
kopieen
38
Synchronisatie van de draden
Elke draad voert zijn iteraties uit door het aanroepen van de speculatieve lusfunctie
met de start en stop iteratie.
Draad 0:
1. De set met geschreven adressen wordt doorgegeven aan de volgende draad.
2. Draad 1 wordt wakker gemaakt om te synchroniseren.
3. Er wordt gewacht tot de synchronisatiedata, een set met alle geschreven locaties
en het adres van de actueelste kopie, door de laatste draad teruggegeven wordt.
4. De data in de lokale kopieen wordt gekopieerd naar de originel geheugenlocaties.
5. Er wordt gecontroleerd of er nog intervallen uitgevoerd moeten worden en hogere
draden worden daarover ingelicht.
Andere draden:
1. De draad wacht op de set met controledata van de geschreven adressen van de
vorige draden.
2. Er wordt gecontroleerd of synchronisatie van deze draad nodig is.
3. Is er synchronisatie nodig, dan worden read after write afhankelijkheden gedetec-
teerd, de enige soort afhankelijkheden die kunnen voorkomen bij het werken met
lokale kopieen. Dit gebeurt door het vergelijken van de adressen die door vorige
draden geschreven zijn met een lijst van adressen die deze draad gelezen heeft
voordat hij er zelf naar geschreven heeft.
4. Wordt er een conflict gedetecteerd, dan wordt aan de volgende draden doorgegeven
dat die niet moeten synchroniseren, en wordt draad 0 wakker gemaakt om de
synchronisatie tot en met de vorige draad te voltooien.
5. Waren er geen conflicten, dan voegt de draad de controledata van zijn geschreven
adressen toe aan de synchronisatieset, waarbij hij de data voor dezelfde adressen
geschreven door vorige draden overschrijft.
39
6. Er wordt getest of er een break statement uitgevoerd is in de lus, met andere
woorden of de lus hier onverwacht beeindigd werd. Is dit het geval, dan wordt
doorgegeven aan de volgende draden om niet meer te synchroniseren en wordt
draad 0 verwittigd om de synchronisatie tot en met deze draad te voltooien.
7. De volgende draad wordt wakker gemaakt om te synchroniseren.
8. De draad wacht tot draad 0 aangeeft of er een volgend interval uitgevoerd moet
worden.
4.2.3 Beperkingen
De versie met gedeelde data kan niet overweg met overlappende geheugenregio’s. Er
moet dus voor een bepaalde locatie altijd met dezelfde granulariteit gewerkt worden.
Wanneer rekening gehouden moet worden met overlappingen, wordt het opzoeken van
de controledata voor een bepaalde locatie een stuk ingewikkelder, wat voor een stuk
meer overhead zorgt. Aangezien dit in de versie met gedeelde data bij elke operatie
moet gebeuren, lijkt dit niet aangewezen. Elke operatie naar een bepaalde locatie in
het geheugen moet dus hetzelfde begin- en eindadres hebben.
De versie die werkt op lokale data kan wel overweg met varierende granulariteit, op
voorwaarde dat een opgevraagde regio niet deels binnen en deels buiten een andere regio
valt. Een opgevraagde regio mag dus wel een stukje zijn van een eerder opgevraagde
grotere regio, of een grotere regio zijn waar een eerder opgevraagde kleinere locatie
volledig binnen valt. Het is echter aan te raden zoveel mogelijk dezelfde granulariteit
voor een bepaalde locatie te gebruiken, omdat het vergroten van de granulariteit leidt
tot het opnieuw alloceren van de nieuwe lokale kopie, en het kopieren van de inhoud
van de kleinere kopieen.
40
Tabel 4.1: De hardwareconfiguratie van de uitgevoerde testen
4.3 Resultaten
De prestatie van het raamwerk werd op volgend codefragment getest:
for(int i=0; i<writes; i++) {
for(int j=0; j<reads; j++) {
*a[i] += b[j] * c[i];
}
}
Er is een array a met pointers naar variabelen waarmee een berekening wordt uitgevoerd,
en waarnaar het resultaat weggeschreven wordt. Door deze pointers te veranderen
kunnen we conflicten genereren. De reads en de writes variabele geven het aantal
iteraties van respectievelijk de buitenste en de binnenste lus aan, en bepalen de grootte
van de arrays. We testen twee versies: een waarbij de arrays statisch gedeclareerd zijn,
en een waarbij ze dynamisch gedeclareerd zijn.
Dit zijn de technische specificaties van de machine waarop alle testen zijn uitgevoerd
is weergegeven in tabel 4.1.
4.3.1 Gedeelde data
Bij het werken met gedeelde data moeten we de read en de write functies van het
raamwerk gebruiken, we krijgen dan de volgende speculatieve code:
41
Figuur 4.4: De uitvoeringstijd zonder conflicten bij het werken met gedeelde data
params_struct* specloop(params_struct* params, int is, int ie) {
int tmp;
for(int i=is; i<ie; i++) {
for(int j=0; j<READS; j++) {
tmp = read(params,a[i], 4) +
read(params,&b[j],4) * read(params,&c[i],4);
write(params,a[i],&tmp,4);
}
}
return params;
}
Resultaat
Het resultaat toont meteen aan dat het werken met gedeelde data veel te traag is.
Het oproepen van het raamwerk voor elke geheugenoperatie levert een veel te grote
42
overhead op. Het aantal uit te voeren instructies per iteratie wordt verveelvuldigd. Die
overhead kan enkel verminderd worden door niet alle operaties te moeten registreren,
en geen locks te moeten nemen bij het uitvoeren van geheugenoperaties. Deze twee
zaken kunnen enkel verwezenlijkt worden door te werken met lokale kopieen van de
data omdat dan het tijdstip van elke operatie niet geweten moet zijn om conflicten
te kunnen detecteren en de data dan zonder locks gelezen en geschreven kan worden
omdat elke draad op een eigen kopie werkt.
4.3.2 Lokale data
Voor het testen van het werken met lokale data kunnen we gebruik maken van de
pointerfuncties. We vertrekken van volgende speculatieve luscode:
params_struct* specloop(params_struct* params, int is, int ie) {
int* bptr = (int*)getReadOnlyPtr(params, b, sizeof(b));
int* cptr = (int*)getReadOnlyPtr(params, &c[is], sizeof(int) * (ie-is));
for(int i=is; i<ie; i++) {
int* aptr = (int*)getReadAndWritePtr(params, (void*)a[i], sizeof(int));
for(int j=0; j<READS; j++) {
*aptr += bptr[j] * cptr[i];
}
}
return params;
}
Hierbij werken we met grotere granulariteit om minder pointers te moeten opvragen.
Het resultaat van de testen met gedeelde data hebben immers uitgewezen dat voor elke
operatie het raamwerk oproepen een veel te grote overhead oplevert. Voor de b array
kan de grotere granulariteit geen valse conflicten opleveren aangezien hij in elke iteratie
volledig gelezen wordt. Voor de c array vragen we enkel een pointer naar het gedeelte
dat in de draad gebruik wordt, dus ook hier levert de grotere granulariteit geen valse
conficten op.
43
Figuur 4.5: De uitvoeringstijd zonder conflicten bij het werken met lokale kopieen van
de versie met statische datastructuren
Parallellisatie van de statische versie
Figuur 4.5 vergelijkt de uitvoeringstijd van de sequentiele uitvoering met statische data
en speculatieve versies met een verschillend aantal draden voor 10000 iteraties van de
buitenste lus en verschillend aantal iteraties van de binnenste lus.
We zien dat er meer tijdswinst geboekt wordt naarmate de binnenste lus groter
wordt, wat logisch is, gezien het relatieve aandeel van de speculatieve overhead dan
verkleint.
Voor een verschillend aantal draden zien we weinig verschil, vanwege het uitvoeren
van de testen op een processor met twee kernen. Met meer dan twee draden kan nog
een kleine tijdswinst geboekt worden door een andere draad uit te voeren wanneer op
het geheugen gewacht moet worden om zo geheugenlatentie te verbergen, maar aan de
andere kant zorgen meerdere draden ook voor meer synchronisatieoverhead.
44
Figuur 4.6: De uitvoeringstijd zonder conflicten bij het werken met lokale kopieen van
de versie met dynamische datastructuren
Parallellisatie van dynamische versie
De uitvoeringstijd van de sequentiele uitvoering met dynamische data en speculatieve
versies met een verschillend aantal draden voor 10000 iteraties van de buitenste lus en
verschillend aantal iteraties van de binnenste lus wordt weergegeven in figuur 4.6.
De tijdswinst is hier groter dan bij de versie met statische data. Dit komt doordat het
raamwerk de lokale kopieen dynamisch alloceert, en dynamisch gealloceerd geheugen
een hogere toegangstijd heeft dan statisch gealloceerd geheugen. Daardoor is er in
de vorige sectie een extra vertraging van de speculatief parallelle code die werkt met
dynamisch gealloceerde kopieen, ten opzichte van de sequentiele code die werkt met
statisch gealloceerde data. In dit geval werkt de sequentiele code ook met dynamisch
geheugen, waardoor die ook die extra vertraging heeft. Ook hier is er geen merkbaar
verschil tussen de verschillende aantallen speculatieve draden, vanwege het uitvoeren
op een processor met twee kernen.
Bij de resterende testen werken we telkens met twee draden en de resultaten zijn
45
Figuur 4.7: De uitvoeringstijd zonder conflicten met twee speculatieve draden voor
verschillende intervalgroottes
gebaseerd op de versie met dynamisch gealloceerde data.
Parallellisatie met intervallen
In de testen hiervoor werd telkens na het voltooien van de lus pas op conflicten gecon-
troleerd en de data gesynchroniseerd. Zoals beschreven in hoofdstuk 3 kan het nuttig
zijn om dit meermaals te doen na een bepaald aantal lusiteraties. Het interval waar-
na gesynchroniseerd moet worden kan meegegeven worden bij het aanroepen van de
execute functie van het raamwerk, en kan dus voor elk stuk speculatieve code anders
gekozen worden. De uitvoeringstijd van de testlus voor verschillende intervalgroottes is
weergegeven in figuur 4.7. We maken hierbij, net zoals in de rest van deze paragraaf,
gebruik van twee speculatieve draden.
We zien dat de uitvoeringstijd voor kleinere intervalgroottes slechts zeer licht toe-
neemt, en de overhead van het tussentijds synchroniseren dus nog meevalt. Dit komt
doordat dit deels gecompenseerd wordt door het feit dat er na elk interval minder
46
Figuur 4.8: De uitvoeringstijd bij het werken met lokale kopieen bij een conflict
synchronisatiewerk is dan bij een grote synchronisatie achteraf, omdat er per interval
minder verschillende adressen gebruikt worden en dus minder lokale kopieen te syn-
chroniseren zijn.
Parallellisatie met conflicten
Figuur 4.8 toont voor verschillende intervalgroottes het resultaat wanneer een specula-
tieve uitvoering met twee draden geconfronteerd wordt met een conflict. Wie zien de
uitvoeringstijd voor drie verschillende conflicten: een in het begin van de uitvoering, een
ongeveer in het midden van de uitvoering, en een in het laatste interval. We zien dat
de uitvoeringstijd afneemt voor kleinere intervalgroottes, maar bij hele kleine groottes,
wanneer er te veel gesynchroniseerd wordt, terug licht toeneemt.
Hieruit blijkt dus dat het werken met relatief kleine intervallen voordelig is: de
bijkomende overhead is klein, conflicten worden sneller gedetecteerd en er moet minder
code opnieuw uitgevoerd worden.
47
4.3.3 Conclusie
Uit de resultaten blijkt dat het werken met lokale kopieen van de data de efficientste
manier is om speculatief code uit te voeren. Het laat toe niet elke operatie te moeten
registreren, en een grotere granulariteit te gebruiken.
Het raamwerk werkt met dynamisch gealloceerde kopieen, waardoor het een extra
vertraging introduceert bij het parallelliseren van programma’s die werken met statisch
gealloceerde data. Daarom is de snelheidswinst groter bij programma’s die werken met
dynamisch gealloceerde data.
Een kleine intervalgrootte blijkt voordelig te zijn. Tussentijdse synchronisaties le-
veren geen al te grote overhead op, maar zorgen wel voor een snellere conflictdetectie.
48
Hoofdstuk 5
Parallellisatie van de mcf
benchmark
5.1 MCF benchmark
De 429.mcf benchmark is afgeleid van een programma voor het plannen van busroutes.
Het berekent een minimale kost scenario op basis van een gewogen graaf.[9]
De belangrijkste functie van deze benchmark is de primal_net_simplex functie,
die verantwoordelijk is voor 54.7% van de uitvoeringstijd. Deze wordt schematisch
weergegeven in figuur 5.1. Daar wordt in een lus het gekozen schema geoptimaliseerd
tot er een optimum bereikt is (deze iteraties noemen we het a-blok), en elke 200 iteraties
wordt bovendien het potentieel van de knopen van de graaf herberekend (deze code
noemen we het b-blok). Het herberekende potenieel uit het b-blok wordt in de knopen
van de graaf opgeslagen, maar blijkt telkens uit louter silent stores te bestaan. Silent
stores zijn schrijfoperaties die data wegschrijven die gelijk is aan de data die reeds op
die plaats in het geheugen zat, en dus eigenlijk niets veranderen.[10] Daardoor kunnen
we het b-blok parallel met het daaropvolgende a-blok uitvoeren. Het a-blok verandert
echter wel zaken aan de data, waardoor die veranderingen gescheiden moeten worden
van de originele data zodat die de uitvoering van het b-blok, dat op de data van het
vorige a-blok werkt, niet beınvloedt. Dit kunnen we gebruiken om de overhead van
49
ons speculatieve raamwerk te testen bij het parallel uitvoeren van verschillende stukken
code, het werken met een lokale kopie van de data, en het synchroniseren daarvan met
de originele data.
5.2 Parallellisatie
Het a-blok en het b-blok worden beiden in een nieuwe functie geplaatst. De variabelen
die geınitialiseerd worden voor de lus en afhankelijkheden hebben tussen verschillende
iteraties, worden in een afzonderlijke structuur buiten de functie geplaatst, zodat die
bereikbaar zijn vanuit de nieuw aangemaakte functies.
5.2.1 Het a-blok
Het a-blok wordt in een functie spec_a geplaatst, waar het wordt aangepast om alle
lees- en schrijfoperaties naar knopen van de graaf om te leiden naar het speculatieve
raamwerk. We maken daarvoor gebruik van de hulpklasse node_t_ptr, die alle operaties
die op variabelen van het node_t type worden toegepast implementeert met de nodige
functieoproepen naar het raamwerk. Van alle functies die worden opgeroepen vanuit het
a-blok worden speculatieve versies aangemaakt waarbij de node_t variabelen veranderd
worden in node_t_ptr variabelen.
Bij het aanmaken van een node_t_ptr, of het veranderen van de node waar hij
naar wijst, wordt door de hulpklasse aan het raamwerk een nieuwe pointer naar de
bijhorende lokale kopie opgevraagd met de functie getReadAndWritePtr, waarna die
pointer bewaard wordt en gebruikt wordt in de rest van de speculatieve uitvoering.
5.2.2 Het b-blok
Het b-blok in de functie spec_refresh_potential kan vrijwel onaangepast blijven,
enkel op de plaatsen waar het potentieel van een knoop weggeschreven wordt, moet
gecontroleerd worden of het om silent stores gaat.
50
Figuur 5.1: De originele primal net simplex functie
51
5.2.3 De primal net simplex functie
Op de plaats waar de oorspronkelijke luscode stond wordt er een array aangemaakt met
pointers naar de twee speculatieve functies. Het a-blok wordt eerst 1 keer afzonderlijk
uitgevoerd, daarna wordt de execute_code functie van het raamwerk aangeroepen met
de array van pointers. Het raamwerk zal er voor zorgen dat het b-blok telkens parallel
uitgevoerd wordt met het volgende a-blok, tot de functie eindigt, wat betekent dat de
lus voltooid is. Daarna eindigt de primal_net_simplex functie onveranderd.
5.2.4 Resultaat
De benchmark is nu speculatief geparallelliseerd met zo weinig mogelijk ingrepen of
specifieke optimalisaties, wat de bedoeling is bij een proces dat automatiseerbaar moet
zijn. Het resultaat laat echter te wensen over, de uitvoeringstijd neemt maar liefst
toe met een factor 7 bij het uitvoeren van de train input. Welke functie hoeveel tijd
in beslag neemt voor het uitvoeren van de train input wordt weergegeven in tabel
5.1. We zien hier duidelijk een enorme overhead van de getReadAndWritePtr functie
van het raamwerk, die 72% van de uitvoeringstijd in beslag neemt, en we zien dat de
speculatieve uitvoering van het b-blok in de spec_refresh_potential functie niet veel
tijd in beslag neemt.
In de volgende sectie beschrijven we aanpassingen om de uitvoeringstijd te verbete-
ren.
5.3 Optimalisaties in de speculatieve uitvoering
Uit voorgaand resultaat is duidelijk dat enkel de noodzakelijke functie oproepen in-
voeren voor het gebruik van speculatief raamwerk niet volstaat om snelheidswinst te
boeken met het parallelliseren van deze code. Er zijn verbeteringen nodig. We proberen
de prestatie te verbeteren door de speculatieve uitvoering wat aan te passen.
Deze versie had met zeer veel onnodige overhead te kampen. Zoals we in de resul-
taten kunnen zien neemt de speculatieve b-functie weinig tijd in beslag. Dit betekent
52
Tabel 5.1: Tijdsverdeling tussen de belangrijkste methodes
dat na voltooien van elk b-blok, het a-blok nog lange tijd aan het werk is, en dat op een
speculatieve manier doet terwijl het de enige actieve draad is. Dit betreft eigenlijk het
overgrote deel van de uitvoering van het a-blok. Het volgende b-blok starten kunnen
we nog niet, omdat die afhankelijk is van de wijzigingen die het nog lopende a-blok
aanbrengt. We kunnen er wel het a-blok op sequentiele code laten overschakelen wan-
neer het b-blok voltooit, om zo vele nutteloze oproepen van de getReadAndWritePtr
te vermijden. Daarom hebben we een synchronisatievlag aangemaakt, die na het uit-
voeren van het b-blok ingesteld wordt, en bij elke iteratie van het a-blok gecontroleerd
wordt. Wanneer die voldaan is, wordt de data van het a-blok gesynchroniseerd door het
raamwerk, dus worden de waarden van de lokale kopiees naar de oorspronkelijke loca-
ties gekopieerd, en wordt er verder sequentiele code uitgevoerd. Wanneer het volgende
b-blok kan beginnen wordt dan weer overgeschakeld op speculatieve code. De control
flow van de speculatieve code ziet er dan uit als in figuur 5.2.
Om de overhead van de getReadAndWritePtr functie nog wat verder te beperken
vereenvoudigen we de code. Aangezien er altijd pointers naar knopen opgevraagd wor-
den aan het raamwerk, die altijd dezelfde grootte hebben, hebben we niet te maken met
overlappende adresregio’s, en kunnen we dus het zoeken naar een lokale kopie baseren
op enkel het beginadres. Verder gebruiken we nu ook een boom-structuur voor het
opzoeken van geheugenlocaties, in plaats van een set.
53
Figuur 5.2: Control flow van de speculatieve primal net simplex functie
54
5.4 Resultaat
Ook nu loopt de benchmark voor de train input nog zo’n 4% trager dan het origineel.
Het resultaat is nog steeds slecht, maar wel een grote verbetering ten opzichte van het
vorige resultaat.
Hieruit moeten we echter concluderen dat er codetransformaties en optimalisaties
noodzakelijk zijn voor het creeren van code die speculatief parallel kan presteren. Het
simpelweg gebruiken van een speculatief raamwerk volstaat niet. In het voorbeeld
van de mcf benchmark zit de geparallelliseerde code vol pointeroperaties, waarbij vaak
geıtereeerd wordt over zeer veel elementen, met als gevolg zeer veel functieoproepen
naar het raamwerk, voor zeer veel verschillende adressen die allemaal lokaal gekopieerd
moeten worden. Zonder aanpassingen aan de originele mcf code kan dat niet op een
winstgevende manier speculatief geparallelliseerd worden.
De uitvoeringstijd van de benchmark zonder het b-blok bedraagt voor de train
input 89% van de originele uitvoeringstijd. Dit is dus de maximale snelheidswinst die
we zouden kunnen verkijgen bij een parallellisatie van het a-blok en het b-blok zonder
overhead. Bij de speculatieve uitvoering hebben we dus een extra overhead van 15%.
De resultaten in hoofdstuk 4 hebben aangetoond dat het raamwerk wel goed kan
presteren. Er is verder onderzoek vereist naar hoe lussen getransformeerd moeten wor-
den om die snelheidswinst te kunnen behalen.
55
Hoofdstuk 6
Besluit
In deze masterproef hebben we het ontwerp van een softwarematig speculatief raamwerk
besproken en de mogelijkheden getest. Eerst werd er ingegaan op het waarom van
een softwarematig raamwerk voor speculatieve parallellisatie, en gekeken naar ander
wetenschappelijk werk rond deze kwestie.
Vervolgens werden hoofdzakelijk twee technieken besporken om zo’n raamwerk te
laten functioneren, namelijk het werken op gedeelde data en het werken op lokale ko-
pieen van de data. Daarna werden die technieken geımplementeerd en getest, waaruit
bleek dat het werken op lokale kopieen veruit de beste resultaten opleverd.
Het raamwerk werd ook uitgetest op de mcf-benchmark uit de SPEC CPU 2006
bibliotheek, waar het echter weinig resultaat op behaalde. Dit toonde aan dat het
raamwerk voor het parallelliseren van complexe code met veel pointeroperaties gecom-
bineerd moet worden met code transformaties en optimalisaties om de code geschikter
te maken voor speculatieve parallellisatie.
56
Bibliografie
[1] H. Zhong, M. Mehrara, S. Lieberman, and S. Mahlke. Uncovering hidden loop
level parallelism in sequential applications. In 14th International Symposium on
High Performance Computer Architecture, pages 290–301. IEEE Computer Society,
2008.
[2] M. Bridges, N. Vachharajani, Y. Zhang, T. Jablin, and D. August. Revisiting the
sequential programming model for multi-core. In Proceedings of the 40th Annual
IEEE/ACM International Symposium on Microarchitecture, pages 69–84. IEEE
Computer Society, 2007.
[3] S. Balakrishnan and G. Sohi. Program demultiplexing: Data-flow based specula-
tive parallelization of methods in sequential programs. In Proceedings of the 33rd
annual international symposium on Computer Architecture, pages 302 – 313. IEEE
Computer Society, 2006.
[4] P. Papadimitriou and T. Mowry. Exploring thread-level speculation in software:
The effects of memory access tracking granularity. Technical report, 2001.
[5] M. Prabhu and K. Olukotun. Exposing speculative thread parallelism in spec2000.
In Proceedings of the tenth ACM SIGPLAN symposium on Principles and practice
of parallel programming, pages 142 – 152. ACM, 2005.
[6] Peter Rundberg and Per Stenstrom. An all-software thread-level data dependence
speculation system for multiprocessors. Journal of Instruction-Level Parallelism,
3:2002, 2001.
57
[7] C. Oancea and A. Mycroft. Software thread-level speculation: an optimistic library
implementation. In Proceedings of the 1st international workshop on Multicore
software engineering, pages 23–32, 2008.
[8] David Butenhof. Programming with POSIX Threads. Addison-Wesley, 1997.
[9] Standard Performance Evaluation Corporation. Spec cpu 2006.
http://www.spec.org/cpu2006/.
[10] K. Lepak, G. Bell, and M. Lipasti. Silent stores and store value locality. IEEE
Transactions on Computers, 50:1174 – 1190, 2001.
58
Recommended