A stratified view of programming language parallelism for undergraduate CS education

Panel presentation with Dick Brown, Joel C. Adams, Jens Mache, and Libby Shoop. Presented at 43rd ACM Technical Symposium on Computer Science Education (SIGCSE), 2012


Our goal was to show different levels at which parallelism could be approached. My portion was languages that incorporate parallelism. We deviated somewhat from the description because the referees encouraged us to focus more rather than giving quite as much of an overview as we'd originally planned.

The panel used numerical integration to calculate Pi as a running example. My first code example was a Chapel program to do this. Here is the complete program, which nicely illustrates a reduction (parallelized automatically):

const numRect = 10000000;
const D : domain(1) = [1..numRect];
const width = 2.0 / numRect;  //rectangle width
const baseX = -1 - width/2;   //baseX+width is midpoint of 1st rectangle

proc rectangleArea(i : int) {  //computes area of rectangle i
  const x = baseX + i*width;
  return width * sqrt(1.0 - x*x);
}

const halfPI = + reduce rectangleArea(D);
writeln(2.0*halfPI);

Here is an even more concise Chapel implementation that removes the need to explicitly declare the function (courtesy of Brad Chamberlain):

const numRect = 10000000;
const D : domain(1) = [1..numRect];
const width = 2.0 / numRect;  //rectangle width
const baseX = -1 - width/2;   //baseX+width is midpoint of 1st rectangle

const halfPI = + reduce [i in D] (width * sqrt(1.0 - (baseX + i*width)**2));
writeln(2.0*halfPI);

Here is the Habanero Java program to do this task, written using a recursive style (creates a large number of tasks for load balancing):

public class Integral {

    public static final int numRect  = 100000000;                // number of rectangles
    public static final double startingX = -1.0;
    public static final double overallWidth = 2.0;
    public static final double width = overallWidth / numRect; // width of each rectangle
    public static double halfPI = 0;

    public static int threshold = 1000000;

    public static void computeIntegral(double from, int num) {
	if(num < threshold) {
	    double x = from + width/2;
	    double accum = 0;
	    
	    for(int i=0; i < num; i++) {
		accum += width * Math.sqrt(1.0 - x*x);
		x += width;                
	    }

	    isolated { halfPI += accum; }
	} else {
	    int halfNum = num/2;
	    async computeIntegral(from, halfNum);
	    computeIntegral(from+halfNum*width, num-halfNum);
	}
    }
    
    public static void main(String[] args) {
	finish {
	    computeIntegral(startingX, numRect);
	}

	System.out.println("Pi = " + 2.0*halfPI);
    }
}

This is much more verbose; it's not as nice a problem to illustrate HJ. (At least I don't know how to write it more nicely.) A slightly more natural way to write this would be to use futures to get return values. (I didn't because I didn't include them in my introduction and it adds a bit more syntax.) What I do like about it, though, is that it shows async and finish being used in a style very different than fork-join, which is kind of the obvious way to use them. For the presentation, this was too much code, so I ended up showing a similarly-structured fibonacci program:

public class Fib {
    private static int accum;

    public static void fib(int n) {
	if(n <= 1)
	    isolated { accum += n; }
	else {
	    async fib(n-1);
	    fib(n-2);
	}
    }

    public static void main(String[] args) {
	accum = 0;
	finish { fib(6); }
	System.out.println(accum);
    }
}