Lambdas and Streams

Java Parallel Programming Part 3: Lambdas and Streams

In my last article, I showed you how to use the Java 7’s Fork/Join framework to write a parallel program. If you examine the code carefully, you will notice that it looks quite different from normal serial Java code. Developers having to write code differently for parallel programs imposes a serious barrier to its wide adoption as a framework for writing parallel programs. What developers want is that they can write code that can be executed serially as well as in parallel. Enter lambdas and streams. The Java community has observed the trend (Moore’s Law, hitting the frequency wall ie, power dissipation issues as described in my previous article) and recognised that chip designers have nowhere to go but parallel. Consequently, software has to be written such that it can take advantage of the parallel hardware. And the OpenJDK Project Lambda was  started in Dec 2009 with the aims to support programming in a multicore environment by adding closures and related features to the Java SE platform. The objectives are realised in Java SE 8 as JSR 335: Lambda Expressions for the Java Programming Language. In this article, I am going to show you how to use lambdas and streams to implement a Mandelbrot generation parallel program and compare its performance with the Fork/Join framework we examined last time.

Mandelbrot Generation using Lambdas and Streams

The obvious place to start parallelism is in collections. By using lambdas and streams together, a program can be developed that can be run serially as well as in parallel to take advantage of multicore hardware. I can’t emphasise enough that this is not a tutorial on lambdas and streams, for that you can read this tutorial. This article assumes you are already familiar or at lease have a basic understanding of lambdas and streams. I am just showing you how they can be used to write parallel programs.

The code can be distilled into the one statement:


IntStream.range(0, WIDTH)
   .forEach(i -> result[i] = computeNow(xc, yc, size, colours, WIDTH, HEIGHT, ITERS, i));

You will notice that I’ve replaced the usual for loop with an integer stream. The forEach method performs an action for each element of the stream and the action is specified by a lambda function. The idea for the implementation of Mandelbrot generation is that I pass around a lambda for the calculation of a single vertical stripe of the Mandelbrot image. And the calculation is carried out for each vertical stripe in the image specified by range.(0, WIDTH). Using a stream and lambda function gives Java the chance to parallelise the execution. However, when unspecified, the above is always executed serially. To run in parallel, one has to explicitly state so using the parallel() method as follows:


IntStream.range(0, WIDTH)
   .parallel()
   .forEach(i -> result[i] = computeNow(xc, yc, size, colours, WIDTH, HEIGHT, ITERS, i));

The means that the serial and the parallel program are one and the same except for the inclusion of the .parallel() call. Contrast this to the way that the Mandelbrot generation is written using Fork/Join in the last article. Since there is no overlap among the stripes to be calculated, a result array can be used safely without any synchronisation. If that is not the case in your problem to solve, you will have to devise some form of synchronisation eg, putting the result in a collection using a syncrhonisation wrapper.

Here is the computeNow() method:


	// compute Mandelbrot
	public static int[] computeNow(double pointX, double pointY, double stepSize, Color[] colours, int width, int height, int iterations,
    		int column) {
		int[] buffer = new int[height];
        for (int j = 0; j < height; j++) {
        	
            double x = pointX - stepSize / 2 + stepSize * column / width;
            double y = pointY - stepSize / 2 + stepSize * j / height;
            Complex z0 = new Complex(x, y);
            // colour is mapped to number of iterations
            buffer[j] = MandelbrotHelper.mandelbrotPoint(z0, iterations);
        }
        return buffer;

	}

And the mandelbrotPoint() method:


    // return number of iterations to check if c = a + ib is in Mandelbrot set
    public static int mandelbrotPoint(Complex z0, int maxIterations) {
        Complex z = z0;
        for (int t = 0; t < maxIterations - 1; t++) {             if (z.abs() > 2.0) return t;
            z = z.times(z).plus(z0);
        }
        return maxIterations - 1;
    }

And the main() is shown below:


	public static void main(String[] args) throws NumberFormatException, IOException {
        final int WIDTH = MandelbrotHelper.DEFAULT_WIDTH;
        final int HEIGHT = MandelbrotHelper.DEFAULT_HEIGHT;
   
    	// read in mandelbrot parameters and colour map
        double xc   = Double.parseDouble(args[0]);
        double yc   = Double.parseDouble(args[1]);
        double size = Double.parseDouble(args[2]);
        
        // read in colour map
        Color[] colours = MandelbrotHelper.readColourMap(args[3]);
        final int ITERS = colours.length;
        System.out.println("Colour array size: " + ITERS);
        
        // print out environment and setting
        System.out.println("Starting point: (" + xc + ", " + yc + "); step size: " + size);
        System.out.println("Colour map \"" + args[3] + "\" read in successfully");


        int processors = Runtime.getRuntime().availableProcessors();
        System.out.println(Integer.toString(processors) + " processor"
                + (processors != 1 ? "s are " : " is ")
                + "available");

        int[][] result = new int[WIDTH][];
        
        // generate Mandelbrot - no need splitting work into more chunks than number of processors
        long startTime = System.currentTimeMillis();
        
        if (args.length < 5) {         	// execute serially             IntStream.range(0, WIDTH)         	//.peek(i -> System.out.println(i))
        	.forEach(i -> result[i] = computeNow(xc, yc, size, colours, WIDTH, HEIGHT, ITERS, i));
        }
        else {
        	// execute in parallel
	        IntStream.range(0, WIDTH)
	        	.parallel()
	        	//.peek(i -> System.out.println(i))
	        	.forEach(i -> result[i] = computeNow(xc, yc, size, colours, WIDTH, HEIGHT, ITERS, i));
        }
        System.out.println("Generation of Mandelbrot took " + (System.currentTimeMillis() - startTime) + 
                " milliseconds.");
        
        // create mandelbrot display
        MandelbrotHelper.display(WIDTH, HEIGHT, colours, result);

	}

 

Performance

If you look at the code in the previous section carefully, you will notice that I have not specified how many threads or multucore processors to use. It is all up to the Java implementation to break it up and execute the action of each element in the stream in parallel. In my Fork/Join performance test, I have not tried out the case in which I divide the Mandelbrot generation into more chunks than the number of cores on the machine. In this comparison, I created such a case, tested it and the results or as follows:

Join/Fork

Lambda and Stream

Serial Execution

28.22

27.72

Parallel Execution

7.97

7.91

 

Conclusion

Algorithms and programs written using lambdas and streams are potentially capable of being executed either serially or in parallel. Contrast this to the Fork/Join implementation of the Mandelbrot generation in my previous article. The performance in using the two different approaches is virtually identical. Which approach you are going to use will depend on both the use case and your personal preference. In the next post, I shall move on to Java Enterprise Edition 7 (JEE7) and examine how one can implement the Mandelbrot generation using its new Concurrency Utilities on the Wildfly JEE Application Server.