Showing posts with label Java 7. Show all posts
Showing posts with label Java 7. Show all posts

Monday, 16 December 2013

Fork / Join Framework : RecursiveTask Example

In my previous post, I had demonstrated the use of RecursiveAction class for the Fork / Join framework. Continuing that here in this post, an example for the RecursiveTask class has been explained. 

Two methods inherited from ForkJoinTask have been used:

1. fork() - It allows a ForkJoinTask to be planned for asynchronous execution. This allows a new ForkJoinTask to be launched from an existing one.

2. join() - It returns the result of the computation when it is done

The task here is to find the sum of all the elements in an array. Let's have a look at this: 


package com.fork.join.task;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;

public class ForkJoinSumTask {

 Random random = new Random();

 public void fillArray(int[] array) {
  for (int i = 0; i < array.length; i++) {
   array[i] = array[i] = random.nextInt(10000);
  }
 }

 public static void main(String[] args) {
  ForkJoinSumTask sum = new ForkJoinSumTask();
  int[] array = new int[20_00_00_000];
  sum.fillArray(array);

  long count;
  long start1;

  // Sequential process to get the sum of the elements in array
  for (int j = 0; j < 20; j++) {
   count = 0;
   start1 = System.currentTimeMillis();
   for (long i = 0; i < (long) array.length; i++) {
    count = (count + array[(int) i]);
   }

   System.out.println("Addition Result: " + count);
   System.out.println("Sequential processing time: "
     + (System.currentTimeMillis() - start1) + " ms");

  }
  System.out.println("Parallel processing time");
  System.out.println("Number of processors available: "
    + Runtime.getRuntime().availableProcessors());

  ForkJoinPool fjpool = new ForkJoinPool(Runtime.getRuntime().availableProcessors());
  // Default parallelism level
  // =
  // Runtime.getRuntime().availableProcessors()
  long start2;

  for (int i = 0; i < 20; i++) {
   RecursiveSumTask task = new RecursiveSumTask(array, 0, array.length);
   start2 = System.currentTimeMillis();
   System.out.println("Addition Result: " + fjpool.invoke(task));
   System.out.println("Parallel processing time: "
     + (System.currentTimeMillis() - start2) + " ms");
  }

  System.out
    .println("Number of steals: " + fjpool.getStealCount() + "\n");
 }
}

class RecursiveSumTask extends RecursiveTask {
 private static final long serialVersionUID = 1L;
 final int low;
 final int high;
 private int[] array;
 final int splitSize = 1000_00_000; // Some threshold size to spit the task

 RecursiveSumTask(int[] array, int from, int to) {
  this.low = from;
  this.high = to;
  this.array = array;
 }

 @Override
 protected Long compute() {
  long count = 0L;
  List> forks = new ArrayList<>();

  if (high - low > splitSize) {
   // task is huge so divide in half
   int mid = (low + high) / 2;

   // Divided the given task into task1 and task2
   RecursiveSumTask task1 = new RecursiveSumTask(array, low, mid);
   forks.add(task1);
   task1.fork();

   RecursiveSumTask task2 = new RecursiveSumTask(array, mid, high);
   forks.add(task2);
   task2.fork();

  } else {
   // Calculating sum of the given array range
   for (int i = (int) low; i < high; i++) {
    count = count + array[i];
   }
  }

  // Waiting for the result
  for (RecursiveTask task : forks) {
   count = count + task.join();
  }

  return count;
 }
}

Note that RecursiveSumTask extends ForkJoinTask in this case. The compute() has a return type unlike ForkJoinAction whose compute() did not have a return type.

During sequential processing, the whole array is scanned sequentially to perform addition of the elements present in it. 

For parallel execution, again a ForkJoinPool is created. Runtime.getRuntime().availableProcessors() will return the number of available processors available with the system. fork() is performed on each recursive task and result of each such task is added using the join() function join() returns the computation result for the task. 

Some threshold value (array size in this case) is used to decide whether the computation is to be performed directly or is to be divided into sub tasks (ForkJoinTasks).


Observations: 


Addition Result: 999844939360

Sequential processing time: 122 ms
Addition Result: 999844939360
Sequential processing time: 126 ms
Addition Result: 999844939360
Sequential processing time: 120 ms
Addition Result: 999844939360
Sequential processing time: 120 ms
Addition Result: 999844939360
Sequential processing time: 120 ms
Addition Result: 999844939360
Sequential processing time: 121 ms
Addition Result: 999844939360
Sequential processing time: 120 ms
Addition Result: 999844939360
Sequential processing time: 119 ms
Addition Result: 999844939360
Sequential processing time: 120 ms
Addition Result: 999844939360
Sequential processing time: 121 ms

Parallel processing 
Number of processors available: 4

Addition Result: 999844939360
Parallel processing time: 42 ms
Addition Result: 999844939360
Parallel processing time: 49 ms
Addition Result: 999844939360
Parallel processing time: 40 ms
Addition Result: 999844939360
Parallel processing time: 38 ms
Addition Result: 999844939360
Parallel processing time: 38 ms
Addition Result: 999844939360
Parallel processing time: 38 ms
Addition Result: 999844939360
Parallel processing time: 39 ms
Addition Result: 999844939360
Parallel processing time: 38 ms
Addition Result: 999844939360
Parallel processing time: 38 ms
Addition Result: 999844939360
Parallel processing time: 38 ms
Number of steals: 30


For parallel processing, the processing time is nearly 1/3 rd of the time required for sequential processing. 

CPU Utilization: 

CPU usage was found to be around 25 % during sequential execution. 

















For parallel processing , CPU usage was on an average around 58 %
















There is a clear increase in the CPU usage for parallel processing. The computation involved in this case was pretty simple (sum of the array elements). Better results can be experienced if the tasks involved are more complex.

The Fork / Join framework is more useful in cases where sequential operations are complex and time consuming. An appropriate threshold in such situations can result in much better CPU utilization. For short tasks, Fork/Join framework is not recommended.

You can also refer my previous blog on Fork/Join Framework for more information

Thanks and Happy Coding !

Java 7 - Fork / Join Framework example

Fork / Join as the name suggests is designed for work that can be broken into smaller pieces recursively. It is a new addition to the JDK 1.7 to support parallelism. It is an implementation of the ExecutorService interface that helps you take advantage of multiple processors.The goal is to use all the available processing power to enhance the performance of your application.

The Fork/Join framework is designed to make divide-and-conquer algorithms easy to parallelize. That type of algorithms is perfect for problems that can be divided into two or more sub-problems of the same type. They use recursion to break down the problem to simple tasks until these become simple enough to be solved directly. The solutions to the sub-problems are then combined to give a solution to the original problem.


The center of the fork/join framework is the ForkJoinPool class, an extension of the AbstractExecutorService class. ForkJoinPool implements the core work-stealing algorithm and can execute ForkJoinTask processes.It is similar to the MapReduce approach used to paralyze tasks. Difference is that Fork/Join tasks will subdivide themselves into smaller tasks only if necessary (if too large), whereas MapReduce algorithms divide up all the work into portions as the first step of their execution.


Basic Algorithm:


if(the job is small enough)
{
   compute directly
}
else
{
   split the work in two pieces (fork)
   invoke the pieces and join the results (join)
}


A ForkJoinTask is an abstract base class for tasks that run within a ForkJoinPool. A ForkJoinTask is a thread-like entity that is much lighter weight than a normal thread. Huge numbers of tasks and subtasks may be hosted by a small number of actual threads in a ForkJoinPool, at the price of some usage limitations.

There are two specialized subclasses of the ForkJoinTask :

1. RecursiveAction : It is to be used when you don’t need the task to return a result, for example, when the task works on positions of an array, it doesn’t return anything because it worked on the array. The method you should implement in order to do the job is compute():void, notice the void return.

2. RecursiveTask : It is to be used when your tasks return a result. For example, when computing addition of elements in an array, each task must return the number it computed in order to join them and obtain the general solution. The method you should implement in order to do the job is compute():V, where V is the type of return; for example in calculating the sum of integer elements in an array, V may be java.lang.Integer.

In this post, I ll be demonstrating you an example for the RecursiveAction. The task here is to fill the array elements with a random value. Here's the code :

package com.fork.join.action;

import static java.util.Arrays.asList;

import java.util.Random;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveAction;

public class ForkJoinRandomFillAction {
 Random random = new Random();

 public void loadArray(int[] array) {
  for (int i = 0; i < array.length; i++) {
   array[i] = random.nextInt(10000); // Generates numbers from 0 to
            // 10000
  }
 }

 public static void main(String[] args) {

  ForkJoinRandomFillAction sort = new ForkJoinRandomFillAction();

  int arrayLength = 2_00_00_0000;
  int array[] = new int[arrayLength];

  // No. of times sequential & Parallel operation should be performed
  final int iterations = 10;

  for (int i = 0; i < iterations; i++) {
   long start = System.currentTimeMillis();
   sort.loadArray(array);

   System.out.println("Sequential processing time: "
     + (System.currentTimeMillis() - start) + " ms");

  }

  System.out.println("Number of processor available: "
    + Runtime.getRuntime().availableProcessors());

  ForkJoinPool fjpool = new ForkJoinPool();
  // Default parallelism level
  // Runtime.getRuntime().availableProcessors()

  for (int i = 0; i < iterations; i++) {
   // Create a task with the complete array
   RecursiveAction task = new RandomFillAction(array, 0, array.length);
   long start = System.currentTimeMillis();
   fjpool.invoke(task);

   System.out.println("Parallel processing time: "
     + (System.currentTimeMillis() - start) + " ms");
  }

  System.out
    .println("Number of steals: " + fjpool.getStealCount() + "\n");
 }
}

class RandomFillAction extends RecursiveAction {
 private static final long serialVersionUID = 1L;
 final int low;
 final int high;
 private int[] array;
 final int splitSize = 2000000; // Some threshold size to spit the task

 public RandomFillAction(int[] array, int low, int high) {
  this.low = low;
  this.high = high;
  this.array = array;
 }

 @Override
 protected void compute() {
  if (high - low > splitSize) {
   // task is huge so divide in half
   int mid = (low + high) / 2;
   invokeAll(asList(new RandomFillAction(array, low, mid),
     new RandomFillAction(array, mid, high)));
  } else {
   // Some calculation logic
   Random random = new Random();
   for (int i = low; i < high; i++) {
    array[i] = random.nextInt(10000);
   }
  }
 }
}

For sequential processing the whole array is filled sequentially with random values. During parallel processing,
we first create a ForkJoinPool which will execute the ForkJoinTask. The default parallelism level for the ForkJoinPool is set to the no. of processors available with the system.Runtime.getRuntime().availableProcessors() will get this value.

Note that the RandomFillAction extends the RecursiveAction class and hence it needs to overwrite the default compute function which has the logic to either split the task or to perform the computation.

Some threshold value (array size in this case) is used to decide whether the computation is to be performed directly or is to be divided into sub tasks (ForkJoinTasks).


Observations:


Sequential processing time: 2316 ms
Sequential processing time: 2287 ms
Sequential processing time: 2287 ms
Sequential processing time: 2293 ms
Sequential processing time: 2287 ms
Sequential processing time: 2295 ms
Sequential processing time: 2292 ms
Sequential processing time: 2291 ms
Sequential processing time: 2291 ms
Sequential processing time: 2292 ms
Number of processor available: 4
Parallel processing time: 705 ms
Parallel processing time: 650 ms
Parallel processing time: 684 ms
Parallel processing time: 602 ms
Parallel processing time: 659 ms
Parallel processing time: 737 ms
Parallel processing time: 605 ms
Parallel processing time: 604 ms
Parallel processing time: 602 ms
Parallel processing time: 628 ms

Number of steals: 63

The paralell processing time is only about 30 % of the sequential processing time.


CPU Utilization

Sequential processing - The total CPU usage remained low close to 25 %, never went above 28 %.





















Parallel processing - CPU usage this time around was very attractive, on an average 96 %, hit 100 % at times and never fell below the 90 % mark. 


















This clearly demonstrates the power of Fork / Join framework and its ability to better utilize the CPU processors. 

In my next post, I will be demonstrating an example for the RecursiveTask class.

Thanks and Happy coding !