• No results found

Chapter 7 Specifying and Checking Effects for Framework APIs

7.2 Safe, Reusable Parallel Frameworks

7.2.4 Writing the Framework Implementation

Having studied the framework API, we now focus on the problem of writing a correct framework imple- mentation. The framework writer must ensure three properties: type preservation, effect preservation, and noninterference of effect. The key point is that the API design discussed in the previous sections provides all the information needed to reason soundly about these three properties, even in the presence of unknown user-supplied methods. Further, the framework author can write the framework in DPJ, thereby using the DPJ type and effect system to check some or all of these properties. However, so long as the properties hold for all user-visible types and effects, the framework author is free to use internal operations, such as swapping references with disjoint regions, that the effect system cannot prove correct.

Type preservation: The soundness results presented in Chapter 8 show that type preservation holds for

DPJ as extended in this chapter. Therefore, if the framework is written in DPJ, then this property will be checked “for free,” unless the framework does an assignment (using a cast) that violates the typing rules. The DPJ subtyping rules are quite flexible, so we anticipate that unsound assignments will rarely be needed in practice to work around expressivity constraints of DPJ.

A more likely case is that casts are used to interface with code that is not implemented in DPJ, such as an off-the-shelf Java container implementation. In this case, the framework author must reason about type preservation using the specification of the non-DPJ code. For example, pre-Java 5 code implementing a container might represent the container slots as references toObject. If references to be stored into the slots always have typeT<R>, then it would be sound to cast these references toObjectwhen putting them in the container, and back toT<R>when taking them out. For Java code written with generics, such casts should be rare.

Effect preservation: Effect preservation means that the static effect of every statement is a supereffect of

the actual runtime effect of every execution of that statement. Again, the extended language guarantees this property, so long as (1) type preservation holds; and (2) every method summary covers the effects of the method body. In DPJ, one can always write a correct method summary (in the extreme casewrites *is always correct). So property (2) will hold if property (1) does. Here, if the framework calls into non-DPJ

code, then the framework writer will have to reason about effects manually (i.e., the reasoning cannot be checked by DPJ).

Noninterference of effect: Noninterference of effect means that parallel tasks have no conflicting memory

accesses. While DPJ can establish noninterference in many cases, in some cases it may not be able to. For example, even if a pair of references of typeC<*>always points to objects with distinct regions at runtime, the type system can’t prove that, as discussed in Section 7.1.

1 public class DisjointArray<type T<region Elt>, region Cont | 2 Elt:* # Cont> implements DisjointContainer<T,Cont> {

3

4 /* Internal array representation */

5 private DPJArrayList<T<Elt:*>,Cont> elts in Cont; 6

7 /* Implementation of performOnAll */

8 public <effect E#>void performOnAll(Operation<T,effect E> op) 9 reads Cont writes Elt:* effect E {

10 foreach (int i in 0, elts.size()) {

11 op.operateOn(elts.get(i));

12 }

13 }

14

15 /* Swap elements at idx1 and idx2 */

16 public void swap(int idx1, int idx2) writes Cont { 17 T<Elt:*> tmp = elts.get(idx1); 18 elts.add(idx1, elts.get(idx2)); 19 elts.add(idx2, tmp); 20 } 21 22 }

Figure 7.7: Array implementation of a disjoint container (partial). DPJArrayList (line 5) is an or- dinary Java ArrayList, annotated with region information. The effect of elts.get(i)(line 11) is reads Cont.

In such cases, the framework author has the freedom to “go outside” the type system, and use a different technique to make the noninterference argument. Figure 7.7 shows an example. This an array implementa- tion ofDisjointContainer. We have chosen to represent the array internally as aDPJArrayList, as shown in line 5. The type argument toDPJArrayListisElt:*, reflecting the fact that the dynamic type of elementj isElt:[j], as discussed in Section 7.2.1. TheperformOnAllmethod uses the DPJ foreachconstruct (line 10) to iterate in parallel over the slots of theDPJArrayListand apply the user- supplied operation to each of its elements. We also add aswapmethod, similar to the method discussed in Section 7.1, for swapping two elements of the array.

To show noninterference, it suffices to establish two things: (1) for distinct values i, the region in the dynamic type of elts.get(i)at line 11 is distinct; and (2) i attains distinct values i on distinct iterations of the foreachin line 10. The first statement follows from the inductive argument we made in Section 7.2.1 about maintaining disjointness: to change the shape of the array, we either have to use an inherited creation method, which preserves disjointness as discussed in Section 7.2.2, or do a swap, which also preserves disjointness, as can be seen from the implementation in lines 17–19. The second statement follows from the semantics offoreachin DPJ (Chapter 3). More generally, one would follow the same two-pronged strategy to show noninterference for an a traversal over an arbitrary disjoint container: first show disjointness of slot regionsRi, and then argue that the traversal operates in parallel on the slots.

Notice that once the framework implementer checks noninterference in this way, the user never has to see or even know about how the checking occurred. From the user’s point of view, if the program type checks, then the noninterference property holds. Further, the framework writer is free to use static or dynamic verification techniques such as program logic, model checking, or testing to check the framework implementation. We can thus think of the techniques presented here as making DPJ into an extensible language. By writing a suitable API, and doing appropriate checks, the framework writer can add new capabilities for parallel operations that provide the same guarantees as if those capabilities had been built in as first-class parts of the language. A good example of this extensibility is the pipeline framework described in Section 7.3, which supports a parallel control structure that cannot be expressed in the DPJ language at all. This extensibility makes DPJ much more powerful than if the only checking mechanism were the type system itself.

7.3

Evaluation

We have evaluated the techniques discussed above with two goals in mind:

1. Can we use the techniques to write realistic frameworks and user programs? Do any additional issues arise in real frameworks or user code?

2. What is the user experience of using such an API? How burdensome is it to write the type and effect annotations, and how difficult is it to get the annotations correct?

To perform our evaluation, we first extended the DPJ compiler to support effect variables, effect constraints, and type region parameters as discussed in Section 7.2.3 and Chapter 8. Then we studied how to (1) use our techniques to write generic array, tree, and pipeline frameworks; and (2) use the frameworks to write three parallel codes: a Monte Carlo simulation algorithm, a Barnes-Hut n-body computation using a spatial octtree, and RadixSort expressed as a pipeline. We chose these three algorithms because they exemplify different styles of parallelism: Monte Carlo uses direct loop-style parallelism over arrays; Barnes-Hut uses recursive, divide-and-conquer parallelism over trees; and RadixSort uses concurrent pipelined computations over a stream of inputs.