Saturday, March 21, 2015

Multiple or Single Exit Point in a Java Methods

Recently, during a code review, a question popped up: Whether a method should have a single or multiple exit point once some conditions are satisfied. As discussed in here, there is not a clear, one cut answer for this question.

In terms of readability and exposing responsibility of method immediately, my personal preference is to return immediately from a method once task is done. Based on my experience with dealing complex, long methods with limited documentation, multiple returns in a method helped me to understand code easier in past.

But readability is not always the only factor when I write code in my current job. Performance (low latency) beats readability. So, I decided to benchmark these two methods:

Multiple return points 
public Number multipleReturnPoints(Number num) {
	if (num instanceof Integer) {
		return Integer.valueOf(num.intValue());
	}

	if (num instanceof Float) {
		return Float.valueOf(num.floatValue());
	}

	if (num instanceof Double) {
		return Double.valueOf(num.doubleValue());
	}
	return null;
}

Single returns in the end
public Number singleReturnPoint(Number num) {
	Number val = null;
	if (num instanceof Integer) {
		val = Integer.valueOf(num.intValue());
	}

	if (num instanceof Float) {
		val = Float.valueOf(num.floatValue());
	}

	if (num instanceof Double) {
		val = Double.valueOf(num.doubleValue());
	}
	return val;
}

I used following setup for testing:

  • Both method is called 5 million times, after 500K warm up  
  • In order to asses JIT compile's affect, two different schemes is used for methods parameter selection: 
    • Clustered (Batch): Same type parameter called in batch sequentially. In other words first Double, then Integer and finally Float type is chosen. 
    • Random : Type parameter is chosen from a uniform distribution. This is more realistic case would happen in a system. 
  • Following JRE are tested: 
    •  Oracle JRE 1.6.0_20 
    • Oracle JRE 1.7.0_15 
    • Oracle JRE 1.8.0_11
    • Azur Zing 1.7.0-5.9.5.0 
  • Garbage collection is prevented by allocating large amount of heap size
  • Tests are run on Redhat Linux  (64 bit,2.93GHz).

Results

Above benchmark test is done 7 times and in below charts, average latency (mSec) is illustrated .


 

Oracle JDK, although single returns perform marginally better , there is not much difference between multiple or single returns for both clustered random parameter selection. Whereas for Zing JVM, there is not a definitive conclusion. But given that random parameter selection is more realistic, single return is the winner, I think.
In conclusion, I could not find a definitive answer which method exit case performs better in terms of low latency. 
Source Code is as below:
package com.denizstij;

public interface Simulation {
	 Number run(Number num);
	 String getName();
}

////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////
package com.denizstij;

import java.util.Arrays;
import java.util.Random;

public class SingleOrMultipleExitPoints {

	public static class MultipleReturns implements Simulation {


		public Number run(Number num) {
			if (num instanceof Integer) {
				return Integer.valueOf(num.intValue());
			}

			if (num instanceof Float) {
				return Float.valueOf(num.floatValue());
			}

			if (num instanceof Double) {
				return Double.valueOf(num.doubleValue());
			}
			return null;
		}
		@Override
		public String getName() {
			return "Multiple Returns";
		}

	}

	public static class SingleReturn implements Simulation {
		public Number run(Number num) {
			Number val = null;
			if (num instanceof Integer) {
				val = Integer.valueOf(num.intValue());
			}

			if (num instanceof Float) {
				val = Float.valueOf(num.floatValue());
			}

			if (num instanceof Double) {
				val = Double.valueOf(num.doubleValue());
			}

			return val;
		}
		@Override
		public String getName() {
			return "Single Return";
		}
	}
	

	public static void main(String[] args) {
		
		Random random = new Random(0);
		int warmUpLoopSize = 50000;
		int loopSize = warmUpLoopSize * 10;
		if (args.length==2){
			warmUpLoopSize=Integer.parseInt(args[0]);
			loopSize=Integer.parseInt(args[1]);
			System.out.println("Number of warmUpLoopSize:"+warmUpLoopSize);
			System.out.println("Number of loopSize  :"+loopSize);
		}		
		// Arrays for warming up JVM 
		Integer warmUpIntArr[] = new Integer[warmUpLoopSize];
		Float warmUpFloatArr[] = new Float[warmUpLoopSize];
		Double warmUpDoubleArr[] = new Double[warmUpLoopSize];
		// Simulation arrays
		Integer intArr[] = new Integer[loopSize];
		Float floatArr[] = new Float[loopSize];
		Double doubleArr[] = new Double[loopSize];
		// Creatring random numbers 
		createRandomNumbers(random, warmUpIntArr, warmUpFloatArr, warmUpDoubleArr,warmUpLoopSize);
		createRandomNumbers(random, intArr, floatArr, doubleArr, loopSize);

		// Creating test cases 
		MultipleReturns multipleReturns = new MultipleReturns();
		SingleReturn singleReturn= new SingleReturn();
		
		int[] warmUpRandomCallSeq = generateRandomCallSequence(warmUpLoopSize,3,random);
		int[] warmUpClusteredCallSeq = generateClusteredCallSequence(warmUpLoopSize,3,random);
		int[] randomCallSeq = generateRandomCallSequence(loopSize,3,random);
		int[] clusteredCallSeq = generateClusteredCallSequence(loopSize,3,random);
		
		//System.out.println(Arrays.toString(warmUpRandomCallSeq));
		//System.out.println(Arrays.toString(warmUpClusteredCallSeq));
		
		String simName="Clustered Warmup";
		callSimulation(warmUpIntArr, warmUpFloatArr, warmUpDoubleArr,simName, multipleReturns,warmUpClusteredCallSeq);
		callSimulation(warmUpIntArr, warmUpFloatArr, warmUpDoubleArr,simName, singleReturn,warmUpClusteredCallSeq);
		
		simName="Clustered Calls";
		callSimulation(intArr, floatArr, doubleArr,simName, multipleReturns,clusteredCallSeq);
		callSimulation(intArr, floatArr, doubleArr,simName, singleReturn,clusteredCallSeq);
		System.out.println("==============================");
		
		simName="Random Warmup";
		callSimulation(warmUpIntArr, warmUpFloatArr, warmUpDoubleArr,simName, multipleReturns,warmUpRandomCallSeq);
		callSimulation(warmUpIntArr, warmUpFloatArr, warmUpDoubleArr,simName, singleReturn,warmUpRandomCallSeq);
		
		simName="Random Calls";
		callSimulation(intArr, floatArr, doubleArr,simName, multipleReturns,randomCallSeq);
		callSimulation(intArr, floatArr, doubleArr,simName, singleReturn,randomCallSeq);
		System.out.println("==============================");
		

	}

	private static void callSimulation(Integer[] intArr, Float[] floatArr, Double[] doubleArr, String msg, Simulation sim, int[] callSeq) {
		long now = System.currentTimeMillis();
		int clusterIndexes[]= new int[3];
		int index=0;
		for (int i = 0; i < callSeq.length; i++) {
			int callerIndex = callSeq[i];
			index=clusterIndexes[callerIndex]++;
			if (index>=intArr.length){
				continue;
			}		
			switch (callerIndex){
				case 0:
					doubleArr[index] = (Double) sim.run(doubleArr[index]);
					break;
				case 1:
					intArr[index] = (Integer) sim.run(intArr[index]);
					break;
				case 2:
					floatArr[index] = (Float) sim.run(floatArr[index]);
					break;
				default:
					System.out.println("Unknown cluster :("+callerIndex);
			}
		}

		long finish = System.currentTimeMillis();
		long elapsedTime = finish - now;
		System.out.println(elapsedTime + ": " +msg+" :"+ sim.getName());
		System.out.println(Arrays.toString(clusterIndexes));
		System.out.flush();
		
	}	
	
	static void createRandomNumbers(Random random, Integer intArr[],
			Float floatArr[], Double doubleArr[], int size) {
		for (int i = 0; i < size; i++) {
			intArr[i] = Integer.valueOf(random.nextInt(size));
			floatArr[i] = Float.valueOf(random.nextFloat() * size);
			doubleArr[i] = Double.valueOf(random.nextDouble() * size);
		}
	}
	
	static int[] generateClusteredCallSequence(int simSize,int numOfCluster,Random random){
		int totalSize=numOfCluster*simSize;
		int callSeq[]= new int[totalSize];
		int index=0;
		for (int j=0;j < numOfCluster;j++){
			for (int i=0;i < simSize;i++){
				callSeq[index++]=j;
			}
		}
		return callSeq;
	}
	
	static int[] generateRandomCallSequence(int simSize,int numOfCluster,Random random){
		int totalSize=numOfCluster*simSize;
		int callSeq[]= new int[totalSize];
		for (int i=0;i < totalSize;i++){
			callSeq[i]=random.nextInt(numOfCluster);
		}
		return callSeq;
	}
}