coarse-grained speculative parallelism and optimization

191
Coarse-grained Speculative Parallelism and Optimization by Kirk Kelsey Submitted in Partial Fulfillment of the Requirements for the Degree Doctor of Philosophy Supervised by Dr. Chen Ding Department of Computer Science Arts, Sciences and Engineering Edmund A. Hajim School of Engineering and Applied Sciences University of Rochester Rochester, New York 2011

Upload: khangminh22

Post on 12-Jan-2023

0 views

Category:

Documents


0 download

TRANSCRIPT

Coarse-grained Speculative Parallelismand Optimization

by

Kirk Kelsey

Submitted in Partial Fulfillment

of the

Requirements for the Degree

Doctor of Philosophy

Supervised by

Dr. Chen Ding

Department of Computer ScienceArts, Sciences and Engineering

Edmund A. Hajim School of Engineering and Applied Sciences

University of RochesterRochester, New York

2011

ii

To Ellen:

Always Hopes,

Always Perseveres

iii

Curriculum Vitae

The author was born in New Haven, Connecticut on March 3rd, 1979. He

attended Vanderbilt University from 1997 to 2003, and graduated with a Bachelor

of Science degree in 2001 followed by a Master of Science degree in 2003. He came

to the University of Rochester in the Fall of 2003 and began graduate studies

in Computer Science. He pursued his research in software speculative parallelism

under the direction of Professor Chen Ding and received a Master of Science degree

from the University of Rochester in 2005.

iv

Acknowledgments

More than any other factor, I have to contribute so much to the unyielding

support of my wife, Ellen. This certainly extends well beyond the time spent

working towards a thesis, but so few pursuits offer the opportunity for a formal

acknowledgment. If I had the words, my thanks would dwarf this document. My

parents, also, deserve my heart-felt appreciation for many more years of support,

as well as for providing early models of scholarship.

I am deeply thankful to my adviser, Chen Ding, for guiding me through a

marathon process. Chen has been a constant through the many stages of graduate

education and study. Ultimately, he helped me develop a direction in research and

reminded me that we are measured not by the information we consume, but by the

knowledge we create. I owe a sincere debt to the members of my thesis committee

for their advice during the development of ideas that has led to this work, and for

the broader education they provided within the department.

My cohort of fellow aspiring researchers were an invaluable source of insight,

inspiration, humility and support. I’d like to thank other students in the compiler

and systems groups who have helped to show the way ahead of me — specifically

Yutao Zhong and Xipeng Shen — and kept me motivated, especially Mike Spear

and Chris Stewart. From a broader standpoint, I have appreciated time spent

with Ashiwin Lall, Chris Stewart, Ben Van Durme and Matt Post immensely.

My friends outside of the department helped to take my mind off computer

science from time; Jason and Ana stand out specifically in that regard. Finally,

v

I’d like to thank the staff of the computer science department for their help in

innumerable ways. Jo Marie Carpenter, Marty Gunthner, Pat Mitchell and Eileen

Pullara keep a lot of things running around the department and I’m happy to be

included among them.

vi

Abstract

The computing industry has long relied on computation becoming faster through

steady exponential growth in the density of transistors on a chip. While the

growth in density has been maintained, factors such as thermal dissipation have

limited the increase in clock speeds. Contemporary computers are rapidly becom-

ing parallel processing systems in which the notion of computer power comes from

multi-tasking rather than “speed”. A typical home consumer is now more likely

than not to get a parallel processor when purchasing a desktop or laptop.

While parallel processing provides an opportunity for continued growth in

mainstream computational power, it also requires that programs be built to use

multiple threads of execution. The process of writing parallel programs is ac-

knowledged as requiring a significant level of skill beyond general programming,

relegating parallel programming to a small class of expert programmers. The dif-

ficulty of parallel programming is only compounded when attempting to modify

an existing program. Given that the vast majority of existing programs have not

been written to use parallelism, a significant amount of code could benefit from

an overhaul.

An alternative to explicitly encoding parallelism into a program is to use spec-

ulative parallelism of some form. Speculative parallelism removes the burden of

guaranteeing the independence of parallel threads of execution, which greatly sim-

plifies the process of parallel program development. This is especially true when

vii

retrofitting existing programs because the programmer is less likely to have a

complete understanding of the code base.

In many cases, the safety of the parallelism can be speculative. There are also

cases in which it makes sense to parallelize tasks that are inherently speculative.

One may wish to speculate about the result of some computation, the safety of ap-

plying an optimization, or the best heuristics to use when searching for a solution.

This style of speculative parallelism is referred to as speculative optimization.

In this work I describe a speculative parallelism system based on POSIX pro-

cesses and communication. The system comprises a set of run-time libraries and

compiler support for easily generating a speculatively parallel program. The im-

plementation is designed to be general and portable, and the programming inter-

face is designed to minimize the programmer effort needed to effectively parallelize

a program. There are two variants on the run-time system intended for different

forms of parallelism. Both of these general forms of speculative parallelism are

generally applicable to many different problems.

viii

Table of Contents

Curriculum Vitae iii

Acknowledgments iv

Abstract vi

List of Tables xiii

List of Figures xiv

List of Algorithms xvi

Foreword 1

1 Introduction 2

1.1 Explicit Parallel Programing . . . . . . . . . . . . . . . . . . . . . 4

1.2 Speculative Execution . . . . . . . . . . . . . . . . . . . . . . . . 6

1.3 Road Map . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7

2 Background 9

2.1 Thread Representation . . . . . . . . . . . . . . . . . . . . . . . . 9

2.1.1 Data Sharing . . . . . . . . . . . . . . . . . . . . . . . . . 9

ix

2.1.2 Message Passing . . . . . . . . . . . . . . . . . . . . . . . 12

2.2 Speculative Threads . . . . . . . . . . . . . . . . . . . . . . . . . 12

2.2.1 Ancillary Tasks . . . . . . . . . . . . . . . . . . . . . . . . 12

2.2.2 Run-Ahead . . . . . . . . . . . . . . . . . . . . . . . . . . 16

2.3 Fork and Join . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18

2.3.1 Futures . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18

2.3.2 Cilk . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20

2.3.3 Sequential Semantics . . . . . . . . . . . . . . . . . . . . . 21

2.4 Pipelining . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23

2.4.1 Decoupling . . . . . . . . . . . . . . . . . . . . . . . . . . 23

2.5 Support Systems . . . . . . . . . . . . . . . . . . . . . . . . . . . 25

2.5.1 Operating System . . . . . . . . . . . . . . . . . . . . . . . 26

2.5.2 Compiler . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28

2.5.3 Race Detection . . . . . . . . . . . . . . . . . . . . . . . . 31

2.6 Correctness Checking . . . . . . . . . . . . . . . . . . . . . . . . . 32

2.6.1 Heavyweight . . . . . . . . . . . . . . . . . . . . . . . . . . 32

2.6.2 Hardware Techniques . . . . . . . . . . . . . . . . . . . . . 33

2.6.3 Monitoring . . . . . . . . . . . . . . . . . . . . . . . . . . 34

3 Process-Based Speculation 36

3.1 Implementation . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36

3.1.1 Creation . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37

3.1.2 Monitoring . . . . . . . . . . . . . . . . . . . . . . . . . . 37

3.1.3 Verification . . . . . . . . . . . . . . . . . . . . . . . . . . 38

3.1.4 Abort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39

x

3.1.5 Commit . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39

3.2 Advantages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40

3.3 Disadvantages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41

3.4 Special Considerations . . . . . . . . . . . . . . . . . . . . . . . . 41

3.4.1 Input and Output . . . . . . . . . . . . . . . . . . . . . . . 41

3.4.2 Memory Allocation . . . . . . . . . . . . . . . . . . . . . . 42

3.4.3 System Signals . . . . . . . . . . . . . . . . . . . . . . . . 43

4 Speculative Parallelism 44

4.1 Design . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45

4.1.1 Lead and Spec Processes . . . . . . . . . . . . . . . . . . . 45

4.1.2 Understudy: Non-speculative Re-execution . . . . . . . . . 47

4.1.3 Expecting the Unexpected . . . . . . . . . . . . . . . . . . 48

4.2 Programming Interface . . . . . . . . . . . . . . . . . . . . . . . . 51

4.2.1 Region Markers . . . . . . . . . . . . . . . . . . . . . . . . 51

4.2.2 Post-Wait . . . . . . . . . . . . . . . . . . . . . . . . . . . 55

4.2.3 Feedback . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55

4.3 Run-Time System . . . . . . . . . . . . . . . . . . . . . . . . . . . 56

4.3.1 Creation . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56

4.3.2 Monitoring . . . . . . . . . . . . . . . . . . . . . . . . . . 57

4.3.3 Verification . . . . . . . . . . . . . . . . . . . . . . . . . . 71

4.3.4 Commit . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72

4.3.5 Abort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74

4.4 Types Of Speculative Parallelism . . . . . . . . . . . . . . . . . . 78

4.4.1 Data-Parallel . . . . . . . . . . . . . . . . . . . . . . . . . 80

xi

4.4.2 Task-Parallel . . . . . . . . . . . . . . . . . . . . . . . . . 80

4.5 Comparison to Other Approaches . . . . . . . . . . . . . . . . . . 81

4.5.1 Explicit Parallelism . . . . . . . . . . . . . . . . . . . . . . 81

4.5.2 Fine-Grained Techniques . . . . . . . . . . . . . . . . . . . 82

4.6 Evaluation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84

4.6.1 Implementation and Experimental Setup . . . . . . . . . . 84

4.6.2 Application Benchmarks . . . . . . . . . . . . . . . . . . . 85

5 Speculative Optimization 95

5.1 Design . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96

5.1.1 Fast and Normal Tracks . . . . . . . . . . . . . . . . . . . 96

5.1.2 Dual-track . . . . . . . . . . . . . . . . . . . . . . . . . . . 97

5.2 Programming Interface . . . . . . . . . . . . . . . . . . . . . . . . 97

5.3 Run-time Support . . . . . . . . . . . . . . . . . . . . . . . . . . . 99

5.3.1 Creation . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99

5.3.2 Monitoring . . . . . . . . . . . . . . . . . . . . . . . . . . 99

5.3.3 Verification . . . . . . . . . . . . . . . . . . . . . . . . . . 101

5.3.4 Abort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104

5.3.5 Commit . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104

5.3.6 Special Considerations . . . . . . . . . . . . . . . . . . . . 107

5.4 Compiler Support . . . . . . . . . . . . . . . . . . . . . . . . . . . 115

5.5 Uses of Fast Track . . . . . . . . . . . . . . . . . . . . . . . . . . 115

5.5.1 Unsafe Program Optimization . . . . . . . . . . . . . . . . 115

5.5.2 Parallel Memory-Safety Checking . . . . . . . . . . . . . . 117

5.6 Evaluation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118

xii

5.6.1 Analysis . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118

5.6.2 Experimental Results . . . . . . . . . . . . . . . . . . . . . 122

6 Conclusion 131

6.1 Contributions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131

6.2 Future Directions . . . . . . . . . . . . . . . . . . . . . . . . . . . 132

6.2.1 Automation . . . . . . . . . . . . . . . . . . . . . . . . . . 132

6.2.2 Composability . . . . . . . . . . . . . . . . . . . . . . . . . 133

6.2.3 Further Evaluation . . . . . . . . . . . . . . . . . . . . . . 135

A Code Listings 137

A.1 BOP Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137

A.2 Fast Track Code . . . . . . . . . . . . . . . . . . . . . . . . . . . 146

A.3 Common Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159

Bibliography 166

xiii

List of Tables

4.1 Speculation actions for unexpected behavior . . . . . . . . . . . . 49

4.2 Three types of data protection . . . . . . . . . . . . . . . . . . . . 65

4.3 Comparisons between strong and weak isolation . . . . . . . . . . 70

4.4 XLisp Private Variables . . . . . . . . . . . . . . . . . . . . . . . 85

4.5 XLisp Checked Variables . . . . . . . . . . . . . . . . . . . . . . . 85

4.6 Execution times for various speculation depths . . . . . . . . . . . 87

4.7 The size of various protection groups in training runs . . . . . . . 88

4.8 Execution times of bop GZip . . . . . . . . . . . . . . . . . . . . 89

xiv

List of Figures

4.1 Sequential and speculative execution of three ppr instances . . . . 46

4.2 Example of matching ppr markers . . . . . . . . . . . . . . . . . 54

4.3 The states of the sequential and parallel execution . . . . . . . . . 66

4.4 State diagram of bop . . . . . . . . . . . . . . . . . . . . . . . . . 79

4.5 The effect of speculative processing on Parser . . . . . . . . . . . 90

4.6 Solving 8 systems of linear equations with Intel MKL . . . . . . . 93

5.1 State diagram of FastTrack processes. . . . . . . . . . . . . . . . . 106

5.2 FastTrack resource allocation state diagram . . . . . . . . . . . . 127

5.3 Analytical results of the FastTrack system . . . . . . . . . . . . . 128

5.4 The effect of FastTrack Mudflap on four spec 2006 benchmarks. . 129

5.5 FastTrack application to sorting routines . . . . . . . . . . . . . . 130

5.6 FastTrack on synthetic benchmarks . . . . . . . . . . . . . . . . . 130

xv

List of Algorithms

2.4.1 Listing of pipeline loop. . . . . . . . . . . . . . . . . . . . . . . . . 24

2.4.2 Interleaved iterations of pipelined loop. . . . . . . . . . . . . . . . 24

4.2.1 Example use of bop to mark a possibly parallel region of code

within a loop. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52

4.2.2 Example use of bop including EndPPR marker. . . . . . . . . . . . 52

4.2.3 Example use of bop in a non-loop context. . . . . . . . . . . . . . 53

4.2.4 Example of a pipelined loop body. . . . . . . . . . . . . . . . . . . 55

4.2.5 Example use of bop post/wait. . . . . . . . . . . . . . . . . . . . 56

4.3.1 Listing of ppr creation. . . . . . . . . . . . . . . . . . . . . . . . 58

4.3.2 Examples of shared, checked, and private data . . . . . . . . . . . 59

4.3.3 Listing of bop termination routine . . . . . . . . . . . . . . . . . 73

4.3.4 Listing of ppr commit in the speculative process . . . . . . . . . 75

4.3.5 Listing ppr commit in the understudy process . . . . . . . . . . . 76

4.3.6 Listing of ppr commit in the main process . . . . . . . . . . . . . 77

4.3.7 Listing of ppr commit finalization routine . . . . . . . . . . . . . 78

5.2.1 Example listing of FastTrack loop optimization . . . . . . . . . . 98

5.2.2 Unsafe function optimization using fast track . . . . . . . . . . . . 98

5.3.1 Listing of FastTrack creation. . . . . . . . . . . . . . . . . . . . . 100

5.3.2 Listing of FastTrack monitoring. . . . . . . . . . . . . . . . . . . . 101

5.3.3 Listing of FastTrack verification routine FT CheckData . . . . . . 103

5.3.4 Listing of slow track commit routine. . . . . . . . . . . . . . . . . 105

xvi

5.3.5 Listing of FastTrack exit point handler. . . . . . . . . . . . . . . . 110

5.6.1 Pseudo code of the synthetic search program . . . . . . . . . . . . 125

6.2.1 Example of FastTrack self-composition . . . . . . . . . . . . . . . 135

1

Foreword

Chapters 4 and 5 of this dissertation are based on collaborative work. Chap-

ter 4 of my dissertation was co-authored with Professor Chen Ding, and with fellow

students Xipeng Shen, Chris Tice, Ruke Huang, and Chengliang Zhang. I con-

tributed the implementation of the computational system, and the experimental

analysis. It has been published in Proceedings of the ACM SIGPLAN Conference

on Programming Language Design and Implementation, 2007. An early prototype

of the run-time system was created by Xipeng Shen, which was rewritten for our

publication, and again for ongoing work. Rule Huang contributed compiler sup-

port, and Chris Tice worked on the MKL benchmark. Chengliang Zhang helped

with system testing.

I am the primary author of Chapter 5, on which I collaborated with Profes-

sor Chen Ding and with fellow graduate student Tongxin Bai. This chapter has

been published in Proceedings of the International Symposium on Code Gener-

ation and Optimization, March 2009. My contribution is the implementation of

the computational system, construction of the experimental frameworks, and the

experimental analysis. Tongxin Bai contributed design ideas, and assisted with

testing.

2

1 Introduction

Since the introduction of the Intel 4004 microprocessor, the number of transistors

on commercial integrated circuits has doubled roughly every two years. This

trend was famously noted by Gordon Moore in 1965 and has continued to the

present [40]. During this period of time the growing number of transistors typically

corresponded with an increase in the clock rate, from 740 kHz for the 4004 chipset

to 3.8 GHz for Intel’s Pentium 4 processor in 2004.

Since the release of the Pentium 4 processor, clock rates have actually de-

creased slightly. Currently, the highest clock rate available on an Intel micro-

processor is 3.33 Ghz. The primary reason for this stagnation and decline is the

problem of thermal dissipation. Each transistor on a chip uses some amount of

power in two forms: constant leakage and per state switch. Increasing the chip

clock rate directly increases the power consumption due to switching, but also re-

quires a reduction in the size of components (to reduce signal propagation time).

This miniaturization increases the density of the transistors, which increases the

amount of power consumed in any given chip area. Increased power consumption

leads to increased heat consumption. The two factors — increased switching and

concentration of components — compound on one another.

On the consumer front, we’ve reached the limits of air cooling a computer

3

sitting a room-temperature environment. Air cooling can be extended by moving

processing into areas with colder ambient temperature, and liquid cooling tech-

niques provide an alternative solution. Even with more sophisticated approaches

to ensure the integrity of a running processor, at some point a significant amount

of power must be used to cool the chip. In contemporary data centers it is com-

mon for the power demands of the cooling systems to surpass the power used to

actually perform computation. The continued growth in power consumption has

been recognized to be unsustainable both technologically and commercially, as

consumers recognize the ancillary costs of their processors.

With the skyward increase in clock rates stalled, the choice has been to expand

processors horizontally. Computers are no longer made “faster” with increasing

clock rates, but instead are made more powerful with multiple processing cores.

We have reached the multicore era in which it is typical to find a multiprocessor

in consumer desktops, laptops, and even mobile devices.

Although computers are now parallel, the same cannot broadly be said of the

programs running on them. The majority of programs, both existing programs

and those being written today, are not designed to take advantage of parallel

processing. One reason for this is the relative scarcity of parallel computers in

the past — particularly in the home consumer arena. Another reason is that

programmers are trained to think about the problems they are solving in an

explicitly sequential way.

The result is a large body of programs that must be retrofitted to take ad-

vantage of parallel processing systems. There are a few significant reasons that

parallel programming is difficult, and many of these are only exacerbated when

attempting to modify an existing code base.

4

1.1 Explicit Parallel Programing

While the general public may recognize that programming requires a certain level

of expertise, parallel programming has largely been relegated to a select group of

programmers. Programmers are typically taught to think explicitly in series —

to write an imperative program as a series of steps that depend on one another.

This can make the transition to parallel programming difficult for programmers,

but more importantly it has led to a legacy of programs that are truly serial by

design.

Finding Parallelism

Identifying portions of a program that can safely run in parallel with one another

is perhaps the most difficult aspect of parallel programming. This task is often

made more difficult by attempts by programmers to optimize their code for the

sequential execution. Once the parallel regions have been identified, the program-

mer must ensure the correctness of each region interacting with all others. This

is most commonly done using locks, which must be correctly associated with the

same collection of data in every case where that data may be modified by multiple

threads. The problems involved in correctly writing a parallel program are exac-

erbated when attempting to update an existing program. Without a familiarity

with the code in question, the programmer is less likely to recognize side effects

of functions or identify poorly isolated data. Currently, no tool exists that can

automatically identify parallelism in an arbitrary program, and it is not possible

to do so in every case.

Ensuring Progress

One of the most well known problems encountered in parallelism, whether de-

signing a single program with multiple threads of execution, or scheduling multi-

5

ple programs with shared resources in an operating system, is deadlock. Of the

four conditions necessary for deadlock to exist identified in [12], three are easily

achieved using locks: mutual exclusion (only one thread can hold a lock), lack

of preemption (a thread cannot steal a lock), and hold and wait (a thread can

acquire locks one after another). The only condition that needs to be added by a

programmer is circular waiting, where a group of multiple threads each wait on a

lock held by another member of the group.

In addition to deadlock, a few other problems can arise that prevent a system

from making progress. Livelock is similar to deadlock in that threads do not make

progress collectively because of interference with one another. The difference being

that each thread is active, typically trying to coordinate with another livelocked

thread. Livelock is a specific example of resource starvation, which describes a

situation in which a thread cannot make progress because it lacks access to some

resource. The distinction from deadlock is that progress is made by the thread

holding the resource. When the resource is released, it is allocated to some thread

other than the starving one.

Guaranteeing Correctness

In the context of parallel programming, correctness is defined to mean that the ob-

servable behavior of the program is maintained. If the program acts as a function,

mapping input to output, then the function must be preserved. In the context of

parallelizing a sequential program, the original serialization of observable points in

the execution implied by that program must be maintained, ruling out deadlock.

To guarantee correctness, the programmer must ensure that all accesses to shared

data are properly guarded. This requires identifying all shared data, identifying

all accesses to that data, and finally creating an association between data objects

and the primitives used to synchronize their access. Particularly in the case of

parallelizing an inherited code base, the programmer may have difficulty simply

6

identifying what data objects are shared. Assuming that using a single global lock

will not allow acceptable performance, the programmer will also be responsible for

determining which data need to be protected collectively because their common

state needs to be consistent.

Debugging

One of the more common problem in parallel programming is the occurrence of

a data race, which is the case of two threads accessing the same data without

synchronization between the accesses (at least one of which must be a write).

The result of a race (i.e., the value that is ultimately attributed to the data)

depends on the sequence and timing of events in both threads leading up to their

accesses. Because the scheduling of threads may depend on other processes in

the system at large, the error is effectively non-deterministic. Generally, we want

to reproduce the conditions under which a bug occurs to isolate it. Because the

problem may appear very intermittently, the conditions for the error are effectively

random. Running the program in a debugger can force a particular serialization,

which ensures a certain outcome of the race, potentially making the debug session

useless for finding the problem.

1.2 Speculative Execution

Speculative parallelism — running some portions of a program in parallel with

the rest on the assumption they will be useful and correct — can extract useful

coarse-grained parallelism from existing programs in several ways. The speculative

execution systems outlined in Section 1.3 directly address the problems of explicit

parallel programming raised in Section 1.1.

7

Finding Parallelism The first-order problem of explicitly parallelizing code is

to identify which portions of a program can safely be executed simultaneously.

The other problems are largely the result of the solutions used once the parallel

regions of the program have been identified. Using a speculative system allows a

programmer to indicate parallel regions without the responsibility of preserving

run-time dependencies, which the system guarantees will not be violated.

Ensuring Progress Ensuring progress is trivial because there is no potential

for deadlock. The programmer does not introduce any serialization primitives

such as locks. This means that the speculative run-time system cannot introduce

a circular waiting condition. It may be tempting to qualify the previous statement

such as “where one did not already exist,” but clearly there cannot have been a

circular dependency in a sequential program. Likewise, there is no potential for

introducing livelock or resource starvation.

Debugging The speculative execution system depends on the ability to dis-

card the speculative portion of execution and follow only the sequential flow of

execution. The intent of this fallback is that the speculatively parallel program

maps directly back to the sequential execution. In this case, there is no need to

explicitly debug a speculatively parallel program because the user can debug the

sequential program with the same effect.

1.3 Road Map

In Chapter 2 I cover the extensive existing literature on speculative execution

related to both sequential programs and optimization. In Chapter 3 I describe

the fundamental aspects of an implementation for a process-based speculative

run-time system. Chapter 4 describes a run-time system intended for speculative

8

execution of program regions ahead of time. In Chapter 5 I describe a software-

only speculation system that enables unsafe optimization of sequential code. I

conclude with a discussion of the limitations of the current speculative execution

system, and of future directions to address, in Chapter 6.

9

2 Background

2.1 Thread Representation

2.1.1 Data Sharing

Because programs written in imperative languages progress by writing to and later

reading from data objects, eventually using their values to represent their result,

data sharing is a natural extension. By allowing multiple processes to share some

of the data they are modifying, the syntax of each process can remain largely the

same. On a surface level, the semantics of each process also remain largely the

same except that the value of a data object may change between being written

and later being read.

Rather than considering a single program and its state represented by data, in

the context of shared data we have to consider all processes and all of their data

as a single state. The problem that arises is guaranteeing that state is always

consistent. A classic example of such consistency is a shared buffer into which

multiple processes may add or remove data. In order to know where new data

should be inserted, or the position from which is should be read, a process must

update an indication of the size of the buffer. If the value is entered first, another

process may overwrite it before the position is updated. If the position is updated

10

first then a reader may attempt to read the buffer and receive garbage.

In order to guarantee a process always sees a consistent view of the global state,

there must be some mechanism to indicate that the data should not be accessed.

This is typically done by introducing a lock, which requires a hardware guarantee

that all processes see the flag consistently and cannot access it simultaneously.

Implementations typically rely on an atomic read-modify-write operation that

only sets the value of a data object if its current value matches what is expected.

Such systems are more efficient if multiple locks are used so that distinct parts

of the shared state can be modified simultaneously. One of the difficulties is

ensuring that the relationship between a lock and the data it is meant to protect

is well defined — that no access to the data is made without first acquiring the

lock. In this way, a portion of the shared state is used to protect the consistency

of the shared state.

An alternative to locking regions of memory to provide protection is to create

the illusion that modifications are made atomically. This typically involves intro-

ducing additional redirection to encapsulated data that must be keep consistent.

By modifying a local (or private) copy of the data, one process can ensure that

no others will read inconsistent state. Once the modifications are complete, the

single point of redirection can be atomically updated to refer to the new (and no

longer private) version of the data.

This sort of redirection can be expanded to be applied to general memory

access in transactional memory systems. These systems indicate that specific

regions of the program should appear atomically. By tracking all of the reads

and writes that a process makes, it is possible to ensure that none of the memory

involved was changed by another transacting simultaneously.

Transactional memory was originally proposed as a hardware mechanism to

support non-blocking synchronization (by extending cache coherence protocols) [26]

and several software implementations are built for existing hardware. Transaction

11

semantics, which require a serializable result, is less restrictive than speculative

parallelization, which requires observational equivalence or the same result as the

original sequential execution. Because transactions have the potential to conflict

with one another, they do not guarantee parallelism. Inserting transactions does

affect the semantic meaning of a program because they alter the serial execution

it performs. The techniques I describe in Chapter 4 do not change the program

in this way, and are easier to use for a user or a tool to parallelize an unfamiliar

program.

At the implementation level, serializibility checking requires the monitoring of

both data reads and writes, so it is more costly than the run-time dependence

checking. Current transactional memory systems monitor data accesses rather

than values for conflict detection. The additional flexibility is useful for supporting

parallel reduction, but it is not strictly necessary for parallelization, as it is for

concurrency problems such as on-line ticket booking.

In most thread-level speculation schemes (e.g., speculative DOALL) the entire

body of a loop constitutes a transaction. When we consider a parallelization

technique like decoupled software pipelining described in Section 2.4.1 the loop

body is spread across multiple threads. In order to introduce speculation to such

pipelines a multi-threaded transaction (mtx) is necessary, which had depended

on hardware support. The work in [51] introduces a software only multi-threaded

transaction system.

The software mtx gives the threads within a transaction the effect of a pri-

vate memory space for their work. Each mtx representing a loop iteration is

divided into sub-transactions that are each executed on a separate thread. Each

sub-transaction forwards the uncommitted values it has computed to the next

through a shared queue during execution, and the final sub-transaction has the

responsibility of committing the transaction as a whole.

12

2.1.2 Message Passing

The other typical way to express synchronization between parallel processes is

through message passing. The most basic form of message passing is through

matched pairs of send and receive statements by which one process explicitly

passes data to another process that has expressly made an attempt to acquire

it. This point-to-point communication can be synchronous or asynchronous, and

may be related to allow unmatched pairs of communication primitives. In com-

parison to data sharing as a synchronization mechanism, message passing benefits

in encouraging local data storage in systems with non-uniform memory access.

Attempting to model message passing as a global, shared state is non-trivial

because of the complexities resulting from delays as message are passed between

processes. Message passing does not have a clear analog to an imperative se-

quential programming, though it clearly translates to client-server models typical

of networking. Such systems are often event based, where a processes’ state is

directly affected by signals given to it, rather than polling a data location or

explicitly receiving a message.

2.2 Speculative Threads

2.2.1 Ancillary Tasks

Past work has suggested specifically using speculative execution to treat some

portion of the program’s work as a parallel task. Such tasks include the addition of

memory error and leak checking, performing user defined assertions, and profiling.

In [48] the authors suggest creating a “shadow” version of a program to address

these ancillary tasks specifically, although they do not address how the shadow

process might be generated.

13

By pruning computation that is not necessary to determine the control flow,

the shadow process creates some slack with which it can instead perform the addi-

tional work. It is not clear however if typical programs contain enough unnecessary

computation to be removed and compensate for extra work.

It is not always possible for a shadow process to determine all of the values

necessary for control flow. These values could be dictated by interactive input or

system calls that the shadow cannot safely reproduce. As a result, the control

flow within the shadow process may depend on values communicated from the

primary process once it has computed them. Additionally, there is a trade off to

be made between having the shadow compute values and having the main process

communicate those values to the shadow.

A limitation of the shadow processing system is composability; there is no

good way to handle multi-threaded or multi-process tasks. There is a limit to the

availability of signal handlers. The shadow process is generated once and runs

in parallel to the entirety of the original process. Empirical results found the

shadow process typically finishing after the serial process. As a result, there is

no mechanism for the shadow process to provide feedback to the original process.

This is acceptable in some use cases, such as error checking, where a report can

be generated after program execution, but prevents shadow processing from being

applicable for accelerating processes in general.

The limitation of shadow processing is addressed in later work by periodically

spawning a new shadow process [41]. The objective in this case is specifically to

make execution profiling more tolerable, described as “shadow profiling”. A new

profiling process is created each time a timer interrupt is triggered1.

Program instrumentation is added using the Pin tool [36] by having the shadow

process switch to a just-in-time rewriting mode within the Pin runtime after it has

forked. Moseley et al. [41] additionally address some of the problems that arise

1By default the SIGPROF timer is used, but this is customizable.

14

even when using copy-on-write protection. Writes to shared memory segments or

memory mapped files cannot be completely protected and trigger a fault in the

child profiling process. The shadow profiling process can choose to skip the trou-

blesome instruction, or it can terminate and allow a new shadow profile process

to be created.

System calls are also a problem for speculative execution, and Moseley et al.

[41] attempt to categorize them into groups: benign, output, input, miscellaneous,

and unsafe. If call is encountered that is not known explicitly to be safe, the

shadow simply aborts and allows a new profiler to begin.

Because the profiling system is only intended to be an instrumentation sample

the shadow profiling can safely afford to abort in many cases. It is also not neces-

sary for the execution performed in the shadow profile to be deterministic. While

allowing the control flow in the speculative process to deviate from the original

program reduces the accuracy of the profile, it does not affect the correctness of

the program. This flexibility is not acceptable for general purpose speculative ex-

ecution, and again precludes shadow processing from being used for accelerating

process in the general case.

Newer work has moved beyond parallelizing memory checking to placing dy-

namic memory management in a parallel task, which is referred to as the memory

management thread (mmt) [58]. If the memory allocation and deallocation sub-

system includes safety checks such as invalid frees then these checks can also be

placed in the memory management thread.

One of the difficulties in moving dynamic memory management into a separate

thread is ensuring that the memory management can be wrapped with minimal

modification to the application and memory management library. Another signifi-

cant difficulty is the overhead of thread synchronization, which the authors note is

comparable to the cost of the memory management itself. These two problems are

addressed by allowing the mmt to run as a server and only requiring synchroniza-

15

tion for memory allocation. When memory objects are released, the program can

essentially issue the deallocation asynchronously and continue without waiting for

memory management to complete.

The synchronous memory requests still have a communication delay in addition

to the period of time needed to actually service the request. This is alleviated by

having the mmt speculative preallocate objects, which can be provided without

delay if the size is right. Delays are further reduced by batching deallocation

requests to the mmt, and symmetrically by providing the client with multiple

preallocated objects.

Although the mmt technique can extract some memory safety checks into a

separate thread, not all types of memory checks are isolated in the allocation or

deallocation routines. Checks such as array over-flow are must be performed in

the context of the memory access.

Some of these limitations are addressed in the approach taken in the Speck

(Speculative Parallel Check) system [45]. The Speck system is intended to decou-

ple the execution of security checking from the execution of the program at large.

During program execution a set of instrumented systems call potentially create an

addition instance of the application that includes the security checks. Like earlier

work, some of the overhead is removed by only entering the instrumented code

path periodically.

The primary focus of the Speck work is on security checks such as virus scan-

ners and taint analysis, though it could be applied to simpler checking for safe

memory access. The limitation of the Speck system is its dependence on the use

of a modified Linux kernel designed to support efficient distributed file system

interaction, called Speculator [44]. This support is necessary to allow for unsafe

actions performed by an application to be rolled back if one of the security checks

were to fail. An addition feature of their operating system support is the ability

to ensure that certain system functionality operates identically in both processes,

16

and that signals are delivered at the same point in the execution of each.

Another recent approach to minimizing the overhead of memory safety check-

ing with thread-level speculation did so by parallelizing an existing memory check-

ing library [31]. Because of the tight synchronization needed by the accesses to the

data structures used by the library, adapting it for use with TLS requires detailed

analysis of the library itself and the manual insertion of source level pragmas to

denote parallel regions. The annotated code was then passed through a paralleliz-

ing compiler which extracts each parallel task. Ultimately, the authors assume

that some form of hardware support will guarantee the ordering of the tasks to

guarantee the sequential semantics of the original program. The system also relies

on the presence of a mechanism to explicitly synchronize access to the library’s

data structures which is not provided.

2.2.2 Run-Ahead

An approach related to the techniques used to off-load ancillary tasks to unused

processing units is to create a second thread of execution to precompute some

values for the primary process. Rather than performing additional work, thus

lagging behind the original program, these processes run ahead of the primary

process in order for it to execute more quickly.

There are a number of methods for producing a run-ahead process, relying on

various kinds of support. The ‘Slipstream’ technique presented in [57] monitors

the original program to detect operations which are redundant at run time. The

operations that are found to be redundant can be speculatively elided from the

leading process when they are next encountered. As a result, the leading process

can run faster because many operations are removed entirely. The trailing process

is also able to execute more quickly because of side-effects from the first process:

memory prefetching and improved branch prediction. The end result is that the

17

two processes together (one of which is the original program) complete faster than

either would independently.

Because the leading process is not performing all of the operations of the

original program, its execution may deviate from the correct execution, which

is always computed by the trailing process. In order to recover from incorrect

speculation, and to generate the lead process, the Slipstream technique requires

a number of additional hardware components. The lead process must have a

program counter that is modified to skip past some instructions by recording

previous traces through the program execution. The address of memory locations

modified by the lead process are recorded to allow for recovery by updating those

values from the memory state of the trailing process2.

The suggested mechanism for determining which operations may be good can-

didates for speculative removal is based on a small data flow graph built in re-

verse as instructions are retired. Operations that write to memory (or registers)

are recorded as being the producer of the value stored there, and a bit denotes

the value as valid. A subsequent write with the same value is redundant, while

a different value updates the producer. A reading operation sets a bit indicat-

ing a location has been referenced, which allows an old producer operation to be

removed if the value was unused.

Another related idea used in hardware systems is to extract a fast version of

sequential code to run ahead while the original computation follows. It is used to

reduce memory load latency with run-ahead code generated in software [33], and

recently to reduce hardware design complexity [19].

A third, more recent idea is speculative optimization at fine granularity, which

does not yet make use of multiple processors [43]. All of these techniques re-

quire modifications to existing hardware. Similar special hardware support has

been used to parallelize program analysis such as basic block profiling, memory

2Additionally, the register file is copied.

18

checking [46], data watch-points [66].

Loop-level software speculation was pioneered by the lazy privatizing doall

(LPD) test [52]. The LPD technique works in two separate phases: a marking

phase executes the loop and records access to shared arrays in a set of shadow

arrays. A later analysis phase then checks for dependence between any two itera-

tions of the loop by analyzing the shadow values. Later techniques speculatively

make shared arrays private to allow avoid falsely detecting dependencies, and com-

bine the marking and checking phases to guarantee progress [11, 14, 22]. Previous

systems also address issues of parallel reduction [22, 52] and different strategies

of loop scheduling [11]. A weaker type of software speculation is used for disk

prefetching, where only the data access of the speculation needs to be protected

(through compiler-inserted checks) [9].

2.3 Fork and Join

2.3.1 Futures

A future is a mechanism to indicate that the result of some computation will be

needed by the process — referred to as the continuation — at some point in the

future. Originally introduced in Multilisp [24], the contents of the future com-

putation are independent of the invoking computation. A system implementing

futures is free to schedule the future computation at any point before the result of

the computation is needed. Support for futures exists in the Java programming

language through its concurrency utilities package. Unlike functional languages

like Multilisp, Java and other imperative languages make frequent modification of

shared state. In its implementation of futures, the Java run-time system does not

make any guarantees about the synchronization of the future with its continua-

tion. The programmer is still responsible for ensuring that access to shared data

19

is protected.

Work on a Java implementation of futures that are “safe” in terms of main-

taining their sequential semantics has been done through modifications to the

run-time virtual machine [63]. In order to ensure the effects of a future are not

intermixed with data accesses of its continuation, each is run in a separate thread

with a local write buffer implemented by chaining multiple versions of an object

together. Reads to the object must traverse a list of versions to location the cor-

rect one for the context of the thread. Each thread must also maintain a read

and write map of data accesses, which is used to detecting read-write violations

between the threads. Despite the name, the future should conceptually complete

its data accesses before the continuation.

The implementation of safe futures depends heavily on the fact that Java is a

managed language in which objects have metadata and are accessed by reference,

simplifying the redirection needed to protect access. The additional work needed

to monitor data access is added to existing read and write barriers designed for

garbage collection, and the rollback routine is based on the virtual machine’s

exception handling.

Recent work has sought to automatically insert synchronization for Java fu-

tures using compiler support [42]. This support determines statically when a

future first accesses a shared data object and inserts a special form of barrier

statement called allowed. The allowed statement is not released in a continua-

tion until all of its futures have granted access with an explicitly matched grant

statement. A list of futures is built as they are spawned, and cleared after they

have granted access to the data. Because the insertion of the grant and allowed

operations is based on static analysis, it is more conservative than what could

be achieved with a system using run time analysis. The static analysis has the

advantage of significantly lower overhead during execution.

20

2.3.2 Cilk

One representation of fork-join style parallel programming is provided by the Cilk

programming language, which an extension of the C programming language us-

ing an additional set of annotations [6, 18]. Cilk allows a programmer to expose

parallelism using a set of language-specific keywords, which can be removed to

return to a valid C program. Because the programmer is responsible for dis-

tinguishing sequential and parallel code, the two portions of a program can be

clearly delineated and the programmer is forced to consider the overheads in the

sequential portion. The code that occurs within the cilk procedures is considered

to be “work” while the code outside these procedures is referred to as the “critical

path”. This distinction is directly analogous to the relationship expressed earlier

as Amdahl’s Law [2, 23].

The original implementation of Cilk required that invocation points distinguish

between threads that produce a value and the continuation that consumes that

value. The first thread would be created using spawn, while the consumer (or

“successor”) had to be created with spawn next. In order to pass the value to the

consumer, the send argument keyword would be used to explicitly place the result,

potentially allowing the waiting thread to begin. The keyword thread was used in

the way that cilk is now to represent code that contains Cilk specific code. The

use of spawn next and send argument is now handled automatically by the runtime

when the sync keyword is used. This improvement removes a requirement that all

Cilk threads (functions with the cilk keyword) be non-blocking.

In addition to the source code annotations, the Cilk programming language

depends on a sophisticated run-time system. The cornerstone of the system is

a work-stealing scheduler that seeks to balance the load between the available

processing units. The scheduler moves Cilk tasks (threads) from the processor

where they were spawned to processors that are idle. All of the overheads of the

21

system (e.g., spawning and moving tasks) are placed on the critical-path, which

is a design decision not shared by all systems.

2.3.3 Sequential Semantics

Although fork-join style semantics for parallelism makes explicit the point at which

parallel computation is needed, as mentioned in Section 2.3.1 there is no implicit

guarantee of atomicity or progress. A programmer is still responsible for guarding

shared data accesses to preserve object consistency and inserting synchronization

to prevent race conditions. Recent work using a run-time system called Grace

converts a program with fork-join parallel threads into a sequentially consistent

program [4].

Guaranteeing sequential consistency requires the effect of operations appear in

a specific order. This sequence is defined by the semantics of the source program

code. By assuming that threads should be serialized in the order they are created,

the sequential semantics of a fork become the same as a simple function call.

Allowing the run-time system to ensure thread ordering and atomicity, locks can

be elided and the program viewed semantically as though it were serial.

The Grace system does this by converting each thread into a heavy-weight

process with isolated (copy-on-write) memory. Heap and global data that would

have originally been available to all threads are placed in a memory mapped

file and each process maintains a local mapping of the same data for privatized

writes. Using a versioning scheme for the memory, and logging accesses during

execution, the run-time system can determine whether the processes execution

is correct. Assuming correct execution, the process must wait until all logically

prior processes complete before committing its local writes to the global map.

Although the process corresponds to a thread in the original program, Grace

intends to detect violations of the sequential semantics to guard against improper

22

parallel implementations.

Somewhat earlier work suggested two ways in which sequential semantics could

be relaxed intuitively to remove common points of misspeculation [8]. They argue

that sequential semantics may be overly restrictive in many cases in which some

portions of execution do not need to be explicitly ordered, and a program may

have multiple valid outputs. The primary suggestion is that groups of functions

be annotated to indicate a commutative relationship if their internal state does

not need to be ordered but does need to be accessed atomically. Put another

way, these functions have side effects that are only visible to one another. This

kind of behavior is common for dynamic memory management, which maintains

metadata that is not accessed externally.

The programmer is still responsible for identifying all functions accessing the

same state. Although this is significantly easier than identifying all functions that

access shared state and subsequently grouping them, it does allow for failures the

speculation system would otherwise prevent. Additionally, it requires atomicity

guards within the functions, which the authors ignore. There is an additional

requirement that commutative functions operate outside the scope of speculation

itself. If a transactional memory system is being used, the functions must use

non-transactional memory. This complicates cases where some state is internal

to the commutative group, while other state is global and also implies that these

functions must have an explicit inverse function because the rollback mechanism

of the speculation system will not protect them. This limits to applicability of

commutative annotations, or requires significantly more programmer effort that

initially suggested.

23

2.4 Pipelining

The general approach of parallelizing a loop — executing each instance of the

loop body separately — is not possible if there are dependencies carried from one

iteration of the loop to the next. There may still be cases in which such a loop

can still be parallelized, if it can successfully be pipelined. By breaking the loop

into a sequence of individual stages, we may find that dependencies are carried

from one stage to the next within an iteration, and one iteration of a stage to the

same stage in the next iteration, but that no dependencies are carried from a late

stage to an early stage in the next iteration.

A pipelined loop is analogous to a manufacturing pipeline in which a product

is created in stages. Each individual widget mirrors an instance of the loop body:

just as only one item can be painted or packaged at once, subsequent instances

of the first stage of the loop cannot execute concurrently. Likewise, just as the

widget must be painted before it is packaged, stages of the loop must be executed

in order. In such cases, the iterations of the loop can be performed in parallel by

skewing successive instances by one stage.

Given the code in Listing 2.4, the first three skewed iterations would appear as

depicted in Listing 2.4. Note that the first stage to be executed on each processor

is stalled until the stage is completed on the previous processor3. There is also

a stall between loop iterations because the number of stages does not align with

the number of processors.

2.4.1 Decoupling

In a traditional software pipeline, the thread on one processor executes an entire

instance of the loop body. Once the instruction carrying a dependency has been

executed (the dependent stage of the pipeline), the next iteration of the loop begins

3The correctness of this pipeline relies on the memory coherence of the architecture.

24

Algorithm 2.4.1 Listing of pipeline loop.

i n t A[ ] , B [ ] , C [ ] , D[ ] ;

fo r ( i n t i =0; i < N; ++i ) {B[ i ] = f (A [ i ] ) ;C [ i ] = g (B[ i ] ) ;D[ i ] = h (C [ i ] ) ;

}

Algorithm 2.4.2 Interleaved iterations of pipelined loop.

Processor 1 Processor 2 Processor 3 Processor 4B[1] = f(A[1]);

C[1] = g(B[1]); B[2] = f(A[2]);

D[1] = h(C[1]); C[2] = g(B[2]); B[3] = f(A[3]);

D[2] = h(C[2]); C[3] = g(B[3]); B[4] = f(A[4]);B[5] = f(A[5]); D[3] = h(C[3]); C[4] = g(B[4]);C[5] = g(B[5]); B[6] = f(A[6]); D[4] = h(C[4]);

on another processor. One seeks to align the loop structure and processor count

so that the first processor completes its loop iteration just as the last processor

completes the dependent stage of the loop. In this case there are no bubbles in

this pipeline and the processors can be maximally utilized.

The above scenario assumes there is no delay between completing the depen-

dent stage one on processor and initiating it on another. In reality, there will likely

be communication latency between the processors causes the later iterations to

stall slightly. Multiple stalls will accumulate over time and propagate through

later iterations.

The reason this problem arises is that communication is flowing cyclically

through all of the processors. Decoupling breaks the communication cycle so the

dependency communication only flows in one direction [47]. In a decoupled soft-

ware pipelined loop, after the dependent stage is executed on the first processor

the remainder of the loop is dispatched to another processor while the first pro-

cessor begins the next dependent stage. The result is that any communications

25

delay applies equally in all cases. The second processor is effectively skewed by

that delay.

The processes of scheduling a decoupled software pipeline involves constructing

a dependence graph of the loop instructions. The instructions represented by a

strongly connected component (scc) in the graph must be scheduled collectively in

a thread (though a thread may compute multiple components). These components

limit possible parallelism in two ways: there can be no more pipeline stages than

there are scc’s, and the size of the largest scc is the minimum size of the all

pipeline stages.

By introducing speculation into the decoupled software pipelined loop it is

possible to break some of the dependencies [60]. Breaking graph edges allows for

a reduction in the size of scc’s and an increase in their number. The specula-

tion temporarily removes dependencies that are highly predictable, schedules the

pipeline normally, then replaces edges that do not cross threads or flow normally

from early threads to later ones.

The implementation presented in [60] relies on compiler support for trans-

formations and versioned memory to enable rollback of misspeculation. Each

loop iteration involves advancing the memory version and sending checkpoint in-

formation to a helper thread, as well as speculation status. The dependence

on additional hardware support can be overcome using software multi-threaded

transactions as described in Section 2.1.1.

2.5 Support Systems

In order for parallel programming — and particularly speculative parallel pro-

gramming — to be possible, there is a number of ways the task of generating

the program must be supported. The initial problem is determining how the pro-

grammer should express the parallelism. The actual implementation of the parallel

26

constructs can be built for an existing language using a new library and program-

ming interface, or may be built around a language explicitly designed for parallel

programming. In the later case, the language compiler may be equipped with

additional analysis techniques to determine whether the parallel execution will be

valid. Below the programming language, the operating system must provide some

form of support. This OS support must at the very least include scheduling for

multiple tasks, but may also provide additional isolation or monitoring. At the

lowest level, the hardware must again provide multiple processing cores.

2.5.1 Operating System

Adding support for speculation at the operating system level provides a broad form

of support for applications. It is however generally limited to use by heavy-weight

processes, while light-weight thread implementations may need to multiplex what

the operating system supports.

One way for the operating system to enable parallel programming is by forcing

sequential semantics on the processes within the system much like the run-time

system described in Section 2.3.3. One way to achieve this is by building a message

based system in which processes only execute in response to receiving a message,

generating output to be passed to another process. Conceptually, only the old-

est message in the system can be consumed, which serialize the computation by

forcing the causality to flow linearly through the virtual time of the system.

The Time Warp operating system (twos) takes this approach and extends it

by speculatively allowing multiple processes to execute simultaneously [27]. twos

is motivated by distributed systems in which synchronization between processes

is impeded by varying latencies between parts of the system. A process cannot

quickly determine whether it may receive a message in future that should have

been handled before those currently waiting in its queue. For this reason, allowing

27

a process to proceed speculatively is also advantageous.

To allow for rolling back incorrect speculation each process periodically gener-

ates check a point, which is enqueued and assigned to the process’ current virtual

time. The virtual time value is incremented on (and assigned to) each sent mes-

sage, while received messages update the virtual time. If the incoming message is

labeled with a time in the virtual past then the process has consumed a message

that should have been processed before it, indicating misspeculation and causing

rollback.

The dependency, or causality, between processes is managed using anti-messages

that annihilate their matching message. Whenever a twos process creates a mes-

sage a matching anti-message is created as well. While the original message is

sent the anti-message is kept in the output buffer. After a process has rolled back

to a checkpoint, it will begin by consuming the oldest message (which just arrived

and caused the misspeculation). As the process proceeds it will often generate

many of the same outgoing messages, which will annihilate the matching message

waiting in the output buffer. If the anti-message is not matched it will be sent to

the original recipient of the message, where it will either cancel a pending mes-

sage in that processes input queue or cause another rollback. Irrevocable messages

(i.e., output to the user) are held in buffers until all messages that were issued

before them in virtual time have been consumed. The condition determines when

anti-message can be expunged from output buffers.

The twos has a number of limitations that make it inappropriate for use

as a general purpose operating system resulting from its intended purpose as a

platform for simulation. One complaint leveraged by later work is that twos

processes must be deterministic. In reality, processes can use a random number

generation as long as it is reproducible. Truly non-deterministic execution is

generally not desirable. twos does not allow for the use of dynamic memory

allocation, which is quite limiting. The operating system is designed only to run

28

in single user mode and on a static set of processes, though as long as processes

are not communicating with one another the principles of twos remain valid.

The Speculator system introduces support for explicitly tracking speculative

processes by extending the Linux operating system kernel [44]. As with all spec-

ulation systems, Speculator implements detection of and recovery from misspec-

ulation and guarantees that speculative processes do not perform irrevocable op-

erations.

Because speculation is performed on heavy-weight processes, rollback of in-

correct speculation is handled by terminating the process and restarting from a

checkpoint. The checkpointing routine is based on extensions to the standard

fork call. The processes is duplicated, but the new child is not made available to

the scheduler and retains the same identifiers as the original process. Additionally,

any pending signals are recorded and file descriptors are saved. The memory of

the process is marked for copy-on-write just as when a normal fork call is made.

During execution of the speculative process the use of output operations are

buffered for playback when the speculation is determined to be correct. Inter-

process communication is generally allowed, but the processes receiving the com-

munication is made to checkpoint and become speculative as well. The depen-

dency between the two processes is tracked so misspeculation will cause a series

of rollbacks to occur. Outside of the kernel, the speculative state of a process is

indeterminate.

2.5.2 Compiler

Any language with support for parallel programming will need some form of com-

piler support, even if it simply interprets a trivial syntax extension as a call to a

run-time library. More powerful analysis by a compiler can allow some degree of

automatic parallelization. The Mitosis compiler implements a form of run-ahead

29

speculation like those described in Section 2.2.2 by automatically selecting and

reducing speculative regions [50].

The objective of the Mitosis compiler is to insert spawn points in a program and

determining a corresponding point to which another thread should speculatively

jump. To enable the speculative thread, the region between the spawn and jump

points is reduced into a pre-computation slice (p-slice) that should evaluate the

state needed by the thread. The compiler estimates the length of the speculative

execution, and traces back through the control-flow graph from the point it should

complete. Any values that are found to live into the region between the jump point

and the assumed end point are required. By continuing past the jump point to

the spawn point, the instructions needed to compute those values are identified

and added to the p-slice.

The Mitosis compiler uses profile information to speculatively optimize the

p-slice in several ways. Branches that are infrequently taken, and write-read

dependencies that occur infrequently are elided. Additionally, control flow that

cannot reach the jump point is removed entirely. The profile information is also

used to select the most effective pairs of spawn and jump points based on the

length of the p-slice and speculative region as well as the likely-hood of the path

being take and correctly speculated. The Mitosis system relies on the underlying

hardware to detect misspeculation and handle recovery.

While the Mitosis system is build on a research system (the Open Research

Compiler), contemporary work implemented similar compiler support for in the

production gcc compiler [35]. Rather than generating p-computation slices,

POSH relies on profile information to select tasks that are likely to speculate

correctly. Tasks are initially created for every loop body and subroutine (and the

continuations of both) and then expanded or pruned to meet size restrictions —

large enough to overcome the cost of creation and small enough to be manageable4.

4Hardware TLS support is inherently limited to tracking a finite number of accesses.

30

Like the Mitosis system, POSH relies on hardware support for detection of vi-

olations of the sequential semantics of the program. In both cases, the assumption

is that threads are explicitly spawned. While POSH specifies that the architecture

provides a spawn instruction, Mitosis leaves the architecture details completely un-

specified. In a departure from the fork/join notation, POSH assumes the spawned

task will explicitly commit, while the parent task does nothing to explicitly re-

claim the child. If the parent attempts to read results from the child before it is

complete, misspeculation will occur.

Rather than inserting spawn and commit, a compiler could automatically gen-

erate the synchronization necessary to guarantee sequential ordering. Past work

has used data-flow analysis to insert wait and signal statements similar to the

grant and allow instructions introduced in Section 2.3.1 [64] to pipeline loop bod-

ies. The precise semantics of the instructions only indicate that access to a par-

ticular variable is guarded (equivalent to introducing a lock) and ordered (version

numbered). It must be assumed that instructions to initiate and finalize tasks are

also generated.

Zhai et al. only consider loop bodies as candidates for parallelization. The

naive placement of the synchronization would place the request at the beginning

of the task (loop body) and the release at the end, encapsulating the entire loop

in a single state. The region of code between the wait and signal represents the

critical section in which only the current task can access the variable, and like any

critical section is should be made as small as possible. To optimize the interleaving

of the tasks, the wait statement should be placed as late as possible while still

preceding all accesses to the variable. Likewise, the signal should be as early as

possible as long as no further writes follow it.

To further reduce the size of the critical section, instructions may be reordered

along with the synchronization instructions. By treating a signal instruction as

a read and following the dependence chain up through a control flow graph, the

31

entire sequence of instructions can be hoisted to a point at which dependency

cannot to determined (e.g., due to aliasing). Like the later Mitosis and POSH

systems, Zhai et al. consider profile information to achieve further optimization.

In cases where branches are highly predictable, synchronization may be hoisted out

of the hot path at the expense of misspeculation due to access in the uncommon

case.

2.5.3 Race Detection

Race detection is concerned with determining whether two task can be run in

parallel or need to be performed in series. One way this can be done is by moni-

toring threads during execution to maintain a representation of their relationship

in terms of being inherently serial or possibly parallel. During specific run-time

operations the representation can be queried to determine if a serial relationship

has been violated [17]. For example, when threads access shared data the order

of accesses must match the order of serial threads.

During execution a tree is maintained to represent threads. The leaves of

the tree represent threads, while the internal nodes indicate either a series or

parallel relationship. To determine the relationship between two threads, their

least common ancestor holds appropriate marker. For a given execution tree, the

leaves are numbered with a depth first traversal, and given a second number by

traversing the parallel nodes in the opposite order. Given these values, two nodes

are in series if the values indicate the same order, while the nodes are executing

in parallel if the values are in opposite orders.

Early implementations required that the reverse ordering of nodes be main-

tained at run time, requiring computation on order with the depth of the tree. The

approach in [3] allows for parallel maintenance of and queries to the series/parallel

information in linear time.

32

The process of data race detection can be made more efficient by reducing the

number of objects that need to be monitored at compile time. The eraser anal-

ysis tool achieves this using a number of deep analysis techniques [38]. Initially,

all accesses within a target Fortran program are assumed to require annotation

(including not just recording of access, but initialization and cleanup of metadata

to allow such recording). Using dependence analysis eraser prunes annotation

around statements without dependencies. With intra-procedural analysis, includ-

ing alias, modification, and reference information as well as whether a procedure

is ever used in a parallel construct, annotation for a procedure’s parameters may

be removed as well. After pruning as much annotation as possible, remaining

checks are handled using calls into an associated run-time library to track data

access during execution.

2.6 Correctness Checking

2.6.1 Heavyweight

Recently three software systems use multi-processors for parallelized program pro-

filing and correctness checking. All use heavyweight processes, and all are based

on Pin, a dynamic binary rewriting tool [36]. SuperPin uses a signature-checking

scheme and strives to divide the complete instrumented execution into time slices

and executing them in parallel [62]. Although fully automatic, SuperPin is not

foolproof since in theory the slices may overlap or leave holes in their coverage.

The speculative execution system I describe in Chapter 5 is not designed for

fully automatic program analysis, although I describe a use case in which auto-

matic analysis is enabled with some manual effort. The resulting system guar-

antees the complete and unique coverage during parallel error checking using a

programming interface that allows selective checking. This is useful when check-

33

ing programs that contain unrecoverable operations on conventional operating

systems. Because the runtime operates at the program level it requires source

code and cannot instrument externally or dynamically linked libraries. On the

other hand, it benefits from full compiler optimization across original and instru-

mented code. This is especially useful for curbing the high cost of memory-safety

checking. For example it takes a minute for gcc to optimize the instrumented

code of 458.sjeng, and the optimized code runs over 20% faster in typical cases.

Unlike the earlier systems that automatically analyze the full execution, a system

that is designed specifically for speculative optimization can provide a program-

ming interface for selecting program regions, the ability for a checking process to

roll back the computation from the last correct point, and a throttling mechanism

for minimizing useless speculation.

2.6.2 Hardware Techniques

Fast track is closely related to several ideas explored in hardware research. One

is thread-level speculative parallelization, which divides sequential computation

into parallel tasks while preserving their dependencies. The dependencies may be

preserved by stalling a parallel thread as in the Superthreaded architecture [59]

or by extracting dependent computations through code distillation [67] and com-

piler scheduling for reducing critical forwarding path [64]. These techniques aim

to only reorganize the original implementation rather than to support any type

of alternative implementation. Fast track is not fully automatic, but it is pro-

grammable and can be used by both automatic tools and manual solutions. The

run-time system checks correctness differently. The previous hardware techniques

check dependencies or live-in values, while fast track checks result values or some

user-defined criterion.

Hardware-based thread-level speculation is among the first to automatically

exploit loop-level and method-level parallelism in integer code. In most tech-

34

niques, the states of speculative threads are buffered and checked by monitoring

the data accesses in earlier threads either through special hardware additions to

a processor [54], bus snooping [10], or an extended cache coherence protocol [56].

Since speculative states are buffered in hardware, the size of threads is usually no

more than thousands of instructions. A recent study classifies existing loop-level

techniques as control, data, or value speculation and shows that the maximal

speedup is 12% on average for SPEC2Kint assuming no speculation overhead and

unlimited computing resources [28]. The limited potential at the loop level sug-

gests that speculation needs to be applied at larger granularity to fully utilize

multi-processor machines.

2.6.3 Monitoring

Correct data monitoring is essential for speculative parallelism techniques, and is

of the major sources of run-time overhead. For large programs using complex data,

per-access monitoring causes slowdowns often in integer multiples. The problem of

data monitoring is closely related to data breakpoints in the realm of debugging,

which must monitor program memory accesses and subsequently perform some

user defined action. Also related is to on-the-fly data race detection, which seeks

to detect inconsistencies in data accesses between threads of the same program.

Past work focused on reducing the overhead of data breakpoints5 has ap-

proached that problem using compile time data-flow analysis to identify accesses

which do not need to be explicitly monitored, and by instrumenting the program

with checks with simplified address calculations [61]. Within a debugger there

are two approaches to handling a break point for a particular memory location:

watch the location itself, or check each operation that could modify the loca-

tion. Typically, many instructions can be statically determined not to require

5Data breakpoints are also known as watch points, as opposed to control breakpoints.

35

monitoring.

Another approach to reducing the overhead of debugging is to use sampling

over a large number of runs. One such technique introduces code instrumentation

to record a number of boolean predicates based on run-time program behavior [34].

The predicates represent possible control flow (e.g., was a branch taken), return

value from functions (if it is positive, negative, zero), and the relationship be-

tween variables in the same scope (if one is greater than, less than, or equal to

the other). The total number of predicates is extremely large and so is the over-

head of potentially recording all of them. This cost is limited by evaluating the

predicate instrumentation infrequently based on random choice at each instance.

By additionally recording whether each predicate was ever observed, it is possible

to evaluate the probability that a given predicate can be used to predict program

failure. Although the approach that Liblit et al. discusses allows for useful analysis

of crash reports from deployed programs, it is not a general solution to program

debugging due to the number of samples needed before a bug can be isolated. For

the same reason, such sampling techniques are not applicable to the monitoring

needed by speculative execution.

36

3 Process-Based Speculation

Process based speculation consists of a run-time system and a programming in-

terface. The run-time system is built as a code library with which a programmer

might link their program. The programming interface defines how the program-

mer would invoke calls into the run-time library. In this chapter I describe the

implementation of the core run-time system. Descriptions of the programming

interface and details of the runtime for particular types of speculative parallelism

are addressed in Chapters 4 and 5.

3.1 Implementation

Speculative program execution requires mechanisms for:

• dividing a sequential execution into a series of possibly parallel tasks.

• spawning a speculative task in addition to the existing sequential execution.

• monitoring the execution of tasks.

• managing access to one or more speculative versions of data.

• determining whether the speculative execution is correct.

37

• terminating the speculative execution and reverting any effects it may have

had.

In the remainder of this chapter I will describe how process-based speculation

achieves each of these goals.

3.1.1 Creation

Process based speculation addresses the problem of creating a speculative task

using the operating system’s fork call. The fork call is advantageous in that

all POSIX operating systems support it, making its use highly portable. Prior

to calling fork, the speculative run-time system establishes the communication

systems needed to ensure correctness (described in Section 3.1.3). After the fork

call has been made, two paths of execution exist. Before returning from the run-

time creation block, each process configures its own correctness routines.

3.1.2 Monitoring

In order to determine whether the execution of a speculative task is correct, the

speculative run-time system must ensure that the state of the speculative execu-

tion corresponds with the state of the non-speculative execution. Because each

task’s execution is sequestered in its own process the state is defined by the mem-

ory contents of each process.

Because the speculative task is forked from the sequential task we know that

the initial state of the two processes is identical. As a result, we need only track

the changes made in each process and compare that subset of memory. Because

the two tasks are executed with processes we can monitor those changes using two

operating system constructs: memory access protection and signal handlers.

38

The signal handler routine has three basic responsibilities: to ensure the vio-

lation is a result of the run-time system monitoring, to record the access for later

reference, and to remove the restriction.

The operating system detects memory access violations in the normal course

of operation in order to protect processes. Because a process might access regions

of memory in violation of the operating systems typical restrictions the run-time

system must ensure such accesses are not allowed to pass. The runtime must dif-

ferentiate between access to memory regions that it has restricted, and access the

program should never be permitted to make. The signal itself identifies whether

the access was made to a memory location that is not mapped (maperr) or to a

region of memory to which the process does not have access (accerr).

Once the location of the access has been deemed legitimate, the run-time

system must record the access for later reference. The speculative run-time system

uses an access bitmap to represent each block of memory. One bit for each page

equals one word for every 32 pages. With a page size of 4096 bytes the access map

uses one byte to record accesses on 131,072 bytes. Because much of the access

map will be zeros, and most of it will not be modified, the OS will typically be

able to map several of these pages to the same zero-filled data.

Once the access has been recorded the process must be allowed to continue

its operation. Additionally, there is no reason to record future access to the same

block. The run-time system can safely remove memory protection for the current

block.

3.1.3 Verification

Once the sequential process has advanced far enough the run-time system must

verify that the speculative execution is correct. Such verification requires an

analysis of the access maps for both processes, but without special consideration

39

each process would only have access to its own map. The run-time system can

facilitate the access map analysis in two ways. One option is to push a copy of

one of the maps using a POSIX pipe established during the spawning process as

indicated in Section 3.1.1. In practice it is only necessary to transfer the non-zero

portions of the map. The second option is to create the maps in a segment of

memory that has been explicitly shared between the two processes.

The details of verification — notably the precise point at which it can be

performed and which types of accesses need to be validated — depend on the

type of speculation being performed. These details are discussed in Sections 4.3.3

and 5.3.3.

3.1.4 Abort

Speculative execution requires a mechanism for unrolling or aborting the specula-

tive portion of a process when the speculation proved to be incorrect. In order to

abort speculative execution that has proven incorrect, process-based speculation

can simply kill the running process. Because the Linux kernel protects the mem-

ory space of running processes from access by other processes, it is not possible for

the speculative process to directly affect the non-speculative portion of execution.

As a result, once the speculative execution is killed the non-speculative process

continues as it would in the sequential case.

3.1.5 Commit

The approach for committing a speculative task amounts to terminating the non-

speculative process and allowing execution to continue based on what was com-

puted speculatively. In addition, the meta-data used to track memory accesses

must be updated to reflect the fact that the speculative process is no longer spec-

ulative.

40

3.2 Advantages

Using processes for speculative parallelism has a major advantage over other

thread based approaches. Perhaps the most significant of these is portability.

By using POSIX constructs the speculative run-time system can be built for any

POSIX operating system. The system does not rely on any specific hardware

architecture or features. The run-time system and compiler support presented in

this work have been built and executed on Linux and Mac OS X.

The access monitoring used by thread based approaches relies on instrumen-

tation to data accesses. This instrumentation must be explicitly applied to both

program code and any libraries used during execution. The process-based system

does not require any attention to external libraries to perform correctly. This

flexibility also improves the portability of the run-time system because only the

annotated source code needs to be recompiled.

Process based memory access monitoring also has the advantage of incurring a

constant cost for each location accessed, rather than a cost at every single access

as in a thread based system. Additionally, because the monitoring is done at the

page level, this cost can be amortized for large tasks with multiple accesses to the

same page.

In addition to monitoring the locations of data accesses, the process-based

system compares the data values for conflicts. Using value based checking guar-

antees that identical changes to the same data will not be reported as a conflict,

a problem known as false sharing. In order to support value based checking, a

run-time system must maintain multiple copies of the data. While the process-

based run-time system gains this for free through the operating system’s virtual

memory system, thread based systems need to introduce additional data copies.

Additionally, these multiple copies must be explicitly managed to differentiate

access and guarantee that rollback is possible.

41

3.3 Disadvantages

The process-based protection has a high overhead. However, much of this over-

head is inherently unavoidable for a software scheme to support unpredictable

computations. A major goal of this thesis is to show that general protection can

be made cost effective by three techniques. The first is programmable speculation.

Since the overhead depends on the size of (write) accessed data rather then the

length of the ppr region, it can be made negligible if the size of the parallel task

is large enough.

Second, most overheads—starting, checking, and committing—are off the crit-

ical path, so the non-speculative execution is almost as fast as the unmodified

sequential execution. Moreover, a race is run in every parallel region, where the

correct speculative result is used only when the speculation finishes faster than

the would-be sequential execution. The overhead of determining the winner of

this race is placed in the speculative execution, off the critical path.

Last, the run-time system uses value-based checking, which is more general

than dependence-based checking, and satisfies the Bernstein conditions [5]. Value-

based checking permits parallel execution in the presence of true dependencies

and it is one of the main differences between process-based system and existing

thread-based systems (as discussed in Section 4.3.2).

3.4 Special Considerations

3.4.1 Input and Output

To ensure that the output of a program running with speculative parallelism

support is correct, we ensure output is produced only by a non-speculative process

or by a speculative process that is known to be correct and is serialized in the

42

correct order. Until a speculative process has confirmed that its initialization and

execution was correct (i.e., that all previous speculation was correct), it buffers all

terminal output and file writes. Given correct execution, any output the process

produces will be the same as what the sequential program would have generated.

Program output buffering is established by creating a temporary file in which

to buffer the output that would otherwise be sent to the standard output. Such

a file is created by the run-time system each time a new speculative process is

created. At link time, we use a linker option1 to replace calls to the known input

and output functions with wrappers included with the run-time library. These

wrappers send file output to the redirection temporary file (in the case of printf)

or abort the speculative process (in all other cases). Although it should be possible

to detect writes to the standard error output using fprintf, such support has not

been implemented.

The task of committing the redirected output is addressed by rewinding to

the beginning of the redirection temporary file, reading it in blocks, and writing

those blocks to the standard output. If the speculative process is aborted, the

temporary redirection file is closed and deleted.

3.4.2 Memory Allocation

Dynamic memory allocation can potentially pose a problem for speculative exe-

cution because, unlike stack allocation, its implementation is library based and

the mechanism is not known in advance. The root of the problem for specula-

tive execution is that the implementation may not return the same sequence of

memory locations when the same sequence of requests are made. Even in cases

where the speculative and non-speculative are performing exactly the same com-

putations, the value of some of their pointers may differ because the dynamic

1The gnu linker supports a --wrap option.

43

allocation return a different location. Additionally, comparing the changes the

processes have made is complicated by the need to recognize that different areas

of memory should be treated as though they were the same.

3.4.3 System Signals

The speculative parallel run-time system uses operating system signals to indicate

or initiate state changes among the running processes. The total number of avail-

able signals is limited, and the user program that is being extended may be relying

on some of the same signals. Some of the signals we’re using are slightly reinter-

preted (for example special action may be taken on termination) while others have

no default meaning.

The run-time system does not attempt to preserve any existing signal handlers

installed by the user program, but it would be extended to identify them. The

user installed signal handler can be stored and invoked from within the runtime’s

handler. While using signals would still provide a means to actively alert another

process, we would also need to differentiate signals initiated by the run-time sys-

tem from those of the user program. This could be accomplished using a shared

flag, which the run-time system would consult before either dispatching the signal

to the original handler or processing it.

Ultimately, it is not possible to guarantee that the user program does not install

a new signal handler during execution, over-writing the run-time system’s handler

functions. One solution would be to replace or wrap the handler installation

functions to ensure the run-time system’s handlers are preserved, while any new

handlers are indirectly dispatched. Because the signals the run-time system is

using are intended for user programs, this change could be performed during

compilation.

44

4 Speculative Parallelism

Introduction

In this chapter I describe a type of process-based speculative execution referred

to as Behavior Oriented Parallelism (or bop). The bop system is designed to

introduce parallelism into sequential applications. Many sequential applications

are difficult to parallelize because of problems such as unpredictable data access,

input-dependent parallelism, and custom memory management. These difficulties

motivated the development of a system for behavior-oriented parallelization, which

allows a program to be parallelized based on partial information about program

behavior. Such partial information would be typical of a user reading just part of

the source code, or a profiling tool examining a small number of inputs.

The bop style of speculative parallelism allows for some portions of code to

be marked as potentially safe for parallel execution. I refer to these regions of

code as possibly parallel regions, abbreviated ppr. The goal of bop is to allow a

programmer or an analysis tool to provide hints about parallel execution without

needing to guarantee that the parallelism is safe in all cases.

In Section 4.2 I describe the programmatic way in which code is annotated for

bop. The burden on the programmer is intended to be minimal, and the interface

45

to be a natural extension of the existing program. In Section 4.3 I describe how

the run-time system manages the speculative execution. In Section 4.6 I show

an evaluation of performance gains using the bop system, which has improved

the whole-program performance by integer factors for a Lisp interpreter, a data

compressor, a language parser, and a scientific library on a multicore personal

computer.

4.1 Design

The bop system uses concurrent executions to hide the speculation overhead off

the critical path, which determines the worst-case performance where all specula-

tion fails and the program runs sequentially.

4.1.1 Lead and Spec Processes

The execution starts as the lead process, which continues to execute the program

non-speculatively until the program exits. At a pre-specified speculation depth k,

up to k processes are used to execute the next k ppr instances. For a machine

with p available processors, the speculation depth is set to p− 1 to make the full

use of the CPU resource.

Figure 4.1 illustrates an example run-time setup of either the sequential execu-

tion or the speculative execution of three ppr instances. As shown in Part 4.1(b),

when the lead process reaches the start marker mbP , it forks the first spec process

and continues to execute the ppr instance P . The first spec jumps to the end

marker of P and executes the next ppr instance Q. At mbQ, it forks the second

spec process, which jumps ahead to execute the third ppr instance R.

At the end of P , the lead process becomes the understudy process, which re-

executes the next ppr instance non-speculatively. In addition, it starts a parallel

46

27

P

Q

R

mbP

meP

mbQ

meQ

mbR

meR

P

Q

Q

R(partial)

R

mbP

meP

mbQ

meQ

meP

meQ me

R

spec 1 starts

spec 2 starts

spec 1commits

(b) A successful parallel execution, with lead on the left, spec 1 and 2 on the right.

Speculation starts by jumping from the start to the end marker. It commits when

reaching another end marker.

(a) Sequential execution of PPR

instances P, Q, and R and their start and

end markers.

(

(

(

(

(

(

(

(

(

(

(

((

spec 2 finishes firstand aborts understudy

(parallel exe. wins)(

(

(

(spec 2

commits

mbR

understudybranch

starts

lead process

next lead

(a) Sequential exe-cution.

P

Q

R

mbP

meP

mbQ

meQ

mbR

meR

P

Q

Q

R(partial)

R

mbP

meP

mbQ

meQ

meP

meQ me

R

spec 1 starts

spec 2 starts

spec 1commits

(b) A successful parallel execution, with lead on the left, spec 1 and 2 on the right.

Speculation starts by jumping from the start to the end marker. It commits when

reaching another end marker.

(a) Sequential execution of PPR

instances P, Q, and R and their start and

end markers.

(

(

(

(

(

(

(

(

(

(

(

(

(

spec 2 finishes firstand aborts understudy

(parallel exe. wins)

(

(

(

(

spec 2commits

mbR

understudybranch

starts

lead process

next lead

(b) Parallel execution.

Figure 4.1: Sequential and speculative execution of PPR instances P, Q, and Rwith their start and end markers. In the successful parallel execution the leadis depicted on the left. Speculation starts by jumping from the start to the endmarker, and commits when reaching another end marker.

(a) Sequential execution.

P

Q

R

mbP

meP

mbQ

meQ

mbR

meR

P

Q

Q

R(partial)

R

mbP

meP

mbQ

meQ

meP

meQ me

R

spec 1 starts

spec 2 starts

spec 1commits

(b) A successful parallel execution, with lead on the left, spec 1 and 2 on the right.

Speculation starts by jumping from the start to the end marker. It commits when

reaching another end marker.

(a) Sequential execution of PPR

instances P, Q, and R and their start and

end markers.

(

(

(

(

(

(

(

(

(

(

(

(

(

spec 2 finishes firstand aborts understudy

(parallel exe. wins)

(

(

(

(

spec 2commits

mbR

understudybranch

starts

lead process

next lead

(b) Parallel execution.

Figure 4.1: Sequential and speculative execution of PPR instances P, Q, and Rwith their start and end markers. In the successful parallel execution the leadis depicted on the left. Speculation starts by jumping from the start to the endmarker, and commits when reaching another end marker.

branch (a process in our current implementation) to check the correctness of spec

1. If no conflict is detected, the checking branch commits with spec 1, and the

two are combined into a single process. More speculation processes are handled

recursively in a sequence. The kth spec is checked and combined after the first

k − 1 spec processes commit. When multiple spec processes are used, the data

copying is delayed until the last commit. The changed data are copied only once

instead of multiple times in a rolling commit.

The speculation runs slower than the normal execution because of the costs re-

sulting from initialization, checking, and commit. The costs may be much higher

in process-based systems than in thread-based systems. In the example in Fig-

ure 4.1(b), the startup and commit costs, shown as gray bars, are so high that

the parallel execution of spec 1 finishes slower than the sequential understudy.

However, by that time spec 2 has finished and is ready to commit. The second

commit finishes before the understudy finishes, so spec 2 aborts the understudy

47

and becomes the next lead process.

bop executes ppr instances in a pipeline and shares the basic property of

pipelining: if there is an infinite number of pprs, the average finish time is de-

termined by the starting time not the length of each speculation. In other words,

the parallel speed is limited only by the speed of the initialization and the size

of the sequential region outside ppr. The delays during and after speculation do

not affect the steady-state performance. This may be counter intuitive at first

because the commit time does not matter even though it is done sequentially. In

the example in Figure 4.1(b), spec 2 has similar high startup and commit costs

but they overlap with the costs of spec 1. In experiments with real programs, if

the improvement jumps after a small increase in the speculation depth, it usually

indicates a high speculation overhead.

4.1.2 Understudy: Non-speculative Re-execution

bop assumes that the probability, the size, and the overhead of parallelism are all

unpredictable. The understudy provides a safety net not only for correctness when

the speculation fails, but also for performance when speculation is slower than

the sequential execution. For performance, bop holds a two-way race between the

non-speculative understudy and the team of speculative processes.

The non-speculative team represents the worst-case performance along the

critical path. If all speculation fails, it sequentially executes the program. As I

will explain below, the overhead for the lead process consists only of the page-based

write monitoring for the first ppr instance. The understudy runs as the original

code without any monitoring. As a result, if the granularity of ppr instance is

large or when the speculation depth is high, the worst-case running time should

be almost identical to that of the unmodified sequential execution. On the other

hand, whenever the speculation finishes faster than the understudy, it means a

48

performance improvement over the would-be sequential execution.

The performance benefit of understudy comes at the cost of potentially re-

dundant computation. However, the cost is at most one re-execution for each

speculatively executed ppr, regardless of the depth of the speculation.

Using the understudy, the worst-case parallel running time is equal to the

best-case sequential time. One may argue that this can be easily done by running

the sequential version side by side in a sequential-parallel race. The difference is

that the bop system is running a relay race for every group of ppr instances. At

the whole-program level it is sequential-parallel collaboration rather than compe-

tition because the winners of each relay are joined together to make the fastest

time. Every improvement in time counts when speculation runs faster, and no

penalty is incurred when it runs slower. In addition, the parallel run can benefit

from sharing read-only data in cache and memory, while multiple sequential runs

cannot. Finally, running two instances of a program is not always possible for

a utility program, since the communication with the outside world often cannot

be undone. In bop, unrecoverable I/O and system calls are placed outside the

parallel region.

4.1.3 Expecting the Unexpected

Figure 4.1 shows the expected behavior when an execution of pprs runs from

BeginPPR to EndPPR. In general, the execution may reach an exit (normal or

abnormal) or an unexpected ppr marker. Table 4.1 shows the actions taken by

the lead process, its understudy branch, and spec processes when encountering

an exit, error, or unexpected ppr markers.

The abort by spec in Table 4.1 is conservative. It is possible for, speculation

to reach a program exit point during correct execution, so an alternative scheme

might delay the abort and salvage the work if it turns out to be correct. We favor

49

Table 4.1: Speculation actions for unexpected behaviorbehavior prog. exit or error unexpected ppr markers

lead exit continueunderstudy exit continuespec abort speculation continue

the conservative design for performance. Although it may recompute useful work,

the checking and commit cost will never delay the critical path.

The speculation process may also allocate an excessive amount of memory

and attempt permanent changes through I/O and other OS or user interactions.

The latter cases are solved by aborting the speculation upon file reads, system

calls, and memory allocation exceeding a pre-defined threshold. The file output is

managed by buffering and is either written out or discarded at the commit point.

The current implementation supports stdout and stderr for the pragmatic purpose

of debugging and verifying the output. Additional engineering effort could add

support for regular file I/O.

Strong Isolation

I describe the bop implementation as having strong isolation because the inter-

mediate results of the lead process are not made visible to speculation processes

until the lead process finishes the first ppr. Strong isolation comes naturally with

process-based protection. It is a basic difference between bop and thread-based

systems, where the updates of one thread are visible to other threads, which I

describe as weak isolation.I discuss the control aspect of the difference here and

complete the rest of comparisons in Section 4.3.2 after describing the data pro-

tection.

Weak isolation allows opportunistic parallelism between two dependent threads,

if the source of the dependency happens to be executed before the sink. In the

50

bop system, such parallelism can be made explicit and deterministic using ppr

directives by placing dependent operations outside the ppr region. As an exam-

ple, the code outside ppr in Figure 4.1 executes sequentially. At the loop level,

the most common dependency comes from the update of the loop index variable.

With ppr, the loop control can be easily excluded from the parallel region and

the pipelined parallelism is definite instead of opportunistic.

The second difference between strong and weak isolation is that strong isola-

tion does not need synchronization during the parallel execution but weak isolation

needs to synchronize between the lead and the spec processes when communi-

cating the updates between the two. Since the synchronization delays the non-

speculative execution, it adds visible overheads to the thread-based systems when

speculation fails. bop does not suffer this overhead.

Although strong isolation delays data updates, it detects speculation failure

and success before the speculation ends. Like systems with weak isolation, strong

isolation detects conflicts as they happen because all access maps are visible to all

processes for reads (each process can only update its own map during the parallel

execution). After the first ppr, strong isolation can check for correctness before

the next speculation finishes by stopping the speculation, checking for conflicts,

and communicating data updates. As a design choice, bop does not abort spec-

ulation early because of the property of pipelined parallelism, explained at the

end of Section 4.1.1. The speculation process may improve the program speed,

no matter how slowly it executes, when enough of them are working together.

51

4.2 Programming Interface

In addition to the ppr markers, the bop programming interface two other im-

portant components. First, the programmer may provide a list of global and

static variables that are privatizable within each parallel process. By specifying

where the variables are initialized, the system can treat their data as shared until

the initialization and as private thereafter. The third component is described in

Section 4.2.3.

4.2.1 Region Markers

The bop programming interface allows a programmer to indicate what portions of

code are candidates for parallelism. The primary component of the bop program-

ming interface is the BeginPPR function that denotes the beginning of a parallel

region. The return value of BeginPPR is a Boolean value where truth corresponds

to execution of the speculative code path. Put in terms of the run-time system,

the speculative process receives a non-zero return value while the non-speculative

process receives a return value of zero.

A call to BeginPPR is typically wrapped in a conditional statement to control

the flow of execution through the two paths of execution. Listing 4.2.1 illustrates

an example use of a ppr to parallelize a loop. Each iteration of the loop computes

the value to fill one element of a table based on the corresponding index. If one

assumes that the function compute is free of side-effects, then each iteration of the

loop can be executed in parallel with the others. Using ppr guarantees correct

execution even when the assumption about compute’s purity is not valid.

As counterpart to the BeginPPR marker used to indicate the start of a possible

parallel region, the bop interface provides an EndPPR marker to finalize the region.

52

Algorithm 4.2.1 Example use of bop to mark a possibly parallel region of codewithin a loop.

fo r ( i n t i = 0 ; i < N; ++i ) {i f ( ! BeginPPR (0 ) ) {

tab [ i ] = compute ( i ) ;} EndPPR ( 0 ) ;

}

These two functions both accept a single scalar value that identifies the region to

ensure the markers are properly matched, which allows for nesting. Using the

identifier, an incorrectly matched marker can be safely ignored on the assumption

that another marker matches it and is also ignored.

Algorithm 4.2.2 Example use of bop including EndPPR marker.

fo r ( i n t i = 0 ; i < N; ++i ){i f ( ! BeginPPR ( ) ) {

tab [ i ] = compute ( i ) ;}

}EndPPR ( ) ;agg r ega t e ( tab ) ;

In the loop body example shown in Listing 4.2.1, there is little meaning to

the else branch of the BeginPPR conditional. One can view the second branch as

containing any execution until the next ppr marker of any kind. In straight-line

code it may be more clean to explicitly enclose a block of code within an else

branch to place it in juxtaposition to the speculative path. The code in List-

ing 4.2.3 represents a case in which the else branch is explicitly used to demarcate

distinct paths of execution that may be processed in parallel. Note that there is

no reason that a simple pair of if/else must be used, and in the listing a nest

of conditions is used.

53

Algorithm 4.2.3 Example use of bop in a non-loop context.

i f ( ! BeginPPR ( ) ){f ( tab1 ) ;

} e l se i f ( ! BeginPPR ( ) ){f ( tab2 ) ;

} e l se {f ( tab3 ) ;

}EndPPR ( ) ;

Explicitly Matched Markers

While multiple BeginPPR(p) invocations may exist in the code, a EndPPR(p) must

be unique for the same p, and the matching markers must be inserted into the

same function. The exact code sequence in C is as follows:

BeginPPR if (BeginPPR(p)) goto EndPPR p;

EndPPR EndPPR(p); EndPPR p:;

In the presence of unpredictable control flow, there is no guarantee that a start

marker will be correctly followed by its end marker, or that the matching markers

are executed the same number of times. For example, a longjmp in the middle

of a parallel region may cause the execution to back out and re-enter.

The bop system constructs a sequence of zero or more non-overlapping ppr

instances at run time using a dynamic scope. At any point t, the next ppr instance

starts from the first BeginPPR start marker operation after t and then ends at the

first EndPPR end marker operation after the BeginPPR. For example, assume the

program has two ppr regions P and Q, which are marked by the pairs {BP , EP}

and {BQ, EQ} respectively. If the program executes from the start t0, invoking

the markers six times from t1 to t6 as in Figure 4.2(a), then the two dynamic ppr

instances are depicted in Figure 4.2(b). The ppr range from t1 to t3 and from t4

54

BP

t0 t1 t2 t3 t4 t5 t6

BP BQEP EP EQ

t0 t1 t3 t4 t6

PPRP PPRQ

(a) Sequential execution

BP

t0 t1 t2 t3 t4 t5 t6

BP BQEP EP EQ

t0 t1 t3 t4 t6

PPRP PPRQ(b) Parallel execution

Figure 4.2: Example of matching ppr markers

to t6, and will be run in parallel. The other fragments of the execution will be

run sequentially, although the portion from t3 to t4 will be speculative.

Compared to the static and hierarchical scopes used by most parallel con-

structs, the dynamic scope lacks the structured parallelism to model complex

task graphs and data flows. While it is not a good fit for static parallelism, it is

a useful solution for the extreme case of dynamic parallelism in unfamiliar code.

A coarse-grain task often executes thousands of lines of code, communicates

through dynamic data structures, and has non-local control flows. Functions may

be called through indirect pointers, so parallel regions may be interleaved instead

of being disjoint. Some forms of non-local error handling or exceptions may be

frequent, for example when an interpreter encounters a syntax error. Some forms

are rare, as found in the error checking and abnormal exit found in the commonly

used gzip program’s compression code. Although no error has ever happened

in our experience, if one cannot prove the absence of error in such software of

this size, the dynamic scopes implemented by a ppr can be used to parallelize

the common cases while guarding against unpredictable or unknown entries and

exits.

Since the ppr markers can be inserted anywhere in a program and executed

in any order at run time, the system tolerates incorrect marking of parallelism,

which can easily happen when the region is marked by a profiling tool based on

a few inputs or given by a user unfamiliar with the code. The markers, like other

aspects of the interface, are programmable hints where the quality of the hints

55

affects the parallelism but not the correctness or the worst-case performance.

4.2.2 Post-Wait

The basic ppr structure allows for regions of code to be executed in parallel if

there are no dependencies carried from one to another. In many cases a loop body

may have carried dependencies, but be parallelizable if care is taken. Consider

a loop that is structured in stages so that some stages carry a dependency, but

the dependency is consumed by the same stage in the next iteration. In such a

scenario, the stages of the loop body can be viewed as stages of a pipeline.

Post-Wait is an extension of the basic ppr mechanism provided by the bop

system to allow for pipelining portions of the possibly parallel region. Using the

post-wait interface the speculative processes can be synchronized so that the writes

in the earlier process occur before the corresponding reads during run time.

Algorithm 4.2.4 Example of a pipelined loop body.

fo r ( i n t i = 0 ; i < N; ++i ) {B[ i ] = f (A [ i ] ) ;C [ i ] = g (A[ i ] ) ;D[ i ] = h (B[ i ] , C [ i ] ) ;

}

4.2.3 Feedback

The third component of the bop interface is run-time feedback to the user. When

speculation fails, the system generates output indicating the cause of the failure,

particularly the memory page on which receives conflicting accesses occurred. In

our current implementation, global variables are placed on separate memory pages

by the compiler. As a result, the system can output the exact name of the global

variable when it causes a conflict. A user can then examine the code and remove

56

Algorithm 4.2.5 Example use of bop post/wait.

fo r ( i n t i = 0 ; i < N; ++i ) {i f ( ! BeginPPR ( ) ) {B[ i ] = f (A [ i ] ) ;BOP post ( ‘B ’ ) ;C [ i ] = g (A[ i ] ) ;BOP wait ( ‘B ’ ) ;D[ i ] = h (B[ i ] , C [ i ] ) ;

}EndPPR ( ) ;

}

the conflict by marking the variable privatizable or moving the dependency out

of the parallel region.

Three features of the API are especially useful for working with large, unfa-

miliar code. First, the user does not write a parallel program and never needs

parallel debugging. Second, the user parallelizes a program step by step as hid-

den dependencies are discovered and removed one by one. Finally, the user can

parallelize a program for a subset of inputs rather than all inputs. The program

can run in parallel even if it has latent dependencies.

4.3 Run-Time System

4.3.1 Creation

On the first instance of BeginPPR the run-time system initializes the signal han-

dlers and memory protection used by all of the subsequent process. The beginning

of a possibly parallel region is marked by a call to the system fork function. The

fork function creates a new operating system process which will act as the spec-

ulative process. This new process is considered to be the child of the preexisting

process, which is non-speculative. The original process returns immediately and

57

continues execution in non-speculative state.

4.3.2 Monitoring

The bop system guarantees that if the speculation succeeds the same user vis-

ible output is produced as in the sequential execution. bop partitions the ad-

dress space of a running program into three disjoint groups: shared, checked,

and private. More formally, Dall = Dshared + Dchecked + Dprivate, and any two of

Dshared, Dchecked, and Dprivate do not overlap.

For the following discussion we consider two concurrent processes — the lead

process that executes the current ppr instance, and the spec process that executes

the next ppr instance and the code in between. The cases for k (k > 1) speculation

processes can be proved by induction since they commit in a sequence in the bop

system.

Three types of data protection

Page-based protection of shared data All program data are shared at the

BeginPPR marker by default, and are protected at the memory page granularity.

During execution, the system records all global variables and the range of dy-

namic memory allocation. At BeginPPR, the system turns off write permission

for the lead process and read/write permission for the spec processes. It installs

customized page-fault handlers that loosen the permission for read or write upon

the first read or write access. At the same time, the handler records which type

of access each process has to each page. At commit time, each spec process is

checked in increasing order based on creation. The kth process fails if and only if

a page is written by the lead process and the previous k − 1 spec processes but

read by spec k. If speculation succeeds, the modified pages are merged into a

single address space at the commit point.

58

Algorithm 4.3.1 Listing of ppr creation.

i n t BOP PrePPR( i n t i d ) {i f ( mySpecOrder == specDepth ) return 0 ;

switch ( myStatus ) {defau l t : return 0 ; // ignore nested PPRs (status = MAIN)

case CTRL : // CRTL is the initial statememset ( accMapPtr , 0 , ACC MAP SIZE ) ;myStatus = MAIN ;mySpecOrder = 0 ;

// signal handlers for monitoringSP se tupAct i on ( BOP SegvHandler , SIG MEMORY FAULT ) ;// signals for sequential−parallel race arbitrationSP se tupAct i on ( BOP RaceHandler , SIGUSR1 ) ;SP se tupAct i on ( BOP UndyTermHandler , SIGUSR2 ) ;

// fall throughcase SPEC :

ppr ID = i d ; // record identifier of this PPR

i n t f i d = f o r k ( ) ;i f (−1 == f i d ) return 0 ; // fork failurei f ( f i d > 0) { // the MAIN or older SPEC

specP id = f i d ; // track the SPEC process ’ IDi f ( myStatus==MAIN) BOP se tPro tec t i on (PROT READ) ;return 0 ;

} // the newer SPEC continues here

specP id = 0 ;myStatus = SPEC ;mySpecOrder++;

s e t p g i d (0 , SP gpid ) ;SP Red i r ec tOutput ( ) ;

i f ( mySpecOrder==1) // set this up only onceBOP setPro tec t i on (PROT NONE) ;

return 1 ;}

}

59

Algorithm 4.3.2 Examples of shared, checked, and private data

sha r ed = GetTable ( ) ;. . .whi le ( . . . ) {

. . .BeginPPR (1). . .i f ( . . . )

checked = checked + Search ( shared , x )I n s e r t ( p r i v a t e , new Node ( checked ) )

. . .i f ( ! e r r o r ) Rese t ( checked ). . .EndPPR(1). . .

}

By using Unix processes for speculation, the bop system eliminates all anti-

dependencies and output dependencies through the replication of the address

space, and detects true dependencies at run time. An example is the variable

shared in Figure 4.3.2, which may point to some large dictionary data structure.

Page-based protection allows concurrent executions as long as a later ppr does

not need the entries produced by a previous ppr. The overwrites by a later ppr

are fine even if the entries are used concurrently by a previous ppr.

The condition is significantly weaker than the Bernstein condition [5], which

requires that no two concurrent computations access the same data if at least

one of the two writes to it. The additional parallelism is possible because of

the replication of modified data, which removes anti-dependencies and output

dependencies. The write access by spec k never causes failure in previous spec

processes. As an additional optimization, the last spec process is only monitored

for data reads. In fact, when the system is limited to only one spec process, a

case termed co-processing, the lead process is monitored only for writes and the

spec only for reads.

60

Page-based protection has been widely used for supporting distributed shared

memory [29, 32] and many other purposes including race detection [49]. While

these systems enforce parallel consistency among concurrent computations, the

bop system checks for dependence violation when running a sequential program.

A common problem in page-level protection is false-positive alerts. We allevi-

ate this problem by allocating global variables on separate memory page. Writes

to different parts of a page may be detected by checking the difference at the end

of ppr, as in [29]. In addition, the shared data are never mixed with checked and

private data on the same page, although at run time newly allocated heap data

are private at first and then converted to shared data at EndPPR.

Value-based checking Typical dependence checking is based on data access

rather than data value. Although this type of checking is sufficient for correctness,

it is not necessary. Consider the variable checked in Figure 4.3.2, which causes

true dependencies because both the current and next ppr instances may read and

modify it. On the other hand, the reset statement at the end may re-install the

old value that checked had at the beginning of the ppr. The parallel execution is

still correct at run time despite the true dependence violation. This case is called

a silent dependence [53].

There is often no guarantee that the value of a variable is reset by EndPPR. In

the above example, the reset depends on a flag, so the “silence” is conditional.

Even after a reset, the value could be modified by pointer indirection in the general

case. Finally, the reset operation may assign different values at different times.

Hence run-time checking is necessary.

For global variables, the size is statically known, so the bop system allo-

cates checked variables in a contiguous region, makes a copy of their value at the

BeginPPR of the lead process, and checks their value at the EndPPR. For dynamic

data, the system needs to know the range of addresses and performs the same

61

checking steps. Checked data can be determined through profiling analysis or

identified by the user as described in more detail in Section 4.2.3. Since the values

are checked, incorrect hints would not compromise correctness. In addition, a

checked variable does not have to return to its initial value in every ppr instance.

Speculation still benefits if the value remains constant for just two consecutive

ppr instances.

Most silent dependencies come from implicit re-initialization of a variable.

Some examples are incrementing and decrementing a scope level when a compiler

compiles a function, setting and clearing traversed bits of the nodes in a graph

during a depth-first search, and filling then clearing the work-list in a scheduling

pass. Such variables that may take the same value at BeginPPR and EndPPR are

classified as checked data. In other words, the ppr execution may have no visible

effect on the checked data variable.

The shared data and checked data have a significant overlap, which is the set

of data that are either read only or untouched by the parallel processes. Data in

this set are classified as checked if their size is small; otherwise, they are shared.

A problem arises when different parts of a structure or array require different

protection schemes. Structure splitting, when possible, may alleviate the problem.

The correctness of checked data is not obvious because their intermediate

values may be used to compute other values that are not checked. I will present

a formal proof of the correctness to show how the three protection schemes work

together to cast a complete shield against concurrency errors.

Likely private data The third class of objects is private data, which is initial-

ized before being used and therefore causes no conflict. In Figure 4.3.2, if private

is always initialized before it is used, the access in the current ppr cannot affect

the result of the next ppr, so any true dependency caused by it can be ignored.

Private data come from three sources. The first is the program stack, which

62

includes local variables that are either read-only in the ppr, or always initialized

before use. Intra-procedure dataflow analysis is capable of identifying such data

for most programs. When the two conditions of safely cannot be guaranteed by

compiler analysis, for example due to unknown control flow or the escape of a local

variable’s address into the program heap, we redefine the local variable to be a

global variable and classify it as shared data. Recursive functions are not handled

specially, but could be managed either using a stack of pages or by disabling the

ppr.

The second source of private data is global variables and arrays that are al-

ways initialized before the use in the ppr. The standard technique to detect this

is inter-procedural kill analysis [1]. In general, a compiler may not always ascer-

tain all cases of initialization. For global data whose access is statically known

in a program, the compiler automatically inserts calls after the initialization as-

signment or loop to classify the data as private at run time. Any access by the

speculation process before the initialization causes it to be treated as shared data.

For (non-aggregate) data that may be accessed by pointers, the system places it

on a single page and treats it as shared until the first access. Additionally, we

allow the user to specify the list of variables that are known to be written before

read in ppr. These variables are reinitialized to zero at the start of a ppr instance.

Since we cannot guarantee write-first access in all cases, we call this group likely

private data.

The third type of private date is newly allocated data in a ppr instance. Before

BeginPPR, the lead process reserves regions of memory for speculation processes.

Speculation would abort if it allocates more than the capacity of the region. The

main process does not allocate into the region, so at EndPPR, the newly allocated

data can be merged with the data from the speculation process. For programs that

use garbage collection, we encapsulate the heap region of spec processes, which

we will describe when discussing the test of a lisp interpreter. Another solution is

63

to ignore garbage collection, which will cause speculation to fail if it is initiated

during a ppr instance because of the many changes it makes to the shared data.

A variable is marked by bop private if its value is assigned before it is used

within a ppr task. Because the first access is a write, the variable does not

inherit value from prior tasks. Verifying the suggestion requires capturing the

first access to a variable, which can be costly if the variable is an array or a

structure. For efficiency we use a compromise. We insert code at the start of

the ppr to write a constant value in all variables that are marked bop private. If

the suggestion is correct, the additional write adds a small extra cost but does

not change the program semantics. If the suggestion is wrong, the program may

not execute correctly, but the sequential version has the same error, and the error

can be identified using conventional debugging tools. Under this implementation,

bop private is a directive rather than a hint, unlike other bop primitives.

Overheads on the critical path The three data protection schemes are sum-

marized and compared in Table 4.2. Most of the overhead of speculation — the

forking of speculation processes, the change of protection, data replication and

read and write monitoring, the checking of access maps for conflicts, the merging

of modified pages, and the competition between the understudy and the spec pro-

cesses — are off the critical path. Therefore, the relation between the worst-case

running time Tmaxparallel and the time of unmodified sequential program Tseq is

Tmaxparallel = Tsequential + c1 ∗ (Sshared/Spage) + c2 ∗ (Smodified by 1st ppr + Schecked)

The two terms after Tseq are the cost from data monitoring and copying on the

critical path, as explained below.

For monitoring, at the start of ppr, the lead process needs to set and reset

the write protection and the access map for shared data before and after the first

ppr instance. The number of pages is the size of shared data Sshared divided

by the page size Spage plus a constant cost c1 per page. During the instance, a

64

write page fault is incurred for every page of shared data modified in the first ppr

instance. The constant per page cost is negligible compared to the cost of copying

a modified page.

Two types of copying costs may appear on the critical path. The first is for

pages of shared data modified by the lead process in the first ppr instance and

(among those) pages modified again by the understudy. The second cost is taking

the snapshot of checked data. The cost in the above formula is the worst case,

though the copy-on-write mechanism in modern OS may completely hide both

costs.

Data copying may hurt locality across ppr boundaries, although the locality

within is preserved. The memory footprint of a speculative run is larger than the

sequential run as modified data are replicated. However, the read-only data are

shared by all processes in main memory and in shared cache, which is physically

indexed. As a result, the footprint may be much smaller than running k copies of

a program.

A Formal Proof of Correctness

It is sufficient to prove the correctness for a single instance of the parallel execution

between two ppr instances. An abstract model of an execution is defined by:

Vx : a set of variables. Vall represents all variables in memory.

St : the content of V at time t.

Stx : the state of Vx at t.

rx : an instruction. The instructions we consider are the markers of the two pprs,

P and Q, P b, P e, Qb, and Qe (corresponding to mbP , me

P , mbQ, and me

Q in

Section 4.2.1). P and Q can be the same region.

65

Table 4.2: Three types of data protectionshared data

protection: Not written by lead and read by specgranularity: page/elementsupport: compiler, profiler, run-timecritical path overhead: 1 fault per modified page

checked dataprotection: Value at BeginPPR is the same at EndPPR in lead.

Concurrent read/write allowed.granularity: elementsupport: compiler, profiler, run-timecritical path overhead: copy-on-write

private dataprotection: no read before 1st write in spec. Concurrent read-

/write allowed.granularity: elementsupport: compiler (run-time)critical path overhead: copy-on-write

〈rx, StV 〉 : a point in execution where in terms of instruction and state.

〈r1, St1all〉

p=⇒ 〈r2, St2

all〉 : execution of a process p from one point to another.

Figure 4.3 shows the parallel execution and the states of the lead and the spec

processes at various times. If a parallel execution passes the three data protection

schemes, all program variables in our abstract model can be partitioned into the

following categories:

• Vwf : variables whose first access by spec is a write. wf stands for write first.

• Vexcl lead: variables accessed only by lead when executing the first ppr

instance P .

• Vexcl spec: variables accessed only by spec.

66

(a) sequential execution (b) parallel execution by three processes

main process(main)

(rb

, S init)

(re , Smid)

(re , Sseq)

(rb

, S init) (re , S init)

(re , Sspec)

(re , Smain) (re

, Smain)

(re , Sundy)

understudy process(undy)

speculation process(spec)

Figure 4.3: The states of the sequential and parallel execution

• Vchk: the remaining variables. chk stands for checked.

Vchk = Vall − Vwf − Vexcl lead − Vexcl spec

Examining Table 4.2, we see that Dshared contains data that are either accessed

by only one process (Vexcl lead and Vexcl spec), written before read in spec (Vwf ),

read only in both processes or not accessed by either (Vchk). Dprivate contains data

either in Vwf or Vchk. Dchecked is a subset of Vchk. In addition, the following two

conditions are met upon a successful speculation.

1. lead process reaches the end of P at P e, and the spec process, after leaving

P e, executes the two markers of Q, Qb and then Qe.

2. the state of Vchk is the same at the two ends of P (but it may change in the

middle), that is, Sinitchk = Slead

chk .

67

To analyze correctness, examine the states of the sequential execution, Sinit

at P b and Sseq at Qe of the sequential process seq, and the states of the parallel

execution, Sinit at P b, Slead at P e of the lead process and Sinit at P e and Sspec at

Qe of the spec process. These states are illustrated in Figure 4.3.

The concluding state of the parallel execution, Sparallel at Qe, is a combination

of Slead and Sspec after the successful speculation. To be exact, the merging step

copies the modified pages from the lead process to the spec process, so

Sparallel = Sspecall−excl lead + Slead

excl lead

In the following proof, each operation rt is defined by its inputs and outputs,

which all occur after the last input. The inputs are the read set R(rt). The out-

puts include the write set W (rt) and the next instruction to execute, rt+1. For

clarification, an operation is an instance of a program instruction. For the sim-

plicity of the presentation, symbol rx is overloaded as both the static instruction

and its dynamic instances. To distinguish in the text, former is referred to as an

instruction and the latter as an operation, so there may be only one instruction

rx but any number of operations rx.

Theorem:

If the spec process reaches the end marker of Q, and the protection in Table 4.2

passes, the speculation is correct, because the sequential execution would also

reach Qe with a state Sseq = Sparallel, assuming that both the sequential and the

parallel executions start with the same state, Sinit at P b.

Proof:

Consider the speculative execution, (P e, Sinit)spec=⇒ (Qe, Sspec), for the part of the

sequential execution, (P e, Smid)seq

=⇒ (Qe, Sseq). The correct sequential execution

is denoted as pe, r1, r2, · · ·, and the speculative execution as pe, r′1, r′2, · · ·. Proving

the above theorem must show that every operation r′t in the speculative execution,

68

and the corresponding operation rt in the sequential execution must:

1. map to the same instruction as rt

2. read and write the same variables with the same values

3. move to the same next instruction rt+1

Which is done through contradiction.

Assume the two sequences are not identical and let r′t be the first instruction

that produces a different value than rt, either by modifying a different variable,

the same variable with a different value, or moving next to a different instruction.

Since rt and r′t are the same instruction, the difference in output must be due to

a difference in the input.

Suppose rt and r′t read a variable v but see different values v and v′. Since the

values cannot differ if the last writes do not exist, let rv and r′v be the previous

write operations that produce v and v′. The operation r′v can occur either in spec

before r′t or in the lead process as the last write to v. The contradiction depends

on showing neither of these two cases is possible.

First, if r′v happens in spec, then it must produce the same output as rv per

our assumption that r′t is the first instruction to deviate. Second, r′v is part of

lead and produces a value not visible to spec. Consider the only way v can be

accessed. Given that r′v is the last write, v is read before being modified in spec,

and so it does not belong to Vwf or Vexcl lead. Neither is it in Vexcl spec since it is

modified in the lead process. The only case left is for v to belong to Vchk. Since

V leadchk = V init

chk , after the last write the value of v is restored to the beginning state

where spec starts and consequently cannot cause r′t in spec to see a different value

as rt does in the sequential run. Therefore rt and r′t cannot have different inputs

and produce different outputs, and the speculative and sequential executions must

be identical.

69

Since spec reads and writes correct values, Vwf , Vexcl spec, and the accessed

part of Vchk are correct. Vexcl lead is also correct because of the copying of the

their values at commit time. The remaining part of Vchk is not accessed by lead

or spec and still holds the same value as Sinit. It follows that the two states

Sparallel and Sseq are identical, which means that Sparallel is correct.

The above proof is similar to that of the Fundamental Theorem of Dependence

(Sec. 2.2.3 in [1]). While the proof in the book deals with statement reordering,

the proof here deals with region reordering and value-based checking. It rules

out two common concerns. First, that the intermediate values of checked data

never lead to incorrect results in unchecked data. Second, the data protection

always ensures the correct control flow by speculation. In bop, the three checking

schemes work together to ensure these strong guarantees.

Comparisons

Strong and weak isolation as discussed in Section 4.1.3 is a basic difference between

process-based bop and thread-based systems that include most hardware and

software speculation and transactional memory techniques. The previous section

discussed the control aspect, while the data protection and system implementation

are discussed below. The comparisons are summarized in Table 4.3.

Weak isolation needs concurrent access to both program data and system data,

as well as synchronization to eliminate race conditions between parallel threads

and between the program and the run-time system. The problem is complicated

if memory operations may be reordered by the compiler or by hardware, and

the hardware uses weak memory consistency, which does not guarantee correct

results without explicit synchronization. In fact, concurrent threads lack a well-

defined memory model [7]. A recent loop-level speculation system avoids race

conditions and reduces the number of critical sections (to 1) by carefully ordering

70

Table 4.3: Comparisons between strong and weak isolationduring speculation strong weak

data updates visible to outside no yesoverall overhead proportional to data size data usesynchronization on critical path none neededhardware memory consistency independent dependentsupport value-based checking yes notype of pipelined parallelism definite opportunistic

detect spec failure early yes yescan certify spec success early yes yes

the system code based on a sequential memory consistency model and adding

memory directives to enforce the order under relaxed consistency models [11].

In bop, parallel processes are logically separated. The correctness check is done

sequentially in rolling commits with a complete guarantee as stated on page 67.

There is no synchronization overhead on the critical path, and the compiler and

hardware are free to reorder program operations as they do for a sequential pro-

gram.

Weak isolation cannot efficiently support value-based checking. When data

updates are visible, the intermediate value of a checked variable can be seen by

a concurrent thread and the effect cannot be easily undo even if the variable

resumes the initial value afterward. For locks, this leads to the ABA problem,

where a thread may mistakenly hold a pointer whose value is the same, but the

referenced data has changed. A specific solution the ABA problem has been

developed for a software transactional memory system DSTM [25]. In hardware,

a correct value prediction may cause a thread to read at the wrong time and violate

the sequential consistency, so value prediction requires careful extra tracking by

hardware [37]. No software speculation systems use value-based checking. With

strong isolation in bop, the intermediate values of checked variables have no effect

on other processes, so value-based checking is not only correct but also adds little

cost on the critical path.

71

Value-based checking is different from value-specific dynamic compilation (for

example in DyC [20]), which finds values that are constant for a region of the

code rather than values that are the same at specific points of an execution (and

can change arbitrarily between these points). It is different from a silent write,

which writes the same value as the previous write to the variable. The bop run-

time software checking happens once per ppr for a global set of data, and the

correctness is independent of the memory consistency model of the hardware.

Most previous techniques monitor data at the granularity of array elements,

objects, and cache blocks; bop uses pages for heap data and padded variables

for global data. Paging support is more efficient for monitoring unknown data

structures but it takes more time to set up the permissions. It gives rise to

false sharing. The cost of page-based monitoring is proportional to the size of

accessed data (for the overhead on the critical path it is the size of modified data)

rather than the number of accesses as in thread-based systems, making page-based

protection especially suitable for coarse-grain parallelism.

4.3.3 Verification

In the case of speculative parallelism through pprs, verifying correct execution is

primarily the handled by the run-time monitoring. Any conflict between the main

and speculative processes will be detected when it occurs and does not require

additional analysis after the pprs complete.

In addition to verifying the correctness of the in-flight ppr executions, it is im-

portant to handle cases where one of the processes attempts to terminate (whether

or not the termination is the result of correct execution). It is always safe for the

non-speculative main process to exit. The nature of the main process is such that

a speculative processes must be running as well1, which must be terminated. It is

1The identifier main only exists within the context of a ppr.

72

worth noting that these speculative process are performing useless computation,

but there is no other useful ppr related work that could have been scheduled.

Reaching a program exit point in the understudy process is equivalent doing so

in the main process, except that buffered output must be committed.

If a speculative process reaches a program exit point it cannot be permitted to

commit normally. The current bop system simply forces the speculative process

to abort, which allows the corresponding understudy to eventually reach the exit

point and complete. If the speculative process is the child of another speculative

process, that process is notified of the failure, which allows it to change directly

to control status and elide any further coordination with the terminal speculative

process. An alternative is for the speculative process to treat the exit as the

end marker of the current ppr. This would cause the speculative process to

synchronize with the main process once it reaches its own end marker, after which

the process will potentially commit and exit without delaying until the understudy

reaches the same point.

4.3.4 Commit

The bop commit routine is invoked when a process reaches a EndPPR marker.

The functionality is dependent on the state of the process; sequential and control

processes are ignored, while the other states are handled specifically. If the iden-

tifier parameter does not match the current ppr identifier, then the end marker

is ignored.

The commit routine for the speculative process involves synchronizing with the

non-speculative processes, as well as maintaining order among the other specula-

tive processes. The actual tasks are provided in Listing 4.3.4 but can be summa-

rized as follows: We first pass our token to the next waiting speculative process.

We then wait for the previous speculative process to indicate that it has completed

73

Algorithm 4.3.3 Listing of bop termination routine

void a t t r i b u t e ( ( d e s t r u c t o r ) ) BOP End ( void ) {s t a t i c short f i n i s h e d = 0 ;i f ( f i n i s h e d ) return ;f i n i s h e d = 1 ;

switch ( myStatus ) {case SPEC :

// Tell the parent to start early termination .i f ( mySpecOrder > 1)

k i l l ( g e t pp i d ( ) , SIGUSR1 ) ;e x i t (EXIT SUCCESS ) ;

case UNDY:// Commit any buffered output.SP CommitOutput ( ) ;// (fall through)

case MAIN : case CTRL : case SEQ:BOP pipeClose ( ) ;

// Kill all run−time processes ( including self)k i l l ( SP gpid , SIGTERM) ;// Wait until signal propagates .pause ( ) ;e x i t (EXIT SUCCESS ) ;break ;

defau l t :e x i t ( EXIT FAILURE ) ;

}}

74

(assuming we are not the first). If this process is the first member of a group of

speculative processes then it must also wait for the previous group to have com-

mitted. Once the order among the speculative processes is confirmed the process

verifies the access maps are correct and copies the data changes it has made to

the next speculative process. Synchronization with the understudy is handled

by determining its process identifier, signaling the understudy, and waiting for

confirmation. Finally, the speculative process commits its output.

The commit routine for the understudy process is fairly simple. This is because

the understudy is considered to be on the critical path and much of the burden of

work has been placed elsewhere. Additionally, the understudy is not speculative.

As depicted in Listing 4.3.4, the understudy keeps a count of each EndPPR marker it

reaches. Because the speculative processes are placed into groups, the understudy

must complete all of the work of one group in order to succeed. The understudy

officially beats the speculative processes once it blocks the signal they would use to

declare completion. After this point the understudy can safely change its status to

control (which is not to be confused with being the lead process). The speculative

processes are killed, and output from the understudy committed.

The commit routine for the lead process (MAIN) is somewhat anomalous in

that it does not actually commit anything. The main process is responsible for

spawning the understudy process, and for synchronizing with the first speculative

process by passing its own data changes.

4.3.5 Abort

The abort routine basically just amounts to the speculative process exiting. Be-

cause the output has been buffered, and the operating system virtual memory

isolates any changes made, the process has no outside impact unless it is ex-

plicitly committed. The run-time system is structured so that if the speculative

75

Algorithm 4.3.4 Listing of ppr commit in the speculative process

void PostPPR spec ( void ) {i n t token ;s i z e t s i z e = s i z eo f ( token ) ;//remove the restrictive protections from memory pagesBOP setPro tec t i on (PROT READ |PROT WRITE ) ;// set the segfault handler back to the defaults i g n a l (SIG MEMORY FAULT , SIG DFL ) ;

i f ( myStatus==SPEC) // wait for main doneSP sync r ead ( l oH iP i p e s [ mySpecOrder ] [ 0 ] , &token , s i z e ) ;

i f ( BOP compareMaps ( ) ) e x i t ( 0 ) ; // access conflict

// If I am not the last spec task in the batchi f ( mySpecOrder < specDepth && ! e a r l yT e rm i n a t i o n ) {

PostPPR commit ( ) ; // never returnsreturn ;

}

// copy all updates to the last SPEC task (mySpecOrder)fo r ( i n t k = 0 ; k < specDepth ; k++)

SP Pul lDataAccordingToMap (WRITEMAP(mySpecOrder ) ,updateP ipe [ 0 ] , f a l s e ) ;

// clear the access mapmemset ( accMapPtr , 0 , ( specDepth+1)∗BIT MAP SIZE ) ;

// reset early termination flage a r l yT e rm i n a t i o n = f a l s e ;

// read the PID of the understudySP sync r ead ( undyCreatedP ipe [ 0 ] , &token , s i z e ) ;k i l l ( token , SIGUSR1 ) ; // tell understudy of our progress// wait for acknowledgement from the understudySP sync r ead ( undyConcedesPipe [ 0 ] , &token , s i z e ) ;// spec wins

myStatus = CTRL ;

SP CommitOutput ( ) ;}

76

Algorithm 4.3.5 Listing ppr commit in the understudy process

// BOP PostPPR for the understudyvoid PostPPR undy ( void ) {++undyWorkCount ;

// UNDY must catch SPECsi f ( undyWorkCount < specDepth ) return ;

// ignore notices from the SPEC (the UNDY has won)s i gp rocmask (SIG BLOCK , &sigMaskUsr1 , NULL ) ;myStatus=CTRL ;undyWorkCount = 0 ;

memset ( accMapPtr , 0 , ( specDepth+1)∗BIT MAP SIZE ) ;mySpecOrder = 0 ;

// Indicate the success of the understudy.k i l l (−SP gpid , SIGUSR2 ) ;// Explicitly kill the first SPEC process .k i l l ( specPid , SIGKILL ) ;

s i gp rocmask (SIG UNBLOCK , &sigMaskUsr1 , NULL ) ;

SP CommitOutput ( ) ;}

77

Algorithm 4.3.6 Listing of ppr commit in the main process

void PostPPR main ( void ) {

i f ( e a r l yT e rm i n a t i o n ) {// Speculation has failed . Restart the next round.myStatus = CTRL ;e a r l yT e rm i n a t i o n = f a l s e ;return ;

}

// open page protection for understudyBOP setPro tec t i on (PROT READ |PROT WRITE ) ;

// start the understudyi n t f i d = f o r k ( ) ;switch ( f i d ) {case −1: a s s e r t ( 0 ) ;case 0 : // the understudy

myStatus = UNDY;s e t p g i d (0 , SP gpid ) ;mySpecOrder = −1;

SP Red i r ec tOutput ( ) ;

// tell spec that undy i s readyp i d t c u r r e n t p i d = ge t p i d ( ) ;w r i t e ( undyCreatedP ipe [ 1 ] , &c u r r e n t p i d , s i z eo f ( i n t ) ) ;break ;

defau l t : // main continuesPostPPR commit ( ) ;break ;

}}

78

Algorithm 4.3.7 Listing of ppr commit finalization routine

void PostPPR commit ( void ) {i n t token , s i z e = s i z eo f ( token ) ;

// send ”main i s done” to specw r i t e ( l oH iP i p e s [ mySpecOrder ] [ 1 ] , &mySpecOrder , s i z e ) ;

i f ( myStatus == SPEC)SP sync r ead ( l oH iP i p e s [ mySpecOrder −1 ] [ 0 ] , &token , s i z e ) ;

SP PushDataAccordingToMap (WRITEMAP(mySpecOrder ) ,updateP ipe [ 1 ] ) ;

// send copy donew r i t e ( l oH iP i p e s [ mySpecOrder ] [ 1 ] , &mySpecOrder , s i z e ) ;e x i t ( 0 ) ;

}

process aborts it means that either the understudy has finished the parallel region

first, or that there is an error indicated in the access maps. In either of these cases

the understudy process becomes the control process and continues running. If the

understudy process is aborting then it must be the case that the spec process has

succeeded. Because the understudy is useless at that point it simply exists.

4.4 Types Of Speculative Parallelism

The bop system can be used to express parallelism in several ways. At the pro-

gram level, parallelism can be broken into three categories: instruction level, data,

and task. The coarse-grained nature of process-based speculative parallelism does

cannot take advantage of instruction level improvements, but it does address both

data and task parallelism.

79

START

CTRL

MAIN SPEC i

B E

B

MAIN SPEC i SPEC i+1

B

UNDY SPEC i

E

B

UNDY SPEC i SPEC i+1

E

MAIN SPEC i+1

E

E

B

END1

E

UNDY SPEC i+1

E

END4

E

END2

E

END3

E

Figure 4.4: State diagram of bop. Edge labels represent begin and end pprmarkers (B and E respectively).

80

4.4.1 Data-Parallel

Data parallelism is possible when the same operation can be performed on many

data elements. This form of parallelism is often expressed in a loop, and the con-

version from a sequential program will often focus there. It is not necessary that

all instance of the parallel region perform exactly the same sequence of instruc-

tions, and so control flow can change within the region. This is not the case in the

simplest SIMD (single instruction multiple data) style parallelism. Other system

may offer an explicitly parallel loop, for example the DOALL construct available in

Fortran, or the parallel for directive in OpenMP, in which a loop is marked

are parallel. The same effect is achieved with bop by making the loop body con-

ditional on a BeginPPR marker and placing the EndPPR marker at the end of the

loop body.

4.4.2 Task-Parallel

Task parallelism exists when separate portions of the execution can be performed

independently. This can be implemented with the bop system by placing one

portion of otherwise straight-line code in a conditional block based on the return of

BeginPPR and finalized with a EndPPR marker. At some later point, and additional

EndPPR marker indicates that the speculative process needs the results of the

parallel task. At run time, the main process will execute the code within the ppr

block and spawn its understudy at its conclusion. The speculative process will skip

the conditional block, eventually synchronizing when it reaches the end marker. If

the understudy reaches the marker first, it will terminate the speculative process.

This arrangement is semantically similar to fork-join execution where the sec-

ond end marker represents the join point. One can view the conditional block of

code in terms of a future that is explicitly consumed at the end marker. If the

code block were to be placed in a separate function, the syntax would even be

81

quite similar. This setup can be generalized to multiple parallel tasks by treating

each task as described above. Because only a newly created speculative process

receives a unique return value from BeginPPR the understudy will double check

all of the tasks.

The series of ppr markers is necessary to guarantee that each task is not

dependent on the computation of earlier tasks. If the programmer knows that

the work a task is performing is ancillary to final results, then any data modified

within the task can be ignored by the bop run-time system.

4.5 Comparison to Other Approaches

4.5.1 Explicit Parallelism

In order to explicitly parallelism a program it must be proved the program will

execute correctly in parallel in all cases. Perhaps the most significant advantage

of using bop over an explicit technique is the guarantee of correct execution even

if the region markers are incorrect. Using a ppr to guard a region of an execution

is significantly easier than determine what data are modified within the region

and appropriately protecting it.

In comparison to using locks when explicitly parallelizing a program, one does

not need to ensure the association between the protection (the lock) and the data

are correct. If this association is not correct then the lock fails to serve its purpose.

If one were to implement something like a ppr with locks, it would be necessary to

protect the body of the ppr with a lock and acquire the lock immediately before

attempting to access (either read or write) any of the data accessed within the

ppr.

Attempting to debug a parallel program, particularly in the face of race con-

ditions, relies on the non-deterministic interleaving of the executions. A program

82

running with the bop runtime will behave the same as if it were to be executed

sequentially, which largely obviates the need for debugging it. If errors in the

sequential program need to be diagnosed, the bop markers can be easily disabled

(become a non-operation) and the program run sequentially.

Even if locks are used correctly to synchronize parallel execution, these uses

cannot be composed into more general cases. The use of locks for parallel pro-

gramming has a significant advantage over the bop system in their efficiency.

Locks introduce the least overhead of any synchronization technique, and can use

used in fine-grained cases for which a ppr would not be appropriate.

Attempting to implement something analogous to pprs using a message pass-

ing representation would face many of the same problems as locking. Because

message passing generally requires an explicit receive statement, it must be placed

before the first potential access of any type to any of the data potentially modified

within the ppr. Additionally, the message would need to carry all data modified

in the ppr. Because the members of this set cannot generally be known until run

time, a conservative implementation would need to gather all data modified in the

ppr.

4.5.2 Fine-Grained Techniques

bop is not as efficient as thread-level techniques because of the overhead of general

protection and the problem of false sharing. Speculation also causes unnecessary

computations and by nature cannot handle general forms of I/O and other opera-

tions with unrecoverable side effects (inside a ppr). However, the main advantage

is ease of programming. bop can parallelize a program based on only partial in-

formation. It requires little or no manual changes to the sequential program and

no parallel programming or debugging. The overhead of the system can be hidden

when there is enough parallelism. bop uses unmodified, fully optimized sequential

83

code while explicit threading and its compiler support are often restrained due to

concerns over the weak memory consistency on modern processors. With these

features, bop addresses the scalability of a different sort—to let large, existing

software benefit from parallel execution.

Any technique that does not use heavy-weight processes can be considered fine-

grained. Such techniques are inherently unable to utilize operating system copy-

on-write memory protection. Without hardware support, speculative parallelism

techniques must employ some other mechanism for the roll-back of speculative

writes.

In addition to lacking the operating system mechanism for protecting mem-

ory stores, fine-grained techniques face distinct challenges with regard to logging

memory loads. While the page level read/write access can be manipulated as

in the Fast Track system, this approach is non-viable. The time spent handling

the operating system level signal is far too high in proportion to the duration of

the parallel work. Additionally, the run-time system must do more work than

a system such as Fast Track to determine which thread performed the memory

access.

The more common approach is for the run-time system to instrument mem-

ory loads and stores to allow for logging (and subsequent roll-back or replay).

Excluding systems replying on hardware support, such instrumentation amounts

to expensive additional operations surrounding all memory accesses. These ad-

ditional operations introduce overheads measured as multiples of the execution

time.

84

4.6 Evaluation

4.6.1 Implementation and Experimental Setup

Compiler support is implemented with a modified version of the GNU Compiler

Collection (gcc) 4.0.1 at the intermediate language level. After high-level pro-

gram optimization passes but before machine code generation, the compiler con-

verts global variables to use dynamic allocation for proper protection. We did not

implement the compiler analysis for local variables. Instead the system privatizes

all stack data. All global and heap data are protected. Each global variable is

allocated on separate page(s) to reduce false sharing.

Also based on gcc 4.0.1 are an instrumentor and a behavior analyzer. The

instrumentor collects complete program traces with unique identifiers for instruc-

tions, data accesses, and memory and register variables, so the behavior analyzer

can track all data dependencies and identify ppr.

The bop runtime is implemented as a statically linked library. Shared memory

is used for storing snapshots, access maps, and for copying data at a commit.

Most communication is done by signals, and no locks are used. Two similar

systems have been implemented in the past within our research group using binary

instrumentors. These system do not require program source but offer no easy way

of relocating global data, tracking register dependencies, or finding the cause of

conflicts at the source level.

In bop, the lead process may die long before the program ends, since each

successful speculation produces a new lead (see Figure 4.1 for an example). Now

each parallelized program starts with a timing process that forks the first lead

process and waits until the last process is over (when a lead process hits a program

exit). Instead of collecting user and system times for all processes, the wall-

clock time of the timing process is used, which includes os overheads in process

85

Table 4.4: XLisp Private Variablesbuf : for copying string constants

gsprefix : for generated name stringsxlfsize : for counting the string length in a print call

xlsample : the vestige of a deleted feature called oscheckxltrace : intermediate results for debugging

Table 4.5: XLisp Checked Variablesxlstack : current stack pointer, restored after an evaluation

xlenv : current environment, restored after an evaluationxlcontext : the setjump buffer for exception handling

xlvalue : would-be exception valuexlplevel : parenthesis nesting level, for command prompt

scheduling. Experiments use multiple runs on an unloaded system with four dual-

core Intel 3.40 GHz Xeon processors, with 16MB of shared L3 cache. Compilation

is done with gcc 4.0.1 with “-O3” flag for all programs.

4.6.2 Application Benchmarks

XLisp Interpreter v1.6 by D. M. Betz

The XLisp code, which is available as part of the SPEC 1995 benchmark suite,

has 25 files and 7616 lines of C code. The main function has two control loops,

one for reading expressions from the keyboard and the other for batch processing

from a file. The body of the batch loop is marked by hand as a ppr. Through the

programming interface described in Section 4.2.3, 5 likely privatizable variables are

identifiable (listed in Table 4.6.2), along with 5 checked variables (Table 4.6.2) and

one reduction variable, gccalls, which counts the number of garbage collections.

We do not know much about the rest of the 87 global variables (including function

pointers) except that they are all monitored by bop.

The speculatively parallelized version of XLisp runs successfully until the

garbage collection routine is activated. Because of the extensive changes the

86

collector makes to the memory state, it always kills the speculation. To solve this

problem, the mark-sweep collector implementation is revised for bop as described

briefly here. The key idea is to insulate the effect of garbage collection so it can

be done concurrently, without causing unnecessary conflicts. Each ppr uses a

separate page-aligned memory region. At the beginning of a ppr instance (after

forking but before data protection) the garbage collector performs a marking pass

over the entire heap to record all reachable objects in a start list. New objects are

allocated inside the pre-allocated region during the execution of the ppr. When

the garbage collection is invoked, it marks only objects inside the region but tra-

verses the start list as an additional set of root pointers. Likewise, only objects

within the region that are unmarked are freed. At the end of the ppr, the garbage

collector is run again, so only the pages with live objects are copied at the commit.

The code changes to implement this region-based garbage collection comprise the

introduction of three new global variables and 12 additional statements, most of

which are for collecting and traversing the start list and resetting the MARK flags

in its nodes.

The region-based mark-sweep has non-trivial costs at the beginning and end

of pprs. Within the ppr the collector may not be as efficient because it may fail

to reclaim all garbage because some nodes in the start list would have become

unreachable in the sequential run. The extent of these costs depends on the

input. In addition, the memory regions will accumulate long-live data, which

leads to more unnecessary alerts from false sharing. The lisp evaluation may

trigger an exception leading to an early exit from within a ppr, so the content

of checked variables may not be restored even for parallel expressions. Therefore,

one cannot decide a priori whether the chance of parallelism and its likely benefit

would outweigh the overhead. However, these are the exact problems that bop is

designed to address with its streamlined critical path and the on-line sequential-

parallel race.

87

SerialSpeculative

1 3 7

Times (s)2.25 1.50 0.95 0.68

2.27 1.48 0.94 0.68

2.26 1.47 0.94 0.68

Speedup 1.00 1.53 2.39 3.31

Table 4.6: Execution times for various speculation depths

The N–Queens input from spec95 benchmark suite, which computes all po-

sitions of n queens on an n × n chess board in which no attacks are possible,

is used as a test case of the bop-lisp interpreter. Four lines of the original five

expression lisp program are modified, resulting in 13 expressions, of which 9 are

parallelized in a ppr. When n is 9, the sequential run takes 2.36 seconds using the

base collector and 2.25 seconds using the region-based collector (which effectively

has a larger heap but still needs over 4028 garbage collections for nine 10K-node

regions). The results of testing three speculation depths are listed in Table 4.6.2.

The last row of Table 4.6.2 shows that the speedup, based on the minimum

time of from three runs, is a factor of 1.53 with 2 processors, 2.39 with 4 processors,

and 3.31 with 8 processors. The table does not list the additional cost of failed

speculations, which accounts for 0.02 seconds of the execution.

GZip v1.2.4 by J. Gailly

GZip takes one or more files as input and compresses them one by one using the

Lempel-Ziv coding algorithm (LZ77). This case is based on version 1.2.4, which

available from the spec 2000 benchmark suite. Much of the 8616-line C code

performs bit-level operations, some through in-line assembly. The kernel is based

on a well worn implementation originally written for 16-bit machines. During

testing the program is not instructed to act as a “spec” and behaves as a normal

compressor rather than a benchmark program (which artificially lengthens the

88

Table 4.7: The size of various protection groups in training runsData Groups GZip Parser

Shared Dataobject count 33 35size (bytes) 210K 70Kaccesses 116M 343M

Checked Dataobject count 78 117size (bytes) 2003 5312accesses 46M 336M

Private Data (likely)object count 33 16size (bytes) 119K 6024accesses 51M 39M

input by replication).

Table 4.7 shows the results of the bop analyzer, which identifies 33 variables

and allocation sites as shared data, 78 checked variables (many of which not used

during compression), and 33 likely private variables. Behavior analysis detected

flow dependencies between compressions because the original GZip failed to com-

pletely reinitialize parts of its internal data structure before starting compression

on another new file. The values would have been zeroed if the file was the first

to be compressed, and in this test the code has been changed to reinitialize these

variables. Compression returns identical results in all test inputs.

The sequential GZip code compresses buffered blocks of data one at a time, and

stores the results until an output buffer is full. pprs are manually placed around

the buffer loop and the set of likely private variables are specified through the

program interface described in Section 4.2.3. In this configuration the program

returned correct results, but speculation continually failed because of conflicts

caused by two variables, unsigned short bi buf and int bi valid, as detected by the

run-time monitoring.

The two variables are used in only three short functions. After inspecting the

original source code it became clear that the compression produces bits rather

than bytes, and the two variables stored the partial byte of the last buffer. This

89

SequentialSpeculative

1 3 7

Times (s)8.46 8.56 7.29 7.71 5.38 5.49 4.80 4.478.50 8.51 7.32 7.47 4.16 5.71 4.49 3.108.53 8.48 5.70 7.02 5.33 5.56 2.88 4.88

Average Time 8.51 7.09 5.27 4.10Average Speedup 1.00 1.20 1.61 2.08

Table 4.8: Execution times of bop GZip

dependency was hidden below layers of code and among 104 global variables, but

the run-time analyzer enabled quick discovery of the hidden dependency. The

byte cannot simply be filled (as is done for the final byte) if the resulting file is

to be decompressed with the stock Gunzip. A single extra or error bit will render

the output file meaningless to the decompressor. The solution is to compress

individual data buffers in parallel and concatenate the compressed bits afterward.

The intra-file compression permits single-file compression to use multiple pro-

cessors. The bop version of GZip is testing using a single 84MB file (the gcc 4.0.1

tar file). Table 4.8 shows the comparison between the running time of the unmod-

ified sequential code and the bop version running at three speculation depths.

Although the execution time is stable in sequential runs, it varies by as much as

67% in parallel runs, so in the following table we include the result of six con-

secutive tests of each version is used, and the computed speedup is based on the

average time.

With 2, 4, and 8 processors, the parallel compression gains speedups of 1.20,

1.61, and 2.08. The 8-way GZip is twice as fast and it is slightly faster than data

decompression by Gunzip, whose time is between 4.40 and 4.73 seconds in 6 runs.

The critical path of bopGZip, when all speculation fails, runs slightly faster than

the sequential version because of the effect of prefetching by the speculation. Intra-

file speculation uses additional memory mostly for spec to buffer the compressed

data for the input used. In addition, the program has 104 global variables, so the

90

0

5

10

15

20

25

100502510

wall-clock time (sec.)

num. sentences in the possibly parallel region

Sleator-Temperley English parser v2.1

sequentialco-processing (0% parallel)coprocessing (97% parallel)

Figure 4.5: The effect of speculative processing on Parser

space overhead for page allocation is at most 104 pages or a half mega-byte for

the sequential execution. The space cost of their run-time replication is already

counted in the numbers above (130KB and 7.45MB).

Sleator-Temperley Link Parser v2.1

The parser has a dictionary of about 60000 word forms. It has coverage

of a wide variety of syntactic constructions, including many rare and

idiomatic ones. [. . . ] It is able to handle unknown vocabulary, and

make intelligent guesses from context about the syntactic categories

of unknown words.

(Spec2K web site)

91

SequentialSpeculative

1 3 7

Times (s)11.35 10.06 7.03 5.3411.37 10.06 7.01 5.3511.34 10.07 7.04 5.34

Speedup 1.00 1.13 1.62 2.12

It is not immediately clear from the documentation — or from the 11,391 lines

of its C code — whether the Seatlor–Temperley Link Parser handles sentences in

parallel, but in fact they are not. If a ppr instance parses a command sentence

which changes the parsing environment, e.g., turning on or off the echo mode, the

next ppr instance cannot be speculatively executed. This is a typical example of

dynamic parallelism.

The bop parallelism analyzer identifies the sentence-parsing loop. We man-

ually strip-mine the loop to create a larger ppr. The data are then classified

automatically as shown in Table 4.7. During the training run, 16 variables are

always written first by the speculation process during training, 117 variables al-

ways have the same value at the two ends of a ppr instance, and 35 variables are

shared.

The test input for the parallel version of the parser uses 1022 sentences ob-

tained by replicating the spec95 training input twice. When each ppr includes

the parsing of 10 sentences, the sequential run takes 11.34 second, and the parallel

runs show speedup of 1.13, 1.62 and 2.12 with a few failed speculations due to the

dynamic parallelism.

The right-hand side of Figure 4.5 shows the performance on an input with 600

sentences. Strip-mine sizes ranging from 10 sentences to 100 sentences are tested

in each group, and the group size has mixed effects on program performance.

For sequential and spec fail, the largest group size leads to the lowest overhead,

3.1% and 3.6% respectively. Speculative processing improves performance by 16%,

46%, 61%, and 33% for the four group sizes. The best performance occurs with the

92

medium group size. When the group size is small, the relative overhead is high;

when the group size is large, there are fewer ppr instances and they are more

likely to unevenly sized. Finally, the space overhead of speculation is 123KB,

100KB of which is checked data. This space overhead does not seem to change

with the group size.

Comparison with Threaded Intel Math Kernel Library

The Intel Math Kernel Library 9.0 (mkl) provides highly optimized, processor-

specific, and multi-threaded routines specifically for Intel processors. The li-

brary includes Linear Algebra Package (LAPACK) routines used for, among other

things, solving systems of linear equations. In this experiment the performance of

solving eight independent systems of equations using the dgesv routine is used for

comparison. mkl exploits thread-level parallelism within, but not across, library

calls. The number of threads used is defined by setting the OMP NUM THREADS

environment variable. bop, on the other hand, can speculatively solve the sys-

tems of equations in parallel even when it uses an unparallelized library, and so the

value OMP NUM THREADS is set to one for bop executions. Since the program

data are protected, bop guarantees program correctness if speculation succeeds.

Math Kernel Library experiments were conducted on systems of equations

with a number of equations ranging from 500 to 4500 in increments of 500. For

each system of equations mkl-only implementation is tested with the number of

threads set to 1, 2, 4, and 8. For the bop and mkl implementation the level

of speculation tested was correspondingly set to 0, 1, 3, and 7. Results for the

single-threaded mkl run and zero-speculation bop run are not shown.As shown

in Figure 4.6, bop-mkl depth 1 and omp-mkl thread 2 perform similarly, with the

mkl-only implementation achieving at most an 18% increase in operations per

second for 1000 equations. For bop-mkl depth 3 and bop-mkl depth 7, the run-time

overhead of bop prevents the system from achieving speedups for systems with

93

0

5

10

15

20

25

30

45004000350030002500200015001000500

billion operations per second

num. linear equations

bop-mkl depth 7omp-mkl thread 8bop-mkl depth 3

omp-mkl thread 4bop-mkl depth 1

omp-mkl thread 2

Figure 4.6: Solving 8 systems of linear equations with Intel MKL

1500 equations or fewer. However, above this point the course-grained parallelism

provided by bop is able to outperform the fine-grained, thread-level parallelism

of the mkl library. Increases between 15% and 20% are seen for bop-mkl depth 7

compared to omp-mkl thread 8 and increases between 7% and 11% are seen for

bop-mkl depth 3 compared to omp-mkl thread 4.

The comparison with threaded mkl helps to develop an understanding of the

overhead of the processed-based bop system, in particular its relationship with

the size of parallel tasks and the speculation depth. The results demonstrate the

property explained in Section 4.1.1: the overhead becomes smaller if the granular-

ity is large or if the speculation depth is high. For 1500 equations, 3 speculation

processes perform 10% slower than 4-thread MKL because of the overhead. How-

ever, for the same input size, the greater parallelism from 7 speculation processes

94

more than compensates for the overhead and produces an improvement of 16%

over 8-thread mkl. Similar experiments pitting bop against another scientific li-

brary, the threaded automatically tuned linear algebra software (atlas), shows

similar results.

95

5 Speculative Optimization

Introduction

In this chapter I present a variation on process-based speculative execution called

Fast Track. The Fast Track system is based on the infrastructure for speculative

execution described in Chapter 3 but is applicable for a wholly different set of

uses from those in Chapter 4. Fast Track allows the use of unsafely optimized

code, while leaving the tasks of error checking and recovery to the underlying

implementation. The unsafe code can be implemented by a programmer or by a

compiler or other automated tool, and the program regions to be optimized can be

indicated manually or determined during execution by the run-time system. As

before, the system uses coarse-grain tasks to amortize the speculation overhead

and does not require special hardware support.

The shift in processor technology toward multicore, multi-processors opens

new opportunities for speculative optimization, where the unsafely optimized code

marches ahead speculatively while the original code follows behind to check for

errors and recover from mistakes. In the past, speculative program optimization

has been extensively studied both in software and hardware as an automatic

technique. The level of improvement, although substantial, is limited by the

96

ability of both the static and run-time analyzes. In fact, previous techniques

primarily targeted individual loops and only considered transformations based on

value and dependency information.

One may question the benefit of this setup: suppose the fast code gives correct

results, would we not still need to wait for the normal execution to finish to know

it is correct? The reason for the speed improvement is the overlapping of the

normal tracks. Without fast track, the next normal track cannot start until the

previous one fully finishes. With fast track, the next one starts once the fast code

for the previous normal track finishes. In other words, although the checking is as

slow as the original code, it is now done in parallel. If the fast code has an error

or occasionally runs slower than the normal code, the program would execute the

normal code sequentially and will not be delayed by a strayed fast track.

In Section 5.2 I describe the programming interface for Fast Track. This inter-

face can be used an automated too, or in a natural way by a human programmer

with little effort. In Section 5.3 I describe the ways in which the Fast Track

run-time system extends the basic runtime described in Section 3.1.

5.1 Design

5.1.1 Fast and Normal Tracks

The FastTrack system represents two alternative methods of execution for some

portion of a program. At run time both of the methods are executed in parallel.

One of the two is identified a priori to be the canonical method, while the other is

assumed to potentially be unsafe in some cases. The unsafe execution is expected

to complete more quickly and is referred to as the “fast track” while the correct

computation is called the “normal track”.

97

5.1.2 Dual-track

In addition to the fast and normal track notation, the FastTrack run-time system

allows for a pair of parallel executions that are considered to be indistinguishable.

In this usage, both of the executions are referred to as “Dual Tracks”. Here,

whichever of the dual tracks can complete first leads to the continuing sequential

execution. The track which finishes more slowly will then confirm the results of the

first. If the two tracks are known with certainty to compute the same information

(but at unpredictable rates) the verification can be disabled.

5.2 Programming Interface

The FastTrack programming interface allows a programmer to optimize code at

the semantic level to select competing algorithms at run time, or to insert on-line

analysis modules such as locality profiling or memory-leak detection. Figures 5.2.1

and 5.2 show example uses of FastTrack to enable unsafely optimized loop and

function execution. If the fast tracks are correct, they will constitute the critical

path of the execution. The original loop iterations and function executions, which

we refer as normal tracks, will be carried out in parallel, “on the side.” The use

of FastTrack allows multiprocessors to improve the speed of sequential tasks.

A fast-track region contains a beginning branch if (FastTrack()), the con-

tents of two tracks, and an ending statement EndDualTrack(). An execution of

the code region is called a dual-track instance, in which the two tracks are the fast

track and the normal track. A program execution consists of a sequence of dual-

track instances along with any computations that occur before, between, or after

these instances. Any region of code whose beginning dominates the end in control

flow can be made a dual-track region. Nesting of regions is allowed by maintaining

the type of the track. When a inner dual-track region is encountered, the outer

fast track will take the inner fast track, while the outer normal track will take the

98

Algorithm 5.2.1 Unsafe loop optimization using fast track. Iterations offast fortuitous will execute sequentially. Iterations of safe sequential willexecute in parallel with one another, checking the correctness of the fast iterations.

while ( . . . ){

. . .i f ( FastTrack ( ) ) {

f a s t f o r t u i t o u s ( ) ; // unsafely optimized} e l se {

s a f e s e q u e n t i a l ( ) ; // safe code}EndDualTrack ( ) ;. . .

}

Algorithm 5.2.2 Unsafe function optimization using fast track. Routinesfast step 2 and step 2 can start as soon as fast step 1 completes. They arelikely to run in parallel with step 1.

. . .i f ( FastTrack ( ) ){

f a s t s t e p 1 ( ) ; // optimized} e l se {

s t e p 1 ( ) ; // safe code}. . .i f ( FastTrack ( ) ){

f a s t s t e p 2 ( ) ; // optimized} e l se {

s t e p 2 ( ) ; // safe code}

99

inner normal track. Statements with side effects that would be visible across the

processor boundary, such as system calls and file input and output, are prohibited

inside a dual-track region. The amount of memory that a fast instance may allo-

cate is bounded so that an incorrect fast instance will not stall the system through

excessive consumption. Figure 5.2.1 in the previous section shows an example of

a fast track that has been added to the body of a loop. The dual-track region can

include just a portion of the loop body, multiple dual-track regions can be placed

back-to-back in the same iteration, or a region can be used in straight-line code.

Figure 5.2 shows the use of fast track on two procedure calls, with . . . standing in

for any other statements in between. Multiple dual-track regions do not have to

be arranged in a straight sequence. One might be used only within a conditional

branch, while another could be in loop.

5.3 Run-time Support

5.3.1 Creation

In addition to the general creation process described in Section 3.1.1 the FastTrack

run-time variant must enable state comparison between the fast and regular tracks.

Within the FT BeginFastTrack run-time hook, prior to spawning a normal track,

the system allocates a shared memory space for two access maps, and a shared

data pipe. The use of these objects is described in Section 5.3.2.

5.3.2 Monitoring

During execution, memory pages are protected so that any write access will trigger

a segmentation fault. Both the fast and normal tracks use a signal handler to catch

the faults and record the access in a bit map.

100

Algorithm 5.3.1 Listing of FastTrack creation.

// Returns 1 when entering the fast track, 0 otherwise .i n t FT BeginFastTrack ( void ) {

i n t s e n p i p e [ 2 ] ;

// If we are currently in a fast track, finish it .i f ( FT ac t i v e ) FT PostDualTrack ( ) ;

// the number of processors used i s specDepth + 1i f ( FT maxSpec < 1) return 0 ;i f (SEQ == myStatus ) return 0 ;

// Setup memory access handler to watch pages modification .i f ( SP se tupAct i on ( FT SegvHandler , SIG MEMORY FAULT) )

return FT errorOnBeg in ( ) ;

// Setup communication channel for data updating .i f ( p i p e ( updateP ipe ) ) return FT errorOnBeg in ( ) ;

// Setup new access maps for the fast and normal tracks .i f ( FT setupMaps ( ) ) return FT errorOnBeg in ( ) ;

// Setup pipe for indication track seniority .i f ( p i p e ( s e n p i p e ) ) return FT errorOnBeg in ( ) ;

++FT order ; // Record serial number of the new normal track.

// Enqueue order to readyQueuei f ( FT order > FT maxSpec )

w r i t e ( readyQ−>p i p e [ 1 ] , &FT order , s i z eo f ( FT order ) ) ;

i n t PID = f o r k ( ) ;i f (−1 == s e t p g i d (0 , SP gpid ) )

p e r r o r ( ” f a i l e d to s e t p r o c e s s group ” ) ;

switch (PID ) {case −1:

return FT errorOnBeg in ( ) ;case 0 :

return FT in t e rna lBeg i nNo rma l ( s e n p i p e ) ;defau l t :

return FT i n t e r n a lB e g i nF a s t ( s e n p i p e ) ;}

}

101

In order to compare the memory modifications of the two track, the fast track

must provide the normal track with a copy of any changes it has made. At the

end of each dual track region, the fast track evaluates its access map to determine

what pages have been modified. Each page flagged in the access map is pushed

over a shared pipe, and consumed by the normal track, which then compares the

data to its own memory page.

Algorithm 5.3.2 Listing of FastTrack monitoring.

s t a t i c void FT SegvHandler ( i n t s i g , s i g i n f o t ∗ i n f o ,u c o n t e x t t ∗ con t e x t )

{a s s e r t (SIG MEMORY FAULT == s i g ) ;a s s e r t ( c on t e x t ) ;

// access to pages that are not mapped are true faultsi f ( i n f o−>s i c o d e == SEGV MAPERR)

i f (−1 == k i l l ( SP gpid , SIGALRM))p e r r o r ( ” f a i l e d to k i l l the t ime r ” ) ;

i f ( !WRITEOPT( con t e x t ) ) return ;

// record the page and remove the restrictionvoid∗ f au l tAdd = in f o−>s i a d d r ;SP recordAccessToMap ( fau l tAdd , FT accMap ) ;i f ( mprotect (PAGESTART( f au l tAdd ) , 1 ,PROT WRITE |PROT READ)){

p e r r o r ( ” f a i l e d to change memory a c c e s s p e rm i s s i o n .\ n” ) ;abo r t ( ) ;

}}

5.3.3 Verification

To guarantee that the speculative execution is correct, the memory state of the

fast and normal tracks are compared at the end of the dual track region. If the

fast track reached the same state as the normal track, then the initial state of the

102

next normal track must be correct. Typically, the next normal track was started

well before its predecessor finished, and it will know only in hindsight that it was

correctly initialized.

The normal track is responsible for comparing the writes made by both itself

and the fast track. The memory state comparison is performed once the normal

track has finished the dual track region because this is the first point at which

verification is possible. The comparison first determines if the set of writes made

by the two tracks is identical, which is handled by a simple memcmp on the access

map of each of the two tracks. The process then compares the writes themselves

using the FT CheckData run-time call as in Listing 5.3.3. Verification will fail if

either the set or contents differ, or if the fast track has not yet completed the dual

track region.

Once verification has been completed successfully, the two process are know

to have made identical changes to the same memory locations. From that point

forward, the execution of the two process would be identical. Given this, one of

the tracks is superfluous. Because the fast track is aborted if it does not reach

the end of the dual track region first, we assume that it has continued past that

point and completed other useful work. The normal track is thus useless (since

it would be recomputing exactly what the fast track has already computed) and

aborts.

It is worth noting that although multiple dual track regions (i.e., multiple pairs

of fast and normal tracks) may exist simultaneously, a single process will have at

most one fast access map and one normal access map. Because the normal track

is responsible for performing the verification routine, the fast track can abandon

the access map it had been using for a region once the region is complete. The

normal track will still have access to that map. Once the map has been analyzed,

the normal track will abort or transition to the fast state.

103

Algorithm 5.3.3 Listing of FastTrack verification routine FT CheckData

i n t FT CheckData ( void ) {unsigned long page = 0 ;char b u f f e r [ PAGESIZE ] ;i n t count , c ;

while ( page < PAGECOUNT) {// Returns true if the bitmap i s set for this page.i f ( SP checkMap ( ( void ∗ ) ( page ∗ PAGESIZE ) , FT accMap )){

count = 0 ;// Read a full page into a local buffer .while ( count < PAGESIZE) {

c = read ( updateP ipe [ 0 ] , b u f f e r ,PAGESIZE ) ;i f (−1 == c ) {

p e r r o r ( ” f a i l e d to read from p i p e ” ) ;} e l se {

i f (0 == c ) return UINT MAX ;e l se count += c ;

}}// compare our memory page to the buffered pagei f (0 != memcmp ( bu f f e r , ( void ∗ ) ( page∗PAGESIZE ) ,

PAGESIZE ) ){

return page + 1 ; // non−0 indicates failure}

}page++;

}

return 0 ; // 0 indicates success}

104

5.3.4 Abort

The FastTrack abort routine is handled almost entirely by the normal track. The

normal track first waits to receive a notification that all of the preceding normal

tracks have completed, at which point it commits any buffered output and per-

forms the verification routine. If the fast track needs to be aborted for any of the

reasons indicated in Section 5.3.3 the process executing the fast track is termi-

nated. Because the normal track performs the verification, all cases in which the

fast track is terminated pass through the same code path. The normal path pro-

cess explicitly signals the process running the fast track, which handles the signal

by simply closing the communications pipes and exiting. The steps taken by the

normal track after completing the dual track region are provided in Listing 5.3.5.

The normal track will continue executing until the next dual-track region is

encountered, or a program exit point is reached. Depending on the difference

in execution speed between the fast and normal track, the fast track may have

reached other dual track regions. In this case the abort of the fast track is followed

by the normal track sending a flag through the floodgates as an indication to

any waiting normal tracks that they should abort. Any normal tracks that have

already been released from the floodgate will run through their dual track region.

At the end of the region the process will synchronize by waiting to receive a flag

through the inheritance pipe indicating that it is the oldest running normal track.

In the case of an error in an earlier normal track, that synchronization flag will

indicate that the current process should also abort.

5.3.5 Commit

If the normal track verifies the correct execution of the dual track region, it clean

up and aborts. The fast track is free to continue execution, possibly entering more

FastTrack regions and creating further normal tracks.

105

Algorithm 5.3.4 Listing of slow track commit routine.

s t a t i c void FT slowTakesOver ( void ) {i n t token = −1;i n t pp id = ge tpp i d ( ) ; // (parent i s the fast track)k i l l ( ppid , SIGABRT ) ; // kill the fast track// Tell running slow tracks to abort on completion .SP s yn c w r i t e ( bequest , &token , s i z eo f ( token ) ) ;// Flush processes waiting at floodgates .FT c l e a r F l o odg a t e s ( ) ;FT s lowCleanup ( ) ;FT i n i t ( ) ; // setup meta datamyStatus = FAST ; // become FAST track

}

void FT PostSlow ( void ) {FT becomeOldest ( ) ; // wait for inheritance tokenSP CommitOutput ( ) ;

i f (memcmp( FT fastAccMap , FT slowAccMap , ACC MAP SIZE)| | FT CheckData ( ) ) // true if data changes differ

{FT slowTakesOver ( ) ;return ;

}// pass the inheritance tokenw r i t e ( bequest , &FT order , s i z eo f ( FT order ) ) ;// let a process leave the floodgateFT re l ea s eNex tS l ow ( ) ;FT s lowCleanup ( ) ;e x i t ( 1 ) ;

}

106

F

pass

F

fail

C

F N1

B

F E

B

E

F N1 N2

B | E

F F F

F

F N1

E

F N1 n2

E

F N1

E

Figure 5.1: State diagram of FastTrack processes.

107

5.3.6 Special Considerations

There are a number of corner cases of which the Fast Track system must take

account.

Seniority Control

Because the fast track may spawn multiple normal tracks, which may then run

concurrently, each normal track must know when all of its logical predecessors

have completed. Before a normal track terminates, it waits on a flag to be set by

its predecessor, and then signals its successor when complete. If there is an error

in speculation, the normal track uses the same mechanism to lazily terminate

normal tracks that are already running once they reach the end of their FastTrack

region.

Output buffering

To ensure that the output of a program running with FastTrack support is correct,

we ensure output is produced only by a normal track that is known to be correct

and is serialized in the correct order. Until a normal track has confirmed that

its initialization was correct (i.e., that all previous speculation was correct), it

buffers all terminal output and file writes. Once all previous normal tracks have

been committed the normal track is considered to be the “oldest,” and we can

be certain that its execution is correct. Given correct execution, any output the

process produces will be the same as what the sequential program would have

generated. The fast track never produces any output to the terminal nor does it

write to any regular file.

108

Implicit End Markers

The FastTrack end point can be indicated explicitly using the FT PostDualTrack

programming interface, but it is also handled implicitly in several cases. This

flexibility makes the job of the programmer easier by reducing the amount of

additional code they must write. Implicitly determining the end of the dual track

region also helps ensure correctness by catching cases where the user neglected to

correctly mark the end of the region. It should be noted that explicitly marking

the end of the region reduces the system overhead by pruning one of the system

process earlier.

There are two ways in which the end of a dual track region is determined

implicitly. This first is when the beginning of a dual track region is encountered.

Any process that is currently executing a dual track region (in any state) records

a flag to indicate its active participation. The first step the run-time system

takes when entering the FT BeginFastTrack or FT BeginDualTrack routines is to

check this activity flag and, if it is set, invoke the FT PostDualTrack routine. This

activity flag is distinct from the identifying state of the process (i.e., “FAST”),

which remains in effect.

The other implicit end marker point is a bit more subtle: we must capture

all program exit points in both the fast and normal tracks. This guarantees that

incorrect speculation does not direct a normal track to perform computation that

leads to a spurious exit from the program. In addition to ensuring correctness

in all cases, capturing all of the program exit points allows for correct program

termination to occur within the scope of a dual track region.

In the normal track we are ensuring that the same computation was performed

as in the fast track. In the fast track we must capture the program exit point and

wait for all normal tracks to finish. This may mean that they complete their dual

track region, or that the normal track has also reached the program exit. Because

109

the normal tracks are serialized, the fast track only needs to wait for the last

normal track it spawned to complete. This is achieved using the same mechanism

the normal tracks use to order themselves: the fast track waits on the inheritance

token. Note that the fast track is not necessarily waiting for the normal track to

reach the same program exit point, but the state of two will agree.

Whether or not we are within the scope of a dual track region, the correctness of

the fast track is not known until the verifying normal tracks complete. Although

we could terminate the fast track and allow the normal track to simply do its

work, the normal track may be predicated on the results of other normal tracks.

Keeping the state of the fast track allows the earlier normal tracks to validate.

The alternative would be to abort all but the oldest normal track, potentially

wasting work.

Processor Utilization

The objective of speculative execution is for execution to occur as quickly as

possible. In order to make this happen, the run-time system should use the

available processing cores as wisely as possible. In a naive approach the fast

track would run until it exits the program, spawning normal tracks along the way.

Each normal track would compute its own version its dual track region and verify

correct computation.

Although execution of the normal tracks (with the exception of the oldest) is

speculative based on the correctness of the fast track, they are taking advantage

of otherwise unused resources. However, if we spawn too many normal tracks,

they may begin contend for hardware resources. Ultimately the normal tracks are

performing the real computation, and delaying their execution would be wasteful.

This is true either if we allow a “more speculative” process to be scheduled in

favor of an older one, or if it merely interferes with it.

110

Algorithm 5.3.5 Listing of FastTrack exit point handler.

void a t t r i b u t e ( ( d e s t r u c t o r ) ) FT ex i tHand l e r ( void ) {i n t token ;

i f ( FT ac t i v e ) FT PostDualTrack ( ) ;

switch ( myStatus ) {

case FAST :c l o s e ( readyQ−>p i p e [ 0 ] ) ;c l o s e ( readyQ−>p i p e [ 1 ] ) ;// wait for the last normal trackSP sync r ead ( i n h e r i t a n c e , &token , s i z eo f ( i n t ) ) ;c l o s e ( i n h e r i t a n c e ) ;k i l l ( SP gpid , SIGTERM) ;break ;

case SLOW:i f ( FT order > 1) // wait to be the oldest

SP sync r ead ( i n h e r i t a n c e , &token , s i z eo f ( token ) ) ;SP CommitOutput ( ) ; // commit outputk i l l ( SP gpid , SIGTERM) ; // terminate speculationbreak ;

defau l t :break ;

}

}

111

The FastTrack system mitigates the interference between normal tracks by

limiting the number of tracks active at any one time (below). There is no action

take to encourage the operating system to schedule the normal tracks with respect

to one another, but modifying the scheduling priority of the processes would be

a simple way to further improve the efficiency of the system. The impact of such

scheduling is open for further exploration.

Fast-track Throttling The fast track has thus far been described as specula-

tively running ahead of the normal tracks, constrained only by program termina-

tion or a terminal signal from one of the normal tracks. There are two reasons

why it is undesirable for the fast track to run arbitrarily far ahead. The first prob-

lem is the potential resource demand of the waiting normal tracks. The second

problem is that, should there be an error in the speculation detected in one of the

normal tracks, the processing done by the fast track is essentially wasted. The

FastTrack run-time system implements a throttling mechanism to keep the fast

track running far enough ahead to supply normal tracks and keep the processing

cores utilized, while minimizing potentially wasted resources.

The throttling strategy is to pause the fast track and give the processor to

a normal track, as shown by the middle diagram in Figure 5.2. When the next

normal track finishes, it re-activates fast track. The word “next” is critical for

two reasons. First, only one normal track should activate fast track when it

waits, effectively returning the processor after “borrowing” it. The time of the

activation must be exact. If it is performed by a track to early there will be too

many processes. One track later and there would be under-utilization.

Consider a system with p processors running fast track and p−1 normal tracks

until the fast track becomes too fast and suspends execution giving the processor

to a waiting normal track. Suppose that three normal tracks finish in the order n1,

n2, and n3, and fast track suspends after n1 and before n2. The proper protocol

112

is for n2 to activate fast track so that before and after n2 we have p and only p

processes running concurrently. Activation before and after n2 would lead to less

than or more than p processes.

In order to ensure that suspension and activation of the fast track is timed

correctly with respect to the completion of the normal tracks FastTrack maintains

some extra state. The value of waitlist length indicates the number of normal-track

processes waiting in the ready queue. A flag ft waiting represents whether the fast

track has been paused.

The fast track is considered to be too fast when waitlist length exceeds p. In

this case, the fast track activates the next waiting process in the ready queue, sets

the ft waiting flag, and then yields its processor by. When a normal track finishes,

it enters the critical section and determines which process to activate based on the

flag: if ft waiting is on, it clears ft waiting and reactivates the fast track; otherwise,

it activates the next normal track and updates the value of waitlist length.

A problem arises when there are no normal tracks waiting to start, which can

happen when the fast track is too slow. If a normal track waits inside the critical

section to start its successor, then the fast track cannot enter to add a new track

to the queue. The bottom graph in Figure 5.2 shows this case, where one or more

normal track processes are waiting for fast track to fill the queue.

Resource Allocation Assuming we are executing on a system with N proces-

sors, and that the fast track is executing on one of the processors, the run-time

system should allow at most N − 1 normal processes to execute simultaneously.

The exception is when the fast track has been throttled, allowing an N th normal

track process. In addition to limiting the number of normal tracks, the FastTrack

system should guarantee that the N − 1 oldest (or, least speculative) processes

are allotted hardware resources. The FastTrack run-time system implements these

constraints using a token passing system such that only a process holding a token

113

is released from the FT BeginFastTrack run-time call.

Once a normal track process has been spawned and initialized, it waits to

receive a token by attempting to read from a pipe we refer to as the floodgate.

Although conceptually each normal track needs its own floodgate, we know that

the maximum number of normal tracks is bounded and a pool of floodgates can be

used (implemented as an array with circular access). The system inserts the set of

tokens into the floodgates at initialization, and whenever resetting the floodgates

due to miss-speculation. The whole set of floodgates is available to all processes.

In order to open the floodgates in the correct order, a normal track must iden-

tify to whom it should pass the token it currently holds. The track makes this

determination by reading the identifier from the ready queue. The fast track is re-

sponsible for enqueuing the normal tracks when they are created. Because there is

a single producer into the ready queue (the fast track) and it enqueues the normal

tracks in their sequential order, the normal tracks are guaranteed to be released in

the correct sequential order. This is true regardless of the order in which normal

tracks complete. It is worth noting that the normal tracks commit in sequential

order in any case, however the floodgate is opened before the synchronization is

performed.

Allowing the normal tracks to finish out of order allows the run-time system to

better absorb differences in the computational cost of various dual track regions.

The steady state of the run-time system’s resource allocation control is shown by

the top diagram in Figure 5.2. The execution rate of fast track is the same as

the combined rate of N − 1 normal tracks. When their speeds do not match, the

ready queue may become empty or may continue growing until the fast track is

throttled.

With activity control and fast-track throttling, the FastTrack run-time system

utilizes the available processors as efficiently as possible. Processing resources are

completely utilized unless there is a lack of parallelism and the fast track runs

114

too slowly. When there is enough parallelism, the fast track is constrained to

minimize potentially useless speculative computation.

Memory Usage The FastTrack run-time system relies on the operating sys-

tem implementation of copy-on-write, which lets processes share memory pages

to which they do not write. In the worst case where every dual-track instance

modifies every data page, the system needs d times the memory needed by the

sequential run, where d is the fast-track depth. The memory overhead can be

controlled by abandoning a fast track instance if it modifies more pages than a

empirical constant threshold h. This bounds the memory increase to be no more

than d×h×M , where M is the virtual memory page size. The threshold h can be

adjusted based on the available memory in the system. Memory usage is difficult

to estimate since it depends on the demands of the operating system and other

running processes. Earlier work has shown that on-line monitoring can effectively

adapt memory usage by monitoring the page-fault indicators from Linux [21, 65].

Experimental test cases have never indicated that memory expansion will be a

problem, so I do not consider memory resource further.

Running two instances of the same program would double demand for off-chip

memory bandwidth, which is a limiting factor for modern processors, especially

chip multiprocessors. In the worst case if a program is completely memory band-

width bound, no fast track can reduce the overall memory demand or improve

program performance. However, experience with small and large applications on

recent multicore machines, which are detailed later, is nothing but encouraging.

In FastTrack, the processes originate from the same address space and share read-

only data. Their similar access patterns help to prefetch useful data and keep

it in cache. For the two large test applications used, multiple processes in Fast-

Track ran almost the same speed as that of a single process. In contrast, running

multiple separate instances of a program always degrades the per-process speed.

115

5.4 Compiler Support

The FastTrack system guarantees that it produces the same result as the sequen-

tial execution. By using Unix processes, FastTrack eliminates any interference

between parallel executions through the replication of the address space. During

execution, it records which data are changed by each of the normal and fast in-

stances. When both instances finish, it checks whether the changes they made are

identical. Program data can be divided into three parts: global, stack, and heap

data. The stack data protection is guaranteed by the compiler, which identifies

the set of local variables that may be modified through inter-procedural MOD

analysis [30] and then inserts checking code accordingly. Imprecision in compiler

analysis may lead to extra variables being checked, but the conservative analysis

does not affect correctness. The global and heap data are protected by the op-

erating system’s paging support. At the beginning of a dual-track instance, the

system turns off write permission to global and heap data for both tracks. It then

installs custom page-fault handlers that record which page has been modified in

an access map and re-enables write permission.

5.5 Uses of Fast Track

5.5.1 Unsafe Program Optimization

In general, the fast code can be any optimization inserted by either a compiler

or a programmer; for example memoization, unsafe compiler optimizations or

manual program tuning. The performance of the system is guaranteed against

slow or incorrect fast track implementations. The programmer can also specify two

alternative implementations and let the system dynamically select the faster one.

Below I discuss four types of optimizations that are good fits for fast track because

116

they may lead to great performance gains but their correctness and profitability

are difficult to ensure.

Memoization For any procedure the past inputs and outputs may be recorded.

Instead of re-executing the procedure in the future, the old result can be reused

when given the same input. Studies dated back to at least 1968 [39] show dramatic

performance benefits when using memoization, for example to speed up table look-

up in transcoding programs. Memoization must be conservative about side-effects

and can provide only limited coverage for generic use in C/C++ program [15].

With FastTrack, memoization does not have to be correct in all cases and therefore

can be more aggressively used to optimize the common case.

Semantic optimization Often, different implementation options may exist at

multiple levels, from the basic data structures used such as a hash table, to the

choice of algorithms and their parameters. A given implementation is often more

general than necessary for a program, allowing for specialization. Current pro-

gramming languages do not provide a general interface for a user to experiment

with an unsafely simplified algorithm or to dynamically select the best choice

among alternative solutions.

Manual program tuning A programmer can often identify performance prob-

lems in large software and make changes to improve the performance on test in-

puts. However, the most radical solutions are often the most difficult to verify in

terms of correctness, or to ensure good performance on other inputs. As a result,

many creative solutions go unused because an automatic compiler cannot possibly

achieve them.

Monitoring and safety checking It is often useful to instrument a program

to collect run-time statistics such as frequently executed instructions or accessed

117

data, or to report memory leaks or out-of-bound memory accesses. In such cases,

the original uninstrumented code can serve as the fast track, and the instrumented

code can run in parallel to reduce the monitoring overhead.

5.5.2 Parallel Memory-Safety Checking

To test fast track on real-world applications, it has been applied to the paralleliza-

tion of a memory-safety checking tool called Mudflap [16]. Mudflap is bundled

with the widely used GNU compiler collection (gcc), adding checks for array

range (over or under flow) and validity of pointer dereferences to any program

gcc compiles. Common library routines that perform string manipulation or di-

rect memory access are also guarded. Checks are inserted at compile time and

require that a run-time library be linked into the program.

The Mudflap compilation has two passes: memory recording, which tracks all

memory allocation by inserting mf register and mf unregister calls, and access

checking, which monitors all memory access by inserting mf check calls and

inlined operations. The recording cost is proportional to the frequency of data

allocation and recollection, and the checking cost is proportional to the frequency

of data access.

To fast track the Mudflap checking system we introduced a new compiler pass

that clones all functions in the program. The second Mudflap pass is instructed

to ignore the clones while instrumenting the program. The result is an executable

with the original code fully checked while the clone just records data allocation

and free. The instrumentation of the clones is necessary to maintain the same

allocation and meta data of memory as those of the original code. We create a

Fast Track version of programs by using the fully checked version of the program

to verify the memory safety of the unchecked fast track.

118

5.6 Evaluation

5.6.1 Analysis

Throughout he remainder of this section I use the following notation to represent

the basic parameters of the system:

• Dual track computations are identified ri.

• Interleaving computation regions are uj.

• The program execution E is a sequence of u0r1u1r2 . . . rnun.

• The running time of a region is denoted by T ().

• The number of available processors is p > 1.

• Each ri has a success rate α (0 ≤ α ≤ 1).

• A fast instance takes a fraction x (0 ≤ x ≤ 1) of the time the normal

instance takes

• The dual-track execution has a time overhead qc (qc ≥ 0) per instance and

is slowed down by a factor of qe (qe ≥ 0) because of the monitoring for

modified pages.

Analytical Model

The original execution time is T (E) = T (u0) +∑n

i=1 T (riui). By reordering

the terms leads to T (E) =∑n

i=1 T (ri) +∑n

i=0 T (ui). Name the two components

Er = r1r2 . . . rn and Eu = u0u1 . . . un. The time T (Eu) is not changed by fast-track

execution because any ui takes the same amount of time regardless of whether it

is executed with a normal or a fast instance.

119

Focusing on T (Er) and in particular the average time taken per ri, tr = T (Er)n

,

and how this time changes as a result of FastTrack. Since we would like to derive

a closed formula to examine the effect of basic parameters, consider a regular case

where the program is a loop with n equal length iterations. A part of the loop

body is a FastTrack region. Let T (ri) = tc be the (constant) original time for

each instance of the region. The analysis can be extended to the general case

where the length of each ri is arbitrary and tc is the average. The exact result

would depend on assumptions about the distribution of T (ri). In the following,

we assume T (ri) = tc for all i.

With FastTrack, an instance may be executed by a normal track in time ts =

(1 + qe)tc + qc or by a fast track in time tpf , where qc and qe are overheads. In the

best case, all fast instances are correct (α = 1) and the machine has unlimited

resources p =∞. Each time the fast track finishes an instance, a normal track is

started. Thus, the active normal tracks form a pipeline if considering only dual-

track instances (the component T (Er) in T (E)). The first fast instance is verified

after ts. The remaining n − 1 instances finish at a rate of t∞f = (1 + qe)xtc + qc,

where x is the speedup by fast track and qc and qe are overheads.

Using the superscript to indicate the number of processors, the average time

and the overall speedup are

t∞f =(ts + (n− 1)t∞f )

n

speedup∞ =original time

fast track time=

ntc + T (Eu)

nt∞f + T (Eu)

In the steady state tct∞f

dual-track instances are run in parallel. For simplicity

the equation does not show the fixed lower bound of fast track performance.

Since a fast instance is aborted if it turns out to be slower than the normal

instance, the worst-case is t∞f = ts = (1 + qe)tc + qc, and consequently speedup =

ntc+T (Eu)n((1+qe)tc+qc)+T (Eu)

. While this is slower than the original speed (speedup ≤ 1),

120

the worst-case time is bounded only by the overhead of the system and not by the

quality of fast-track implementation (factor x).

As a normal instance for ri finishes it may find the fast instance incorrect,

canceling the on-going parallel execution, and restarting the system from ri+1.

This is equivalent to a pipeline flush. Each failure adds a cost of ts − t∞f , so the

average time with a success rate α is (1− α)(ts − t∞f ) + tpf .

For the sake of illustration, assume no fast-track throttling when considering

the limited number of processors. With p processors, the system can have a fast

track execution depth d of at most d = min(p−1, tst∞f

) dual-track instances running

concurrently. Because d is an average it may take a non-integral value. When

α = 1, p− 1 dual-track instances take ts + (p− 2)t∞f (p ≥ 2) time. Therefore the

average time (assuming p− 1 | n) is

tpf =ts + (d− 1)t∞f

d

When α < 1, the cost of restarting has the same effect as in the infinite-processor

case. The average time and the overall speedup are

tpf = (1− α)(ts − t∞f ) +ts + (d− 1)t∞f

d

speedupp =ntc + T (Eu)

ntpf + T (Eu)

Finally consider FastTrack throttling. As p − 1 dual-track instances execute

and when the last fast instance finishes, the system start the next normal instance

instead of waiting for the first normal instance to finish (and start the next normal

and fast instances together). Effectively it finishes d+ (ts− dt∞f ) instances, hence

the change to the denominator. Augmenting the previous formula we have

tpf = (1− α)(ts − t∞f ) +ts + (d− 1)t∞fd+ ts − dt∞f

121

After simplification, FastTrack throttling may seem to increase the per instance

time rather than decreasing it. But it does decrease the time because d ≤ tst∞f

.

The overall speedup (bounded from below and n ≥ 2) is as follows, where all the

basic factors are modeled.

speedupp = max

(ntc + T (Eu)

nts + qc + T (Eu),ntc + T (Eu)

ntpf + T (Eu)

)

Simulation Results

By translating the above formula into actual speedup numbers the effect of major

parameters can be examined. Of interest are the speed of the fast track, the

success rate, the overhead, and the portion of the program executed in dual-track

regions. The four graphs in Figure 5.3 show their effect for different numbers of

processors ranging from 2 to 10 in a step of 1. The fast-track system has no effect

on a single-processor system.

All four graphs include the following setup where the fast instance takes

10%the time of the normal instance (x=0.1), the success rate (α) is 100%, the

overhead (qc and qe) adds 10% execution time, and the program spends 90% of the

time in dual-track regions. The performance of this case is shown by the second

highest curve in all but graph 5.3(a), in which it is shown by the highest curve.

FastTrack improves the performance from a factor of 1.60 with 2 processors to a

factor of 3.47 with 10 processors. The maximal possible speedup for this case is

3.47. When we change the speed of the fast instance to vary from 0% to 100%

the time of the normal instance, the speedup changes from 1.80 to 1.00 with 2

processors and from 4.78 to 1.09 with 10 processors, as shown by graph 5.3(a).

When the success rate is reduced from 100% to 0%, the speedup changes from

1.60 to 0.92 (8% slower because of the overhead) with 2 processors and from

3.47 to 0.92 with 16 processors, as shown by the graph in 5.3(a). Naturally the

performance hits the worst case when the success rate is 0%.

122

When the overhead is reduced from 100% to 0% of the running time, the

speedup increases from 1.27 to 1.67 with 2 processors and from 2.26 to 3.69 with

16 processors, as shown by graph 5.3(c). Note that with 100% overhead the

fast instance still finishes in 20% the time of the normal instance, although the

checking needs to wait twice as long.

Finally, when the coverage of the fast-track execution increases from 10% to

100%, the speedup increases from 1.00 to 1.81 with 2 processors and from 1.08

to 4.78, as shown by the graph 5.3(d). If the analytical results are correct, it is

not overly difficult to obtain a 30% improvement with 2 processors, although the

maximal gain is limited by the time spent outside dual-track regions, the speed

of the fast instance, and the overhead of fast-track.

The poor scalability is not a surprise given the program is inherently sequential

to begin with. Two final observations from the simulation results are important.

First, FastTrack throttling is clearly beneficial. Without it there can be no im-

provement with 2 processors. It often improves the theoretical maximum speedup,

although the increase is slight when the number of processors is large. Second, the

model simplifies the effect of FastTrack system in terms of four parameters, which

we have not validated with experiments on a real system. On the other hand,

if the four parameters are the main factors, they can be efficiently monitored at

run time, and the analytical model may be used as part of the on-line control to

adjust the depth of fast-track execution with the available resources.

5.6.2 Experimental Results

Implementation and Experimental Setup

Compiler support for FastTrack is implemented in gcc 4.0.1’s intermediate lan-

guage, GIMPLE (based on static-single assignment [13]). The transformation is

applied after high-level program optimization passes but before machine code gen-

123

eration. The main transformation is converting global variables to use dynamic

allocation, so the run-time support can track them and set appropriate access

protection. The modified compiler allocates a pointer for each global (and file

and function static) variable, inserts an initialization function in each file that

allocates heap memory for variables (and assigns initial values) defined in the file,

and redirects all accesses through the global pointer. The indirection causes only

marginal slowdown because most global-variable accesses have been removed or

converted to (virtual) register access by earlier optimization passes.

Compiler analysis for data protection of local variables has not been imple-

mented. Stack data are not checked, but global and heap variables are protected.

The run-time system is implemented as a statically linked library using shared

memory only for storing access maps. The design guarantees forward progress,

which means no deadlocks or starvation provided that the operating system does

not permanently stall any process.

Parallel Memory Safety Checking

A FastTrack version of Mudflap has been generated for the C-language bench-

marks 401.bzip2, 456.hmmer, 429.mcf, and 458.sjeng from the spec 2006 suite[55].

These benchmarks represent computations in pattern matching, mathematical op-

timization, chess playing, and data compression. The number of program lines

ranges from a few thousand to over ten thousand. All four programs show sig-

nificant improvement, up to a factor of 2.7 for 401.bzip2, 7.1 for 456.hmmer, and

2.2 for 429.mcf and 458.sjeng. The factors affecting the parallel performance are

the coverage of FastTrack and the relative speed of the fast track as discussed in

our analytical model. One factor not tested here is the overhead of correctness

checking and error recovery. The running times with and without Mudflap over-

head, as given in the captions in Figure 5.4, show that memory-safety checking

delays the execution by factors of 5.4, 15.0, 8.6, and 67.4. By utilizing multiple

124

processors, FastTrack reduces the delay to factors of 2.0, 2.1, 3.7, and 28.8, which

are more tolerable for long-running programs.

The code change in 429.mcf includes replicating the call of price out impl in

function global opt in file mcf.c. Similar to the code in the FastTrack example in

the introduction, the original call is placed in the normal track and the call to the

clone, clone price out impl, in the fast track. For 458.sjeng, the call of search in

function search root in file search.c is similarly changed to use clone search in the

fast track and search in the normal track. In both cases, merely four lines of code

need to be modified.

Memory safety checking by Mudflap more than triples the running time of

mcf. FastTrack improves the speed of checking by over 30%. The running time

of fast track is within half a second of a dual track implication, which shows that

FastTrack runs with little overhead. The cost of safety checking for 458.sjeng is

a factor of 200 slowdown—it takes 24 minutes to check the original execution of

7.3 seconds. FastTrack is able to reduce the checking time to 13 minutes, a factor

of two reduction. A dual track style execution without verification runs faster,

finishing in under 9 minutes without the overhead of checking every memory

access.

Results of Sort and Search Tests

The following two tests are intended to measure the performance of FastTrack

use for the support of unsafe optimizations as executed with two Intel dual-core

Xeon 3Ghz processors. Compilation is done using the modified FastTrack version

of gcc using the optimizations specified by the -O3 flag. The first test is a

simple sorting program that repeatedly sorts an array of 10,000 elements. In a

specified percentage of iterations the array contents are randomized. The array

sort is performed with either a short-circuited bubble sort, a quick sort, or by

running both in a FastTrack environment. The results of these tests are shown in

125

Figure 5.5. The quick sort performs consistently and largely independent of the

input array. One can see that the bubble sort quickly detects when the array is

sorted, but performs poorly in cases in which the contents have been randomized.

The FastTrack approach is able to out-perform either of the individual sorting

algorithms. These results illustrate the utility of Fast-Track in cases where both

solutions are correct, knowing which is actually faster is not possible in advance.

In cases where the array is always sorted or always unsorted, the overhead of using

FastTrack will cause it to lose out. Although FastTrack is not a better solution

compared to an explicitly parallel sorting approach, this example motivates the

utility of automatically selecting the faster of multiple sequential approaches.

Algorithm 5.6.1 Pseudo code of the synthetic search program

for i = 1 to n doVi ← random

end forfor 1 to T do

if normal track thenfor i = 1 to n doVi ← f(Vi)

end form← max(v : v ∈ V )

else {fast track}R← S random samples from Vfor j = 1 to S doR← f(Ri)

end form← max(r : r ∈ R)

end ifrandomly modify N1 elements

end forprint m

The second program is a simple search to test the effect of various parameters,

for which the basic algorithm is given in Figure 5.6.1. The program repeatedly

updates some elements of a vector and finds the largest result from certain com-

putations. By changing the size of the vectors, the size of samples, and the

126

frequency of updates, we can effect different success rates by the normal and the

fast instances. Figure 5.6(a) shows the speedups over the base sequential execu-

tion, which takes 3.33 seconds on a 4-CPU machine. The variation between times

of three trials is always smaller than 1 millisecond.

The sampling-based fast instance runs in 2.3% the time of the normal instance.

When all fast instances succeed, they improve the performance by a factor of 1.73

on 2 processors, 2.78 on 3 processors, and 3.87 on four processors. When the

frequency of updates is reduced the success rate drops. At 70%, the improvement

is a factor of 2.09 on 3 processors and changes only slightly when the fourth

processor is added. This drop is because the chance of four consecutive fast

instances succeeding is only 4%. When the success rate is further reduced to

30%, the chance for three consecutive successful fast tracks drops to 2.7%. The

speedup from 2 processors is 1.29 and no improvement is observed for more than 2

processors. In the worst case when all fast instances fail, we see that the overhead

of forking and monitoring the normal track adds 6% to the running time.

The results in Figure 5.6(b) show interesting trade-offs when the fast track

is tuned by changing the size of samples. On one hand, a larger sample size

means more work and slower speed for the fast track. On the other hand, a

larger sample size leads to a higher success rate, which allows more consecutive

fast tracks succeed and consequently more processors utilized. The success rate

is 70% when the sample size is 100, which is the same configuration as the row

marked “70%” in Figure 5.6(a). The best speedup for 2 processors is found when

the sample size is 200 but adding more processors does not help as much (2.97

speedup) as when the sample size is 300, where 4 processors lead to a speedup

of 3.78. The second experiment shows the significant effect of tuning when using

unsafely optimized code. Experience has shown that the automatic support and

analytical model have made tuning much less labor intensive.

127

enqueue new normal track

p-1 active normal-track processes

the fast-track

process

ready queue(1 to p-1 waiting normal track)

dequeue the ready queue

next normal track becomes active

balancedsteady state

less than p-1 active normal-

track processes

the fast-track

process

ready queue(empty)

waiting to dequeue

enqueue new normal track

next normal track becomes active

fast tracktoo slow

p active normal-track processes

fast track stopped

ready queue(p waiting normal tracks)

next normal track becomes active

next ending normal track re-activates the fast track

fast track activates the next normal track and then stops

fast-trackthrottling

Figure 5.2: The three states of fast track: balanced steady state, fast-track throt-tling when it is too fast, and slow-track waiting when fast track is too slow. Thesystem returns to the balanced steady state after fast-track throttling.

128

0.5

1

1.5

2

2.5

3

3.5

4

108642

speedups

number of processors

fast-track speed: 0%, 10%, ..., 100%

0.5

1

1.5

2

2.5

3

3.5

4

108642

speedups

number of processors

success rate: 100%, 90%, ..., 0%

0.5

1

1.5

2

2.5

3

3.5

4

108642

speedups

number of processors

fast-track overhead: 0%, 10%, ..., 100%

0.5

1

1.5

2

2.5

3

3.5

4

108642

speedups

number of processors

exe. in fast track: 100%, 90%, ..., 0%

Figure 5.3: Analytical results of the fast-track system where the speed of the fasttrack, the success rate, the overhead, and the portion of the program executed indual-track regions vary. The order of the parameters in the title in each graphcorresponds to the top-down order of the curves in the graph.

129

1 2 3 4 5 6 7 8

0.0

0.5

1.0

1.5

2.0

2.5

bzip2

number of processors

spee

dup

o

o

o

o

o o o o

0.0

0.5

1.0

1.5

2.0

2.5

(a) The checking time of 401.bzip2 is re-duced from 24.5 seconds to 9.0 seconds.The base running time, without memorysafety checking, is 4.5 seconds.

1 2 3 4 5 6 7 80

24

68

hmmer

number of processors

spee

dup

o

o

o

oo

oo

o

02

46

8

(b) The checking time of 456.hmmer isreduced from 235 seconds to 33.2 sec-onds. The base running time is 15.6 sec-onds.

1 2 3 4 5 6 7 8

0.0

0.5

1.0

1.5

2.0

2.5

mcf

number of processors

spee

dup

o

oo

oo

o o o

0.0

0.5

1.0

1.5

2.0

2.5

(c) Fast track reduces the checking timeof 429.mcf from 56 seconds to 24.8 sec-onds. The base running time is 6.7 sec-onds.

1 2 3 4 5 6 7 8

0.0

0.5

1.0

1.5

2.0

2.5

sjeng

number of processors

spee

dup

o

oo o

o o o o0.

00.

51.

01.

52.

02.

5

(d) Fast track reduces the checking timeof 458.sjeng from 33.7 seconds to 14.4seconds. The base running time is 0.5seconds.

Figure 5.4: The effect of FastTrack Mudflap on four spec 2006 benchmarks.

130

0.001

0.01

0.1

1

10

100

1000

0 5 10 25 50 75 100

Spe

edup

ove

r qu

ick-

sort

Percentage of Iterations that Modify

quickfast-track

bubble

Figure 5.5: Sorting time with quick sort, bubble sort or the FastTrack of both

success number processorsrate 1 2 3 4100% 1 1.73 2.78 3.8770% 1 1.47 2.09 2.1530% 1 1.29 1.29 1.290% 1 0.94 0.94 0.94

(a) Effect of fast-track success rates onthe synthetic benchmark

sample number processorssize 1 2 3 4100 1 1.48 2.09 2.15200 1 1.71 2.64 2.97300 1 1.70 2.71 3.78400 1 1.68 2.69 3.74

(b) The speedup due to fast-track tun-ing of the synthetic benchmark

Figure 5.6: FastTrack on synthetic benchmarks

131

6 Conclusion

6.1 Contributions

I have presented two systems for implementing speculative parallelism in exist-

ing programs. For each system I have implemented a complete working system

including compiler and run-time support. The first system, bop, provides a pro-

grammer with tools to introduce traditional types of parallelism in cases where

program dependencies cannot be statically evaluated or guaranteed. I have shown

the use of bop to effectively extract parallelism from utility programs.

I have also presented FastTrack, a system that supports unsafely optimized

code and can also be used to off-loaded safety checking and other program anal-

ysis. The key features of the systems include a programmable interface, compiler

support, and a concurrent run-time system that includes correctness checking, out-

put buffering, activity control, and fast-track throttling. I have used the system

to parallelize memory safety checking for sequential code, reducing the overhead

by up to a factor of seven for four large size applications running on a multicore

personal computer. We have developed an analytical model that shows the effect

from major parameters including the speed of the fast track, the success rate, the

overhead, and the portion of the program executed in fast-track regions. We have

132

used our system and model in speculatively optimizing a sorting and a search

program. Both analytical and empirical results suggest that fast track is effective

at exploiting today’s multi-processors for improving program speed and safety.

6.2 Future Directions

6.2.1 Automation

Automating the insertion of bop region markers requires identifying pprs auto-

matically, which is similar to identifying parallelism — a major open problem.

Because pprs are only hints at parallelism, it’s not necessary for them to be cor-

rect. In addition to inserting the ppr markers automatically, the system could be

simplified by allowing the EndPPR marker to be optional. The difficulty in doing

this comes in handling the final instance of the ppr. Without an end marker,

the speculative task will continue until it reaches a program exit point. The non-

speculative will execute the ppr, and subsequently repeat the same execution as

the speculative task. Such duplicated work is certainly wasteful, but may be ac-

ceptable if there is no other useful work that could be offloaded to the additional

processing unit.

In order to automate the use of the FastTrack system, markers can be inserted

at various points throughout the code using compiler instrumentation. We can

choose dynamically whether to initiate a new dual-track region based on the past

success rate and the execution time since the start of the last region. A region

can begin at an arbitrary point in execution, as long as the other track makes

the same decision at that point. We can identify the point with a simple shared

counter each track increments every time it passes a marker. The fast track makes

its increments atomically, and when it creates a new normal track it begins a new

counter (leaving the old one for the previous normal track). As the normal tracks

133

pass marks they compare their counter to the fast track’s to identify the mark at

which verification needs to be performed. If the two processes did not follow the

same execution path then the state verification will fail.

A significant problem is ensuring that the fast path includes all of the markers

the normal track has. This is directly related to where the markers are placed, and

how the two tracks are generated. In a case like the fast mudflap implementation

described in Chapter 5 the markers will be consistent as long as they are not

placed in the mudflap routines. In any case where code is similarly inserted to

create the normal track, it will suffice to simply not insert markers with that code.

In a case where the fast track is created by removing optimizations from existing

code, we must ensure that markers are not removed, and that any function calls

are not directly removed because they might contain further markers.

6.2.2 Composability

One of the major problems in parallel programming — particularly when dis-

cussing explicit parallel programming with locks — is the composability of various

operations. The intuition behind composability is that the combination of multi-

ple components should not break correctness.

Lack of composability is a significant weakness of lock based components, and

is one of the strengths of transactional memory systems. Because the speculative

parallelism run-time systems are intended to be a simple way to extend existing

programs, the bop and FastTrack system should seek to compose correctly. There

are several general questions to ask about the composability of these systems: do

each compose with itself, do they compose with one another, and do they compose

with existing parallel programming techniques.

Self-Composition The question of self-composition is whether the run-time

system properly handles entering a speculative region when one is already active.

134

Cases where disjoint regions of the program use speculation compose trivially. The

bop run-time system does correctly compose with itself. The implementation is

designed to that nested use of pprs are not allowed, but are detected and handled

correctly. If a piece of code (for example a library) is built to use pprs, and that

is invoked from within another ppr, the inner regions will be ignored. Although

this maintains semantic correctness — which is the primary concern — it may

not be the most effective solution.

The FastTrack run-time system also maintains semantic correctness when it is

composed with itself. When the FastTrack system encounters a nested fast track

region, the runtime will treat it like any other dual track region. If the fast track is

the first to reach the nested region it will spawn a new normal track. Eventually

the normal track will encounter the end of the original dual track region, and

speculation will fail. Although semantic correctness is maintained, performance

will suffer because speculation over the entire outer region will always fail. This

failure could potentially be avoided if fast track regions were given identifiers. The

run-time system would also need a mechanism to match the identifier the normal

track encounters at the end of its region to the fast track. Additionally, the fast

track would need to abandon the inner normal tracks and to reacquire the changes

it made starting at the beginning of the outer region (which are otherwise simply

left for the inner normal track to verify).

If the normal track reaches a nested region then it will assume that the fast

track has miss-speculated, or is otherwise delayed, and that it has simply com-

pleted executing the region first. As in any case where the normal track wins the

race, it will terminate the fast track. The normal track will then assume the role

of fast track and spawn a new normal track to handle the inner region. From a

performance standpoint this is not likely to be the most effective solution because

only the smaller inner region will be fast tracked. Nevertheless, it is a better out-

come than the case above, and it does maintain semantic correctness. In the case

135

that both tracks encounter a nested dual track region, the result is very much like

the above case in which only the normal track encounters the inner region.

Algorithm 6.2.1 Example of FastTrack self-composition

void ou t e r ( void ) {i f (FT\ Beg inFas tTrack ( ) ) {

i n n e r f a s t ( ) ;} e l se {

i n n e r n o rma l ( ) ;}

}

void i n n e r f a s t ( void ) {i f (FT\ Beg inFas tTrack ( ) ) {

. . .} e l se {

. . .}

}

6.2.3 Further Evaluation

One of the major issues in contemporary computing is power consumption. This is

true for mobile systems drawing from a battery, to high performance computing

systems and data centers built on massive numbers of processors. Because so

much power drawn by a computer ends up as waste heat, even more power must be

drawn to cool the system. As such, the power utilization of speculative parallelism

must be considered. Although there will always be a demand for the ability to

complete tasks more quickly, the power costs to do so have to be put in balance.

One way to frame the energy consumption question is to consider the com-

parable energy necessary to gain the same speed increase using a uni-processor.

Conversely, if the speculative parallelism system can allow a program to be com-

136

pleted as quickly on two or more processors running at a slower clock rate, an

energy savings may be found.

As processor vendors produce systems with higher numbers of cores, they face

the reality that often many of these cores are not utilized. The two largest chip

makes have both introduced schemes to allow some of the cores on the multi-core

systems, which they refer to as “turbo boost” (Intel) and “dynamic speed boost”

(AMD). Evaluating the energy consumption of speculatively parallelized programs

on such systems would provide empirical data to address such a hypothesis.

137

A Code Listings

Included here are source code fragments not found earlier in this dissertation.

Where relevant, a reference to the earlier source is included. The inclusion of

system header files and standard pre-processor include guards have been omitted

for brevity.

A.1 BOP Code

Listing A.1: Private Header

s t a t i c i n t specDepth ; // Between 1 and MaxSpecDepth.

s t a t i c i n t specP id ; // Process ID of SPEC.

s t a t i c i n t undyWorkCount ; // Work done by UND

s t a t i c i n t pprID ; // User assigned region ID.

s t a t i c i n t mySpecOrder = 0 ; // Serial number.

// For Data update .

s t a t i c i n t updateP ipe [ 2 ] ;

// For termination of the understudy .

s t a t i c i n t undyCreatedP ipe [ 2 ] ;

138

s t a t i c i n t undyConcedesPipe [ 2 ] ;

// loHiPipes control information flow. The main process

// takes the 0th slot to send MainDone and MainCopyDone.

s t a t i c i n t l oH iP i p e s [MAX SPEC DEPTH∗2+1 ] [ 2 ] ;

// Flag set if the next speculation fails .

s t a t i c v o l a t i l e char e a r l yT e rm i n a t i o n = f a l s e ;

// Signal sets masking SIGUSR1, SIGUSR2, and both

s t a t i c s i g s e t t s igMaskUsr1 , s igMaskUsr2 , s igMaskUsr12 ;

s t a t i c void BOP AbortSpec ( void ) ;

s t a t i c void BOP AbortNextSpec ( void ) ;

Listing A.2: Access Map Handling

// Implementation depends on maps of single byte units .

char ∗useMap ;

char ∗accMapPtr ;

// Record access of the given type in my access map.

void BOP recordAccess ( void ∗ page add r e s s , AccessType a c c e s s ) {

i n t mapId = mySpecOrder==0 ? 0 : MYRESOURCE(mySpecOrder ) ;

char∗ map = NULL ;

switch ( a c c e s s ){

case READ:

map = accMapPtr + (mapId ∗ 2 ∗ BIT MAP SIZE ) ;

break ;

case WRITE :

map = accMapPtr + ( ( ( mapId ∗ 2) + 1) ∗ BIT MAP SIZE ) ;

break ;

}

139

SP recordAccessToMap ( page add r e s s , map ) ;

}

void BOP setPro tec t i on ( i n t p ro t ) {

unsigned long page ;

unsigned long l a s t = 0 , f i r s t = 0 ;

// Look at each position in the map.

f o r ( page = 0 ; page < PAGECOUNT; page++) {

i f ( SP checkMap ( ( void ∗ ) ( page ∗ PAGESIZE ) , useMap ) ) {

i f ( page == l a s t +1) l a s t ++;

e l s e {

i f ( l a s t > 0) SP pro tec tPages ( f i r s t , l a s t , p r o t ) ;

l a s t = f i r s t = page ;

}

}

}

i f ( l a s t > 0) SP pro tec tPages ( f i r s t , l a s t , p r o t ) ;

}

// Returns zero if the there are no conflicts in the maps .

s t a t i c i n t BOP compareMaps ( void ) {

char∗ p r e vWr i t e s ;

i f ( mySpecOrder == 1)

p r e vWr i t e s = WRITEMAP( 0 ) ;

e l s e /∗ the union map ∗/

p r e vWr i t e s = READMAP(mySpecOrder − 1 ) ;

char∗ cu rWr i t e s = WRITEMAP(mySpecOrder ) ;

char∗ curReads = READMAP(mySpecOrder ) ;

f o r ( unsigned i = 0 ; i < BIT MAP SIZE ; i++) {

i f ( p r e vWr i t e s [ i ] & ( curReads [ i ] | cu rWr i t e s [ i ] ) ) return 1 ;

/∗ compute the union map in place ∗/

140

curReads [ i ] = p r e vWr i t e s [ i ] | cu rWr i t e s [ i ] ;

}

return 0 ;

}

Listing A.3: Signal Handlers

void BOP RaceHandler ( i n t s i gno , s i g i n f o t ∗ i n f o , u c o n t e x t t ∗ cn t x t ) {

a s s e r t ( SIGUSR1 == s i gno ) ;

a s s e r t ( c n t x t ) ;

// Committing Spec seeing its signal , SIGUSR1. No action .

i f ( i n f o−>s i p i d == ge t p i d ( ) ) return ;

// SOS: the next process has a conflict . Start early termination .

// (set myself as the last process of the group) .

i f ( myStatus == SPEC) {

BOP AbortNextSpec ( ) ;

return ;

}

i f ( myStatus != UNDY) return ;

// Sending a symbolic value .

w r i t e ( undyConcedesPipe [ 1 ] , &mySpecOrder , s i z eo f ( i n t ) ) ;

e x i t ( 0 ) ;

}

// Spec or main gets a segmentation fault .

void BOP SegvHandler ( i n t num , s i g i n f o t ∗ i n f o , u c o n t e x t t ∗ cn t x t ) {

void ∗ f au l tAdd = in f o−>s i a d d r ;

// This should only be the handler for SEGV signals , and we

// only handle the case of permission violations .

a s s e r t (num == SIG MEMORY FAULT ) ;

i f ( i n f o−>s i c o d e != SEGV ACCERR) {

141

whi le (1 ) pause ( ) ;

a s s e r t ( 0 ) ;

}

// Check if the predecessor wrote to this page .

// A more complete check i s done after this and pred complete .

unsigned mapID = MYRESOURCE(mySpecOrder − 1 ) ;

char∗ mapPtr = accMapPtr + (mapID ∗ 2 + 1) ∗ BIT MAP SIZE ;

unsigned a c c e s s = SP checkMap ( fau l tAdd , mapPtr ) ;

i f ( myStatus==SPEC && ac c e s s ) BOP AbortSpec ( ) ;

i f (WRITEOPT( cn t x t ) ) { // A write access .

BOP AbortNextSpec ( ) ;

BOP recordAccess ( fau l tAdd , WRITE ) ;

i f ( mprotect (PAGESTART( fau l tAdd ) , 1 , PROT WRITE |PROT READ) )

e x i t ( e r r n o ) ;

} e l s e { // A read access .

BOP recordAccess ( fau l tAdd , READ) ;

i f ( mprotect (PAGESTART( fau l tAdd ) , 1 , PROT READ) )

e x i t ( e r r n o ) ;

}

}

void BOP UndyTermHandler ( i n t num , s i g i n f o t ∗ i n f o , u c o n t e x t t ∗ cn t x t )

{

a s s e r t ( SIGUSR2 == num ) ;

a s s e r t ( c n t x t ) ;

i f ( i n f o−>s i p i d == ge t p i d ( ) ) return ; /∗ Must be Undy ∗/

e x i t ( 0 ) ;

}

// See Listing 4.3.1 for BOP PrePPR implementation .

142

s t a t i c i n t BOP pipeClose ( void ) {

i n t i = 0 , h a sE r r o r =0;

f o r ( i =0; i<=MAX SPEC DEPTH∗2 ; i++) {

ha sE r r o r |= c l o s e ( l oH iP i p e s [ i ] [ 0 ] ) ;

h a sE r r o r |= c l o s e ( l oH iP i p e s [ i ] [ 1 ] ) ;

}

ha sE r r o r |= c l o s e ( undyCreatedP ipe [ 0 ] ) ;

h a sE r r o r |= c l o s e ( undyCreatedP ipe [ 1 ] ) ;

h a sE r r o r |= c l o s e ( undyConcedesPipe [ 0 ] ) ;

h a sE r r o r |= c l o s e ( undyConcedesPipe [ 1 ] ) ;

i f ( h a sE r r o r ) {

p e r r o r ( ” f a i l e d to c l o s e p i p e s ” ) ;

myStatus = SEQ;

return 0 ;

}

e l s e return 1 ;

}

// See Listing 4.3.3 for BOP End implementation .

// See Listing 4.3.4 for PostPPR commit implementation .

// See Listing 4.3.4 for PostPPR main implementation .

// See Listing 4.3.4 for PostPPR spec implementation .

// See Listing 4.3.4 for PostPPR undy implementation .

s t a t i c i n t BOP Pipe In i t ( void ){

i n t i , h a sE r r o r = 0 ;

f o r ( i =0; i<=MAX SPEC DEPTH∗2 ; i++)

ha sE r r o r |= p ipe ( l oH iP i p e s [ i ] ) ;

h a sE r r o r |= p ipe ( undyCreatedP ipe ) ;

h a sE r r o r |= p ipe ( undyConcedesPipe ) ;

143

i f ( h a sE r r o r ) {

p e r r o r ( ” update p i p e c r e a t i o n f a i l e d : ” ) ;

myStatus = SEQ;

return 0 ;

}

e l s e return 1 ;

}

s t a t i c void BOP timerAlarmExit ( i n t s i g no ) {

( void ) s i g no ;

k i l l ( 0 , SIGKILL ) ;

}

s t a t i c void BOP timerTermExit ( i n t s i g no ) {

a s s e r t (SIGTERM == s i gno ) ;

s i g n a l (SIGTERM, SIG IGN ) ;

k i l l ( 0 , SIGTERM) ;

e x i t ( 0 ) ;

}

// Allocates the shared data and installs the timer process .

void BOP In i t ( )

{

s t a t i c i n t i n i t d o n e = 0 ;

i f ( i n i t d o n e ) return ;

i n i t d o n e = 1 ;

char ∗ curPnt = mmap(NULL , ALLOC MAP SIZE + ACC MAP SIZE ,

PROT READ | PROT WRITE,

MAP ANONYMOUS | MAP SHARED, −1, 0 ) ;

a s s e r t ( curPnt ) ;

useMap = curPnt ;

accMapPtr = curPnt + ALLOC MAP SIZE ;

144

// Setup BOP process group.

SP gpid = ge t p i d ( ) ;

s e t p g i d (0 , SP gpid ) ;

// Prepare signal handlers .

s i g n a l ( SIGINT , BOP timerAlarmExit ) ;

s i g n a l ( SIGQUIT , BOP timerAlarmExit ) ;

s i g n a l ( SIGUSR1 , SIG DFL ) ;

s i g n a l ( SIGUSR2 , SIG DFL ) ;

// Pre−made for signal blocking and unblocking

s i g emp t y s e t (&s igMaskUsr1 ) ;

s i g a d d s e t (&sigMaskUsr1 , SIGUSR1 ) ;

s i g emp t y s e t (&s igMaskUsr2 ) ;

s i g a d d s e t (&sigMaskUsr2 , SIGUSR2 ) ;

s i g emp t y s e t (&s igMaskUsr12 ) ;

s i g a d d s e t (&sigMaskUsr12 , SIGUSR1 ) ;

s i g a d d s e t (&sigMaskUsr12 , SIGUSR2 ) ;

// Prepare post/wait

BOP Pipe In i t ( ) ;

// Create the timer process , which waits for the whole

// speculative precessing team to complete .

i n t f i d = f o r k ( ) ;

switch ( f i d ) {

case −1:

myStatus = SEQ;

return ;

case 0 : // The child i s the new control .

myStatus = CTRL ;

s e t p g i d (0 , SP gpid ) ;

return ;

defau l t :

145

// Setup SIGALRM

s i g n a l (SIGALRM , BOP timerAlarmExit ) ;

s i g n a l (SIGTERM, BOP timerTermExit ) ;

whi le (1 ) pause ( ) ; // Timer waits for the program to end .

}

}

void BOP PostPPR( i n t i d ) {

// Ignore a PPR ending if it doesn ’t match the PPR we started

i f ( i d != pprID ) return ;

ppr ID = −1;

switch ( myStatus ) {

case UNDY:

return PostPPR undy ( ) ;

case SPEC :

return PostPPR spec ( ) ;

case MAIN :

return PostPPR main ( ) ;

case SEQ:

case CTRL :

return ; // No action .

defau l t :

a s s e r t ( 0 ) ;

}

}

s t a t i c void BOP AbortSpec ( void ) {

a s s e r t ( myStatus == SPEC ) ;

146

// With no earlier SPEC, just let UNDY take over .

i f ( mySpecOrder == 1) e x i t ( 0 ) ;

// Initiate early termination in the parent .

k i l l ( g e t pp i d ( ) , SIGUSR1 ) ;

e x i t ( 0 ) ;

}

s t a t i c void BOP AbortNextSpec ( void ) {

e a r l yT e rm i n a t i o n = t r u e ;

// Kill any following SPEC

i f ( specP id !=0) k i l l ( specPid , SIGKILL ) ;

}

A.2 Fast Track Code

Listing A.4: Public Header File

i n t FT BeginFastTrack ( void ) ;

i n t FT BeginDualTrack ( void ) ;

void FT PostDualTrack ( void ) ;

Listing A.5: Private Header File

s t a t i c boo l FT ac t i v e ; // True during a dual−track region .

// The maximum number of speculation processes allowed.

s t a t i c unsigned FT maxSpec = 2 ;

// Access maps used by each fast/normal pair :

s t a t i c char∗ FT fastAccMap ; // Fast track

s t a t i c char∗ FT slowAccMap ; // Slow track

s t a t i c char∗ FT accMap ; // Alias to local map

147

// Queue for waiting slow tracks .

typedef s t ruc t {

i n t p i p e [ 2 ] ;

sem t sem ;

v o l a t i l e unsigned r e c e n t ; // Newest activte track.

v o l a t i l e boo l wa i t i n g ; // True when the FT yields .

} readyQueue ;

s t a t i c readyQueue ∗ readyQ ;

// Communication channels :

// Channel for passing data updates after verification .

s t a t i c i n t updateP ipe [ 2 ] ;

// File descriptors for assigning seniority . Each slow track reads

// from inheritance pipe and writes to the bequest .

s t a t i c i n t i n h e r i t a n c e , beques t ;

// Slow tracks ”open” a floodgate of another waiting slow track.

#def ine FLOODGATESIZE (2 ∗ (MAX SPEC DEPTH + 1))

s t a t i c i n t f l o o d g a t e s [ FLOODGATESIZE ] [ 2 ] ;

// Unique identifier for speculative processes .

s t a t i c unsigned FT order = 0 ;

Listing A.6: Utility Functions

s t a t i c void ∗ FT sharedMap ( s i z e t l e n g t h ) {

return mmap(NULL , l eng th ,

PROT READ | PROT WRITE,

MAP ANONYMOUS | MAP SHARED, −1 , 0 ) ;

}

/// Transitions to error state and returns 0.

s t a t i c char FT errorOnBeg in ( void ) {

myStatus = SEQ;

return 0 ;

}

148

s t a t i c i n t FT getDepthFromEnv ( void ) {

char∗ c v a l ;

s t a t i c const i n t de f = 2 ; // Default value

i n t depth = de f ;

c v a l = getenv ( ”BOP SpecDepth” ) ;

i f ( c v a l != NULL) depth = a t o i ( c v a l ) ;

// Must be in the range [0 , MAX]

i f ( depth < 0 | | depth > MAX SPEC DEPTH) depth = de f ;

return depth ;

}

/// Returns zero on success .

s t a t i c char FT setupMaps ( void ) {

// Allocate two access maps contiguously .

char∗ accMap = FT sharedMap (ACC MAP SIZE ∗ 2 ) ;

i f (MAP FAILED == accMap ) return 1 ;

FT fastAccMap = accMap ;

FT slowAccMap = accMap + ACC MAP SIZE ;

return 0 ;

}

Listing A.7: Floodgate Control

s t a t i c i n l i n e i n t ∗ FT f loodGateFor ( i n t specOrde r ) {

return f l o o d g a t e s [ specOrde r%FLOODGATESIZE ] ;

}

/// Synchronously reads a single token from the floodgate

/// associated with the current process .

149

s t a t i c i n l i n e i n t FT readFloodGate ( void ) {

i n t token ;

i n t ∗ gate = FT f loodGateFor ( FT order ) ;

SP sync r ead ( gate [ 0 ] , &token , s i z eo f ( token ) ) ;

return token ;

}

/// Opens the floodgate for track.

s t a t i c i n l i n e void FT openFloodGate ( i n t t r ack , i n t token ) {

i n t ∗ gate = FT f loodGateFor ( t r a c k ) ;

w r i t e ( gate [ 1 ] , &token , s i z eo f ( token ) ) ;

}

/// Tell any processes waiting on a floodgate to give up .

s t a t i c void FT c l e a r F l o odga t e s ( void ) {

f d s e t r e a d s e t ;

const i n t token = −1;

s t ruc t t ime v a l z e r o t ime = {0 ,0} ;

i n t n fd s = readyQ−>p i p e [ 0 ] + 1 ;

i n t nex t s l ow ;

FD ZERO(& r e a d s e t ) ;

FD SET( readyQ−>p i p e [ 0 ] , &r e a d s e t ) ;

i f (−1 == s e l e c t ( nfds , &r ead s e t , NULL , NULL , &ze r o t ime ) )

p e r r o r ( ” s e l e c t i n g ready queue” ) ;

whi le ( FD ISSET ( readyQ−>p i p e [ 0 ] , &r e a d s e t ) ) {

i f (−1 == read ( readyQ−>p i p e [ 0 ] , &next s low , s i z eo f ( n ex t s l ow ) ) )

p e r r o r ( ” read from ready queue” ) ;

FT openFloodGate ( next s low , token ) ;

i f (−1 == s e l e c t ( nfds , &r ead s e t , NULL , NULL , &ze r o t ime ) )

p e r r o r ( ” s e l e c t i n g ready queue” ) ;

}

150

}

s t a t i c void FT re l ea s eNex tS low ( void ) {

whi le (0 != sem wai t (&( readyQ−>sem ) ) ) ;

i f ( readyQ−>wa i t i n g ) {

// restart the fast

a s s e r t (FAST != myStatus ) ;

FT s lowCleanup ( ) ;

readyQ−>wa i t i n g = f a l s e ;

sem post (&( readyQ−>sem ) ) ;

e x i t ( 1 ) ;

} e l s e {

i n t s l ow t r a c k = 0 ;

// Read from ready queue until a value i s returned .

SP sync r ead ( readyQ−>p i p e [ 0 ] , &s l owt r a ck , s i z eo f ( s l ow t r a c k ) ) ;

i f ( s l ow t r a c k > 0) readyQ−>r e c e n t = s l ow t r a c k ;

sem post (&( readyQ−>sem ) ) ;

// If we got a slowtrack from the ready queue , start it .

i f ( s l ow t r a c k ) FT openFloodGate ( s l owt r a ck , FT order ) ;

}

}

// If the fast track gets too far ahead (a lot of slow tracks are

// waiting) it will yield to let some slow tracks get work done.

s t a t i c void FT cont i nueOrY i e l d ( void ) {

i f ( FT order > readyQ−>r e c e n t + FT maxSpec ) {

// Continuing after yielding to slow track

FT re l ea s eNex tS low ( ) ;

readyQ−>wa i t i n g = t r u e ;

whi le ( readyQ−>wa i t i n g ) pause ( ) ;

}

}

s t a t i c void FT becomeOldest ( void )

151

{

i n t token ;

// Wait until we are the most senior slow instance

SP sync r ead ( i n h e r i t a n c e , &token , s i z eo f ( token ) ) ;

i f ( token == −1) {

// Upstream error . Propagate and abort.

SP s yn c w r i t e ( bequest , &token , s i z eo f ( token ) ) ;

e x i t ( 1 ) ;

}

// Now the oldest slow track.

c l o s e ( i n h e r i t a n c e ) ;

}

Listing A.8: Finalization

s t a t i c i n l i n e void FT fas tC l eanup ( void ) {

i n t i ;

f o r ( i =0; i < FLOODGATESIZE ; i++) {

c l o s e ( f l o o d g a t e s [ i ] [ 0 ] ) ;

c l o s e ( f l o o d g a t e s [ i ] [ 1 ] ) ;

}

}

// Closes everything the slow track normally has open .

s t a t i c i n l i n e void FT slowCleanup ( void ) {

i n t i ;

c l o s e ( updateP ipe [ 0 ] ) ;

c l o s e ( beques t ) ;

c l o s e ( readyQ−>p i p e [ 0 ] ) ;

f o r ( i =0; i < FLOODGATESIZE ; i++) {

c l o s e ( f l o o d g a t e s [ i ] [ 0 ] ) ;

c l o s e ( f l o o d g a t e s [ i ] [ 1 ] ) ;

}

}

152

// See Listing 5.3.3 for FT CheckData implementation

#i fnde f FT AUTOMARKPOINT

#def ine FT AUTOMARKPOINT 0

#end i f

s t a t i c void FT StartAutoMarkPointTimer ( void ) ;

s t a t i c void FT In i tAutoMarkPo int ( void ) ;

s t a t i c void a t t r i b u t e ( ( c o n s t r u c t o r ) ) FT i n i t ( void ) {

i n t i ;

i n t s e n p i p e [ 2 ] ;

/∗ Shared floodgate pipes ∗/

f o r ( i =0; i<=MAX SPEC DEPTH∗2 ; i++)

i f ( p i p e ( f l o o d g a t e s [ i ] ) != 0) {

p e r r o r ( ” a l l o c a t i n g f l o o d g a t e s ” ) ;

abo r t ( ) ;

}

readyQ = FT sharedMap ( s i z eo f ( readyQueue ) ) ;

readyQ−>wa i t i n g = f a l s e ;

i f (0 != p ip e ( readyQ−>p i p e ) ) {

p e r r o r ( ” a l l o c a t i n g ready queue” ) ;

abo r t ( ) ;

}

i f (−1 == s em i n i t (&( readyQ−>sem ) , 1 , 1 ) ) {

p e r r o r ( ” unab l e to i n i t i a l i z e semaphore ” ) ;

abo r t ( ) ;

}

// Create the first seniority pipe .

i f (0 != p ip e ( s e n p i p e ) ) {

p e r r o r ( ” unab l e to i n i t i a l i z e s e n i o r i t y p i p e ” ) ;

153

abo r t ( ) ;

}

// Ensure the first slow track will know it i s the oldest .

w r i t e ( s e n p i p e [ 1 ] , &FT order , s i z eo f ( FT order ) ) ;

c l o s e ( s e n p i p e [ 1 ] ) ;

// Keep the read end open .

i n h e r i t a n c e = s e n p i p e [ 0 ] ;

FT maxSpec = FT getDepthFromEnv ( ) ;

FT ac t i v e = f a l s e ;

SP Red i r ec tOutput ( ) ;

i f (FT AUTOMARKPOINT) FT In i tAutoMarkPo int ( ) ;

}

// Automatic branch point insertion

s t a t i c unsigned FT AM count = 0 ;

s t a t i c boo l FT AM active = f a l s e ;

s t a t i c unsigned∗ FT AM joinPoint ;

s t a t i c void FT i t ime rHand l e r ( i n t s i g no ) {

a s s e r t ( s i g no == SIGALRM ) ;

FT AM active = t r u e ;

}

s t a t i c void FT A l l o c a t e J o i nPo i n t e r ( void ) {

FT AM joinPoint = FT sharedMap ( s i z eo f ( FT AM joinPoint ) ) ;

∗FT AM joinPoint = 0 ;

}

s t a t i c void FT StartAutoMarkPointTimer ( void ) {

s t ruc t t ime v a l i n t e r v a l = {0 ,500000} ;

s t ruc t i t i m e r v a l t ime r = { i n t e r v a l , i n t e r v a l } ;

154

i f ( SIG ERR == s i g n a l (SIGALRM , FT i t ime rHand l e r ) )

p e r r o r ( ” s e t t i n g s i g n a l ” ) ;

i f (0 > s e t i t i m e r ( ITIMER REAL,& t imer ,NULL) )

p e r r o r ( ” s e t t i n g t ime r ” ) ;

}

s t a t i c void FT In i tAutoMarkPo int ( void ) {

i f ( !FT AUTOMARKPOINT) return ;

FT StartAutoMarkPointTimer ( ) ;

FT A l l o c a t e J o i nPo i n t e r ( ) ;

}

i n t FT AutoMarkPoint ( void ) {

i f ( !FT AUTOMARKPOINT) return 0 ;

FT AM count++;

i f ( ! FT AM active ) return 0 ;

i f (SLOW == myStatus ){

// If the slow track has already passed the join point , then it

// i s running ahead of the fast track (or the timer didn ’t fire

// soon enough) . Slow Wins.

i f ( FT AM count > ∗FT AM joinPoint ) FT slowTakesOver ( ) ;

// If we have reached the indicated joint point , cleanup .

e l s e i f ( FT AM count == ∗FT AM joinPoint ) FT PostDualTrack ( ) ;

} e l s e i f (FAST == myStatus | | CTRL == myStatus ) {

// reset the activation

FT AM active = f a l s e ;

// indicate where the branch/join i s

∗FT AM joinPoint = FT AM count ;

munmap( FT AM joinPoint , s i z eo f ( FT AM joinPoint ) ) ;

// Setup a new joinpoint record for the next slow track.

FT A l l o c a t e J o i nPo i n t e r ( ) ;

return FT BeginFastTrack ( ) ;

155

}

return 0 ;

}

// See Listing 5.3.5 for FT PostSlow and FT slowTakesOver.

// See Listing 5.3.5 for FT exitHandler implementation

// The slow track kills fast with SIGABRT.

s t a t i c void FT s igAbo r tFa s t ( i n t s i g ) {

a s s e r t (SIGABRT == s i g ) ;

FT fa s tC l eanup ( ) ;

e x i t ( 1 ) ;

}

// Handler for the fast track to recognize a child has aborted .

s t a t i c void FT s i gCh i l dAbo r t ed ( i n t s i g ) {

i n t p id ;

i n t f l a g s = WNOHANG | WUNTRACED | WCONTINUED;

a s s e r t (SIGCHLD == s i g ) ;

// Clean up any and all dead children .

whi le (0 < ( p i d = wa i t p i d (−1 , NULL , f l a g s ) ) ) ;

}

/// Returns 1 on success (fast track started ) .

char FT i n t e r n a lB e g i nFa s t ( i n t s e n i o r i t y [ 2 ] ) {

myStatus = FAST ;

FT ac t i v e = t r u e ;

FT accMap = FT fastAccMap ;

// We need to be able to abort if necessary

s i g n a l (SIGABRT , FT s i gAbo r tFa s t ) ;

// Keep track of what FAST’ s children do.

s i g n a l (SIGCHLD , FT s i gCh i l dAbo r t ed ) ;

156

// Seniority based ordering

c l o s e ( s e n i o r i t y [ 1 ] ) ; // We don ’t need the ’read ’ side .

c l o s e ( i n h e r i t a n c e ) ; // SLOW is responsible for old ’write ’ side .

i n h e r i t a n c e = s e n i o r i t y [ 0 ] ; // We have a new ’write ’ pipe .

FT cont i nueOrY i e l d ( ) ;

return 1 ;

}

char FT in t e rna lBeg inNorma l ( i n t s e n i o r i t y [ 2 ] ) {

myStatus = SLOW;

beques t = s e n i o r i t y [ 1 ] ;

// Stop using handlers from past fast tracks .

s i g n a l (SIGABRT , SIG DFL ) ;

// specDepth control via waiting by the floodgate

i f ( FT order > FT maxSpec ) {

i f ( FT readFloodGate ( ) == −1) {

// An error occurred earlier .

FT slowCleanup ( ) ;

e x i t ( 1 ) ;

}

}

c l o s e ( readyQ−>p i p e [ 1 ] ) ;

c l o s e ( s e n i o r i t y [ 0 ] ) ;

FT accMap = FT slowAccMap ;

c l o s e ( updateP ipe [ 1 ] ) ;

i f ( SIG ERR == s i g n a l (SIGABRT , SIG DFL ) )

p e r r o r ( ” f a i l e d to c l e a r abo r t h and l e r ” ) ;

157

SP Red i r ec tOutput ( ) ;

FT ac t i v e = t r u e ;

i f (FT AUTOMARKPOINT) FT StartAutoMarkPointTimer ( ) ;

return 0 ;

}

// See Listing 5.3.2 for FT SegvHandler implementation

// See Listing 5.3.1 for FT Begin implementation

s t a t i c i n t dua lP id ; // The other dual (not−fast/slow) track.

s t a t i c i n l i n e void FT PostDual ( void ) {

// Just kill the other and move on.

i f (−1 == k i l l ( dua lP id , SIGABRT) )

p e r r o r ( ” f a i l e d to abo r t p a r a l l e l t r a c k ” ) ;

myStatus = CTRL ;

SP CommitOutput ( ) ;

}

i n t FT BeginDualTrack ( void )

{

// Make sure we’re currently running sequentially .

i f ( myStatus != CTRL ) return 0 ;

// Don’t bother if there can ’t be parallelism

i f ( FT maxSpec < 1) return 0 ;

i n t PID= f o r k ( ) ;

i f (−1 == s e t p g i d (0 , SP gpid ) ) {

p e r r o r ( ” f a i l e d to s e t p r o c e s s group ” ) ;

abo r t ( ) ;

}

switch (PID ){

case −1:

myStatus = SEQ;

158

PID = 0 ;

break ;

case 0 :

myStatus = DUAL;

dua lP id = ge tpp i d ( ) ;

break ;

defau l t :

myStatus = DUAL;

dua lP id = PID ;

break ;

}

SP Red i r ec tOutput ( ) ;

return PID ;

}

s t a t i c i n l i n e void FT PostFast ( void ) {

SP PushDataAccordingToMap ( FT fastAccMap , updateP ipe [ 1 ] ) ;

i f (munmap( FT fastAccMap , ACC MAP SIZE) == −1)

p e r r o r ( ”unmapping a c c e s s map” ) ;

i f (munmap( FT slowAccMap , ACC MAP SIZE) == −1)

p e r r o r ( ”unmapping a c c e s s map” ) ;

c l o s e ( updateP ipe [ 0 ] ) ;

c l o s e ( updateP ipe [ 1 ] ) ;

}

void FT PostDualTrack ( void )

{

switch ( myStatus ) {

case SLOW:

FT PostSlow ( ) ;

break ;

case FAST :

159

FT PostFast ( ) ;

break ;

case DUAL:

FT PostDual ( ) ;

break ;

defau l t :

f p r i n t f ( s t d e r r , ” unexpected p r o c e s s s t a t e %d” , myStatus ) ;

abo r t ( ) ;

}

FT ac t i v e = f a l s e ;

}

A.3 Common Code

Listing A.9: Common Header File

#def ine PAGESIZE 4096 // memory page size

#def ine PAGECOUNT (UINT MAX / PAGESIZE)

// The size of any single memory bitmap in bytes .

#def ine BIT MAP SIZE ( (PAGECOUNT) >> 3)

// ALLOCMAP SIZE defines the size of the allocation (use) map

#def ine ALLOC MAP SIZE BIT MAP SIZE

#def ine MAX SPEC DEPTH 16

// The total size of the access maps .

// Dual maps the map pair for specOrder 0 i s reused for unions

#def ine ACC MAP SIZE ( (MAX SPEC DEPTH + 1 ) ∗ 2 ∗ BIT MAP SIZE )

// Write operations are type 2 , and register 13 stores the type info .

#i f d e f i n e d ( MACH )

#def ine SIG MEMORY FAULT SIGBUS

160

#def ine MAPANONYMOUS MAP ANON

#def ine WRITEOPT( cn t x t ) ( ( c n t x t )−>uc mcontext−> e s . e r r & 2)

#el se

#def ine SIG MEMORY FAULT SIGSEGV

#def ine WRITEOPT( cn t x t ) ( ( c n t x t )−>uc mcontext . g r e g s [ 1 3 ] & 2)

#end i f

typedef enum {

CTRL, MAIN,

SPEC , // a speculation process

UNDY, // the understudy

SEQ, // a sequential process

FAST , // a fast track

SLOW, // a slow track

DUAL // either of two equal options

} SP Status ;

v o l a t i l e SP Status myStatus ; // Current processes status .

i n t SP gpid ; // The process group.

#def ine PAGESTART( x ) ( ( void ∗ ) ( ( ( unsigned long ) x/PAGESIZE)∗PAGESIZE ) )

#def ine MYRESOURCE( a ) ( ( a)==0? 0 : ( ( ( a)−1)%MAX SPEC DEPTH)+1)

#def ine WRITEMAP( a ) ( accMapPtr + (MYRESOURCE( a )∗2+1)∗BIT MAP SIZE )

#def ine READMAP( a ) ( accMapPtr + MYRESOURCE( a )∗2∗BIT MAP SIZE )

Listing A.10: IO Capture Header

void SP Red i r ec tOutput ( ) ;

void SP CommitOutput ( ) ;

Listing A.11: Utility Functions Header

typedef enum {

READ,

WRITE

} AccessType ;

161

// Returns map ’ s bit for address .

i n t SP checkMap ( void ∗ page add r e s s , char∗ map ) ;

// Applies the protection prot to any memory pages that

// are marked as in use according to the useMap.

void SP s e tP r o t e c t i o n ( i n t p ro t ) ;

// Call read until it succeeds .

void SP sync r ead ( i n t fd , void ∗buf , s i z e t count ) {

whi le ( r ead ( fd , buf , count ) == −1);

}

// Call write until it succeeds .

void SP s yn c w r i t e ( i n t fd , const void ∗buf , s i z e t count ) {

whi le ( w r i t e ( fd , buf , count ) == −1);

}

Listing A.12: Utility Function Implementations

s t a t i c i n l i n e void

SP pro tec tPages ( unsigned long f i r s t , unsigned long l a s t , i n t p ro t )

{

void ∗ page = ( void ∗) ( f i r s t ∗ PAGESIZE ) ;

s i z e t l e n = ( ( l a s t − f i r s t + 1) ∗ PAGESIZE ) ;

// Try to set the protection all at once .

i f (0 == mprotect ( page , l en , p r o t ) ) return ;

p e r r o r ( ” ” ) ;

f o r ( unsigned long i = f i r s t ; i <= l a s t ; i++) {

i f ( mprotect ( ( void ∗ ) ( i ∗ PAGESIZE ) , PAGESIZE , p r o t ) )

p e r r o r ( ” ” ) ;

}

}

// Sets a bit in map to indicate that page i s accessed .

162

void SP recordAccessToMap ( void ∗ page add r e s s , char∗ map) {

i n t byte , b i t , page ;

page = ( ( unsigned long ) p ag e add r e s s ) / PAGESIZE ;

by te = page >> 3 ; //byte = page / 8

b i t = page & 7 ; //bit = page % 8;

map [ byte ] |= (1 << b i t ) ;

}

i n t SP checkMap ( void ∗ page add r e s s , char∗ map){

i n t page = ( ( unsigned long ) p ag e add r e s s ) / PAGESIZE ;

i n t byte = page >> 3 ;

i n t b i t = page % 8 ;

char mapvalue = map [ byte ] ;

return ( mapvalue >> b i t ) & 0x1 ;

}

// Writes num memory pages to pipeid starting with the i ’th page .

void SP PushPageToPipe ( unsigned long i , i n t p i p e i d , unsigned num) {

unsigned wr i t e c o u n t = 0 ;

whi le ( w r i t e c o u n t < (num ∗ PAGESIZE ) ) {

i n t r e s u l t = w r i t e ( p i p e i d ,

( void ∗ ) ( ( i ∗PAGESIZE) + w r i t e c o u n t ) ,

(num∗PAGESIZE) − wr i t e c o u n t ) ;

i f ( r e s u l t == −1) {

p e r r o r ( ” f a i l e d to w r i t e i n t o p i p e ” ) ;

abo r t ( ) ;

} e l s e {

wr i t e c o u n t += r e s u l t ;

}

}

}

i n t SP PushDataAccordingToMap ( char ∗map , i n t p i p e i d ) {

163

unsigned bchar , b i t , i ;

i n t page count =0;

f o r ( bchar=0; bchar< BIT MAP SIZE ; bchar++) {

i f (map [ bchar ]==0) continue ;

i f (˜map [ bchar ]==0) {

SP PushPageToPipe ( bchar ∗8 , p i p e i d , 8 ) ;

page count += 8 ;

continue ;

}

f o r ( b i t =0; b i t <8; b i t++) {

i f ( (map [ bchar ]>> b i t ) & 0x1 ) {

i = bchar∗8+ b i t ;

SP PushPageToPipe ( i , p i p e i d , 1 ) ;

page count++;

}

}

}

return page count ;

}

// Read a page from pipe and write it to the i ’th page of memory.

s t a t i c void

SP CopyPageFromPipe ( unsigned long i , i n t p ipe , char p r o t e c t e d ) {

unsigned r e ad coun t = 0 ;

i n t i n c r ement ;

i f ( p r o t e c t e d )

mprotect ( ( void ∗ ) ( i ∗PAGESIZE ) , PAGESIZE , PROT WRITE ) ;

whi le ( r e ad coun t < PAGESIZE) {

// read the remaining portion of the page from the pipe .

// The location to read to i s page i offset by the amount

// already read in .

i n c r ement = read ( p ipe ,

( void ∗ ) ( ( i ∗PAGESIZE)+ read coun t ) ,

PAGESIZE−r e ad coun t ) ;

164

i f (−1 == inc r ement ) {

p e r r o r ( ” e r r o r code ” ) ;

e x i t ( 0 ) ;

}

r e ad coun t += inc r ement ;

}

i f ( p r o t e c t e d )

mprotect ( ( void ∗ ) ( i ∗PAGESIZE ) , PAGESIZE , PROT NONE) ;

}

// If we are reading pages into protected space (protected==true) ,

// we’ ll need to first open the protection and then close it .

i n t SP Pul lDataAccordingToMap ( char ∗map , i n t p ipe , char p r o t e c t e d ) {

unsigned bchar , b i t , i ;

i n t page count =0;

f o r ( bchar=0; bchar < BIT MAP SIZE ; ++bchar ) {

i f (map [ bchar ]==0) continue ;

f o r ( b i t =0; b i t <8; ++b i t ) {

i f ( (map [ bchar ]>> b i t ) & 0x1 ) {

i = bchar∗8+ b i t ;

SP CopyPageFromPipe ( i , p ipe , p r o t e c t e d ) ;

++page count ;

} // if map[ bint ] . . .

} //for bit

} //for map

return page count ;

}

/// Returns 0 on success .

char SP se tupAct i on ( void (∗ hand l e r ) ( int , s i g i n f o t ∗ , u c o n t e x t t ∗ ) ,

i n t s i g n a l )

{

s t ruc t s i g a c t i o n a c t i o n ;

165

s i g f i l l s e t (& a c t i o n . sa mask ) ;

a c t i o n . s a f l a g s = SA SIGINFO ;

a c t i o n . s a s i g a c t i o n = ( void ∗) h and l e r ;

i f (−1 == s i g a c t i o n ( s i g n a l , &ac t i on , NULL) ) {

p e r r o r ( ” f a i l e d to s e t ’ f a u l t ’ h and l e r ” ) ;

return 1 ;

}

return 0 ;

}

166

Bibliography

[1] Allen, Randy and Ken Kennedy. 2001. Optimizing Compilers for Modern

Architectures: A Dependence-based Approach. Morgan Kaufmann Publishers.

[2] Amdahl, Gene M. 1967. Validity of the single processor approach to achieving

large scale computing capabilities. In AFIPS ’67 (Spring): Proceedings of the

April 18-20, 1967, spring joint computer conference, pages 483–485. ACM,

New York, NY, USA.

[3] Bender, Michael A., Jeremy T. Fineman, Seth Gilbert, and Charles E. Leiser-

son. 2004. On-the-fly maintenance of series-parallel relationships in fork-join

multithreaded programs. In Proceedings of the ACM Symposium on Paral-

lelism in Algorithms and Architectures, pages 133–144.

[4] Berger, Emery D., Ting Yang, Tongping Liu, and Gene Novark. 2009. Grace:

safe multithreaded programming for C/C++. In Proceedings of the ACM

SIGPLAN Conference on Object oriented programming systems and applica-

tions, pages 81–96. ACM, New York, NY, USA.

[5] Bernstein, A. J. 1966. Analysis of programs for parallel processing. IEEE

Transactions on Electronic Computers, 15(5):757–763.

[6] Blumofe, Robert D., Christopher F. Joerg, Bradley C. Kuszmaul, Charles E.

Leiserson, Keith H. Randall, and Yuli Zhou. 1995. Cilk: an efficient multi-

threaded runtime system. SIGPLAN Not., 30(8):207–216.

167

[7] Boehm, Hans-Juergen. 2005. Threads cannot be implemented as a library.

In Proceedings of the ACM SIGPLAN Conference on Programming language

design and implementation, pages 261–268.

[8] Bridges, Matthew, Neil Vachharajani, Yun Zhang, Thomas Jablin, and David

August. 2007. Revisiting the sequential programming model for multi-core.

In Proceedings of the International Symposium on Microarchitecture, pages

69–84. IEEE Computer Society, Washington, DC, USA.

[9] Chang, Fay W. and Garth A. Gibson. 1999. Automatic i/o hint generation

through speculative execution. In Proceedings of the Symposium on Operating

System Design and Implementation.

[10] Chen, Michael K. and Kunle Olukotun. 2003. The Jrpm system for dy-

namically parallelizing Java programs. In 30th International Symposium on

Computer Architecture, pages 434–445.

[11] Cintra, Marcelo and Diego Llanos. 2005. Design space exploration of a soft-

ware speculative parallelization scheme. IEEE Transactions on Parallel and

Distributed Systems, 16(6):562–576.

[12] Coffman, Edward G., M. J. Elphick, and Arie Shoshani. 1971. System dead-

locks. ACM Computing Surveys, 3(2):67–78.

[13] Cytron, Ron, Jeanne Ferrante, Barry Rosen, Mark Wegman, and F. Kenneth

Zadeck. 1991. Efficiently computing static single assignment form and the

control dependence graph. ACM Transactions on Programming Languages

and Systems, 13(4):451–490.

[14] Dang, Francis, Hao J. Yu, and Lawrence Rauchwerger. 2002. The R-LRPD

test: Speculative parallelization of partially parallel loops. In IEEE Interna-

tional Parallel and Distributed Processing Symposium on, pages 20–29. Ft.

Lauderdale, FL.

168

[15] Ding, Yonghua and Zhiyuan Li. 2004. A compiler scheme for reusing inter-

mediate computation results. In Proceedings of the International Symposium

on Code Generation and Optimization.

[16] Eigler, Frank Ch. 2003. Mudflap: Pointer use checking for C/C++. In GCC

Developers’ Summit, pages 57–69.

[17] Feng, Mingdong and Charles E. Leiserson. 1997. Efficient detection of de-

terminacy races in cilk programs. In Proceedings of the ACM Symposium on

Parallelism in Algorithms and Architectures, pages 1–11. ACM, New York,

NY, USA.

[18] Frigo, Matteo, Charles E. Leiserson, and Keith H. Randall. 1998. The imple-

mentation of the cilk-5 multithreaded language. In Proceedings of the ACM

SIGPLAN Conference on Programming language design and implementation,

pages 212–223. ACM, New York, NY, USA.

[19] Garg, Alok and Michael C. Huang. 2008. A performance-correctness

explicitly-decoupled architecture. In 41st International Symposium on Mi-

croarhictecutre.

[20] Grant, Brian K., M. Philipose, Marcus U. Mock, Craig D. Chambers, and

S. J. Eggers. 1999. An evaluation of staged run-time optimizations in DyC.

In Proceedings of the ACM SIGPLAN Conference on Programming language

design and implementation. Atlanta, Georgia.

[21] Grzegorczyk, Chris, Sunil Soman, Chandra Krintz, and Rich Wolski. 2007.

Isla vista heap sizing: Using feedback to avoid paging. In Proceedings of

the International Symposium on Code Generation and Optimization, pages

325–340. IEEE Computer Society, Washington, DC, USA.

169

[22] Gupta, Manish and Rahul Nim. 1998. Techniques for speculative run-time

parallelization of loops. In Proceedings of the ACM/IEEE conference on

Supercomputing, pages 1–12.

[23] Gustafson, John L. 1988. Reevaluating amdahl’s law. Commun. ACM,

31(5):532–533.

[24] Halstead, Robert H., Jr. 1985. MULTILISP: A language for concurrent sym-

bolic computation. ACM Transactions on Programming Langguage Systems,

7(4):501–538.

[25] Herlihy, Maurice, Victor Luchangco, Mark Moir, and William N. Scherer III.

2003. Software transactional memory for dynamic-sized data structures. In

Proceedings of the ACM Symposium on Principles of Distributed Computing,

pages 92–101. Boston, MA.

[26] Herlihy, Maurice and J. E. Moss. 1993. Transactional memory: Architectural

support for lock-free data structures. In Proceedings of the International

Symposium on Computer Architecture. San Diego, CA.

[27] Jefferson, David R., Brian R. Beckman, Frederick Wieland, L. Blume, and

M. Diloreto. 1987. Time warp operating system. In SOSP ’87: Proceedings

of the ACM Symposium on operating systems principles, pages 77–93. ACM,

New York, NY, USA.

[28] Kejariwal, Arun, Xinmin Tian, Wei Li, Milind Girkar, Sergey Kozhukhov,

Hideki Saito, Utpal Banerjee, Alexandru Nicolau, Alexander V. Veidenbaum,

and Constantine D. Polychronopoulos. 2006. On the performance potential

of different types of speculative thread-level parallelism. In ICS ’06: Proceed-

ings of the 20th annual international conference on Supercomputing, page 24.

ACM, New York, NY, USA.

170

[29] Keleher, Peter J., Allen L. Cox, Sandhya Dwarkadas, and Willy Zwaenepoel.

1994. TreadMarks: Distributed shared memory on standard workstations and

operating systems. In Proceedings of the 1994 Winter USENIX Conference.

[30] Kennedy, Andrew and Claudio V. Russo. 2005. Generalized algebraic data

types and object-oriented programming. In Proceedings of the ACM SIG-

PLAN Conference on Object oriented programming systems and applications,

pages 21–40.

[31] Lee, Sanghoon and James Tuck. 2008. Parallelizing Mudflap using thread-

level speculation on a CMP. Presented at the Workshop on the Parallel Ex-

ecution of Sequential Programs on Multi-core Architecture, co-located with

ISCA.

[32] Li, Kai. 1986. Shared Virtual Memory on Loosely Coupled Multiprocessors.

Ph.D. thesis, Dept. of Computer Science, Yale University, New Haven, CT.

[33] Liao, Shih-Wei, Perry H. Wang, Hong Wang, John Paul Shen, Gerolf

Hoflehner, and Daniel M. Lavery. 2002. Post-pass binary adaptation for

software-based speculative precomputation. In Proceedings of the ACM SIG-

PLAN Conference on Programming language design and implementation,

pages 117–128.

[34] Liblit, Ben, Mayur Naik, Alice X. Zheng, Alex Aiken, and Michael I. Jordan.

2005. Scalable statistical bug isolation. In Proceedings of the ACM SIGPLAN

Conference on Programming language design and implementation, pages 15–

26. ACM Press, New York, NY, USA.

[35] Liu, Wei, James Tuck, Luis Ceze, Wonsun Ahn, Karin Strauss, Jose Renau,

and Josep Torrellas. 2006. Posh: A TLS compiler that exploits program

structure. In Proceedings of the ACM SIGPLAN Symposium on Principles

and Practice of Parallel Programming.

171

[36] Luk, Chi-Keung, Robert Cohn, Robert Muth, Harish Patil, Artur Klauser,

Geoff Lowney, Steven Wallace, Vijay Janapa Reddi, and Kim Hazelwood.

2005. Pin: building customized program analysis tools with dynamic instru-

mentation. In Proceedings of the ACM SIGPLAN Conference on Program-

ming language design and implementation, pages 190–200. ACM, New York,

NY, USA.

[37] Martin, Milo M. K., Daniel J. Sorin, Harold W. Cain, Mark D. Hill, and

Mikko H. Lipasti. 2001. Correctly implementing value prediction in micro-

processors that support multithreading or multiprocessing. In Proceedings of

the International Symposium on Microarchitecture.

[38] Mellor-Crummey, John. 1993. Compile-time support for efficient data race

detection in shared-memory parallel programs. In PADD ’93: Proceedings of

the 1993 ACM/ONR workshop on Parallel and distributed debugging, pages

129–139. ACM Press, New York, NY, USA.

[39] Michie, Donald. 1968. Memo functions and machine learning. Nature, 218:19–

22.

[40] Moore, Gordon E. 1965. Cramming more components onto integrated cir-

cuits, Electronics. Electronics Magazine, 19:114–117.

[41] Moseley, Tipp, Alex Shye, Vijay Janapa Reddi, Dirk Grunwald, and Ramesh

Peri. 2007. Shadow profiling: Hiding instrumentation costs with parallelism.

In Proceedings of the International Symposium on Code Generation and Op-

timization, pages 198–208.

[42] Navabi, Armand, Xiangyu Zhang, and Suresh Jagannathan. 2008. Quasi-

static scheduling for safe futures. In Proceedings of the ACM SIGPLAN

Symposium on Principles and Practice of Parallel Programming.

172

[43] Neelakantam, Naveen, Ravi Rajwar, Suresh Srinivas, Uma Srinivasan, and

Craig Zilles. 2007. Hardware atomicity for reliable software speculation. In

Proceedings of the International Symposium on Computer Architecture.

[44] Nightingale, Edmund B., Peter M. Chen, and Jason Flinn. 2005. Speculative

execution in a distributed file system. In Proceedings of the twentieth ACM

symposium on Operating systems principles, pages 191–205. ACM, New York,

NY, USA.

[45] Nightingale, Edmund B., Daniel Peek, Peter M. Chen, and Jason Flinn.

2008. Parallelizing security checks on commodity hardware. In Proceedings

of the International Conference on Architectural Support for Programming

Languages and Operating Systems, pages 308–318.

[46] Oplinger, Jeffrey T. and Monica S. Lam. 2002. Enhancing software reliability

with speculative threads. In Proceedings of the International Conference on

Architectural Support for Programming Languages and Operating Systems,

pages 184–196.

[47] Ottoni, Guilherme, Ram Rangan, Adam Stoler, and David I. August. 2005.

Automatic thread extraction with decoupled software pipelining. In Proceed-

ings of the International Symposium on Microarchitecture, pages 105–118.

[48] Patil, Harish and Charles Fischer. 1995. Efficient run-time monitoring using

shadow processing. In Mireille Ducasse, editor, International Workshop on

Automated and Algorithmic Debugging, pages 119–132.

[49] Perkovic, Dejan and Peter J. Keleher. 2000. A protocol-centric approach

to on-the-fly race detection. IEEE Transactions on Parallel and Distributed

Systems, 11(10):1058–1072.

[50] Quinones, Carlos Garcıa, Carlos Madriles, Jesus Sanchez, Pedro Marcuello,

Antonio Gonzalez, and Dean M. Tullsen. 2005. Mitosis compiler: An in-

173

frastructure for speculative threading based on pre-computation slices. In

Proceedings of the ACM SIGPLAN Conference on Programming language

design and implementation.

[51] Raman, Arun, Hanjun Kim, Thomas R. Mason, Thomas B. Jablin, and

David I. August. 2010. Speculative parallelization using software multi-

threaded transactions. In Proceedings of the International Conference on

Architectural Support for Programming Languages and Operating Systems,

volume 38, pages 65–76. ACM.

[52] Rauchwerger, Lawrence and David Padua. 1995. The LRPD test: Speculative

run-time parallelization of loops with privatization and reduction paralleliza-

tion. In Proceedings of the ACM SIGPLAN Conference on Programming

language design and implementation. La Jolla, CA.

[53] Shen, Xipeng and Chen Ding. 2005. Parallelization of utility programs based

on behavior phase analysis. In Proceedings of the International Workshop

on Languages and Compilers for Parallel Computing. Hawthorne, NY. Short

paper.

[54] Sohi, Gurindar S., Scott E. Breach, and T. N. Vijaykumar. 1995. Multiscalar

processors. In Proceedings of the International Symposium on Computer Ar-

chitecture.

[55] SPEC. 2010. Standard performance evaluation corporation (SPEC).

http://www.spec.org/.

[56] Steffan, J. Gregory, Christopher B. Colohan, Antonia Zhai, and Todd C.

Mowry. 2005. The STAMPede approach to thread-level speculation. ACM

Transactions on Computer Systems, 23(3):253–300.

174

[57] Sundaramoorthy, Karthik, Zach Purser, and Eric Rotenberg. 2000. Slip-

stream processors: improving both performance and fault tolerance. SIG-

PLAN Not., 35(11):257–268.

[58] Tiwari, Devesh, Sanghoon Lee, James Tuck, and Yan Solihin. 2010. Mmt:

Exploiting fine-grained parallelism in dynamic memory management. IEEE

Transactions on Parallel and Distributed Systems.

[59] Tsai, Jenn-Yuan, Zhenzhen Jiang, and Pen-Chung Yew. 1999. Compiler tech-

niques for the superthreaded architectures. International Journal of Parallel

Programming, 27(1):1–19.

[60] Vachharajani, Neil, Ram Rangan, Easwaran Raman, Matthew J. Bridges,

Guilherme Ottoni, and David I. August. 2007. Speculative decoupled soft-

ware pipelining. In Proceedings of the International Conference on Parallel

Architectures and Compilation Techniques, pages 49–59. IEEE Computer So-

ciety, Washington, DC, USA.

[61] Wahbe, Robert, Steven Lucco, and Susan L. Graham. 1993. Practical data

breakpoints: design and implementation. In Proceedings of the ACM SIG-

PLAN Conference on Programming language design and implementation.

[62] Wallace, Steven and Kim Hazelwood. 2007. Superpin: Parallelizing dynamic

instrumentation for real-time performance. In Proceedings of the Interna-

tional Symposium on Code Generation and Optimization, pages 209–220.

[63] Welc, Adam, Suresh Jagannathan, and Antony L. Hosking. 2005. Safe futures

for Java. In Proceedings of the ACM SIGPLAN Conference on Object oriented

programming systems and applications, pages 439–453.

[64] Zhai, Antonia, Christopher B. Colohan, J. Gregory Steffan, and Todd C.

Mowry. 2002. Compiler optimization of scalar value communication between

175

speculative threads. In Proceedings of the International Conference on Archi-

tectural Support for Programming Languages and Operating Systems, pages

171–183.

[65] Zhang, Chengliang, Kirk Kelsey, Xipeng Shen, Chen Ding, Matthew Hertz,

and Mitsunori Ogihara. 2006. Program-level adaptive memory management.

In Proceedings of the International Symposium on Memory Management. Ot-

tawa, Canada.

[66] Zhou, Pin, Feng Qin, Wei Liu, Yuanyuan Zhou, and Josep Torrellas. 2004.

iWatcher: Efficient architectural support for software debugging. In Pro-

ceedings of the International Symposium on Computer Architecture, pages

224–237.

[67] Zilles, Craig and Gurindar S. Sohi. 2002. Master/slave speculative paralleliza-

tion. In Proceedings of the International Symposium on Microarchitecture,

pages 85–96. IEEE Computer Society Press, Los Alamitos, CA, USA.