package DPJRuntime; import java.util.Stack; import java.util.LinkedList; import java.util.Map; import java.util.HashMap; import java.util.TreeMap; import java.util.Collections; import java.io.PrintWriter; import java.io.FileWriter; import java.io.IOException; /** * {@code Instrument} is the support class for DPJ instrumentation. * * When a DPJ program is compiled with the {@code -instrument} flag, * the compiler generates calls to the methods of this class at * appropriate points in the code. * * @author Robert L. Bocchino Jr. * @author Mohsen Vakilian */ public class Instrument { /** * A simple free list allocator for managing environments */ private static Freelist allocator = new Freelist(); private static class Freelist { LinkedList<Environment> freelist = new LinkedList<Environment>(); /** * Get an environment off the list, or make one if the list is * empty */ public Environment newEnv() { Environment result; if (freelist.size() > 0) { result = freelist.removeLast(); } else { result = new Environment(); } result.ID = Environment.numIDs++; return result; } public Environment newEnv(Environment oldEnv) { Environment result = newEnv(); result.parallelStartTime = oldEnv.parallelStartTime + oldEnv.parallelBranchTime; return result; } /** * Put an environment on the list when we're done with it */ public void freeEnv(Environment e) { e.clear(); freelist.addLast(e); } } /** * Execution environment for measuring times */ private static class Environment { /** * Timer for measuring execution times */ private Timer timer = new Timer(); public int ID = 0; public static int numIDs = 0; /** * Serial execution time (all branches) */ private long serialTime; /** * Pure serial time, i.e., total time when only one thread is * executing. */ private long pureSerialTime; /** * Parallel execution time (all branches) */ private long parallelTime; private long parallelStartTime; /** * Parallel execution time (one branch). Ultimately, * parallelTime will be the maximum of all parallelBranchTimes * we encountered in this environment. */ private long parallelBranchTime; public Environment() { } public Environment(Environment prevEnv) { parallelStartTime = prevEnv.parallelStartTime + prevEnv.parallelTime; } public void clear() { serialTime = 0; parallelTime = 0; parallelBranchTime = 0; } public void startTiming() { timer.start(); } public void stopTiming() { timer.stop(); serialTime += timer.getElapsedTime(); if (envStack.size() == 1) { pureSerialTime += timer.getElapsedTime(); } parallelBranchTime += timer.getElapsedTime(); } } /** * A simple timer class for performance measurement */ private static class Timer { /** * Whether the timer is on */ private boolean on = false; /** * Absolute time when the timer started */ private long startTime = 0; /** * Absolute time when the timer ended */ private long endTime = 0; /** * Time between start and end */ private long elapsedTime = 0; /** * Start the timer */ public void start() { elapsedTime = 0; startTime = System.nanoTime(); } /** * Stop the timer */ public void stop() { endTime = System.nanoTime(); long difference = endTime - startTime; if (difference > 0) elapsedTime = difference; } /** * Get the elapsed time */ public long getElapsedTime() { return elapsedTime; } } /** * A stack of environments for managing scopes */ private static Stack<Environment> envStack = new Stack<Environment>(); /** * The final results */ private static long serialTime; private static long pureSerialTime; private static long parallelTime; /** * Switch to turn the timing on and off */ private static boolean on; /** * Enter time of each spawned task. */ private static Map<Integer, Long> enterTimes = new HashMap<Integer, Long>(); /** * Exit time of each spawed task. */ private static Map<Integer, Long> exitTimes = new HashMap<Integer, Long>(); private static TreeMap<Long,Integer> tasksDeltaAtTime = new TreeMap<Long,Integer>(); /** * Entry tasksDeltaAtTime[t] denotes the number of generated tasks * at time `t'. */ private static TreeMap<Long,Integer> numOfTasksAtTime = null; // Private helper methods private static void addToMapEntry(Map<Long, Integer> map, Long key, Integer delta) { if (!map.containsKey(key)) map.put(key, 0); map.put(key, map.get(key) + delta); } private static Map<Long,Integer> partialSum(TreeMap<Long,Integer> map) { if (numOfTasksAtTime != null) return numOfTasksAtTime; Long[] taskTimes = map.navigableKeySet(). toArray(new Long[]{}); numOfTasksAtTime = new TreeMap<Long,Integer>(); numOfTasksAtTime.put(taskTimes[0], map.get(taskTimes[0])); for (int i = 1; i < taskTimes.length; ++i) { numOfTasksAtTime.put(taskTimes[i], map.get(taskTimes[i]) + numOfTasksAtTime. get(taskTimes[i-1])); } return numOfTasksAtTime; } // Public instrumentation interface /** * Starts the timing. */ public static void start() { // // Clear the map holding the number of active tasks at each // moment. // tasksDeltaAtTime = new TreeMap<Long, Integer>(); // // Turn on the instrumentation // on = true; // // Start with a fresh stack of environments // envStack.clear(); // // Push a new environment on the stack // Environment env = allocator.newEnv(); envStack.push(env); addToMapEntry(tasksDeltaAtTime, 0L, 1); // // Start timing in the current environment // env.startTiming(); } /** * Called upon entry to a {@code foreach} statement. * * @param numIters The number of iterations in the {@code foreach} */ public static void enterForeach(int numIters) { if (on) { // // Stop timing in the current environment // Environment env = envStack.peek(); env.stopTiming(); // // Start a new environment // Environment newEnv = allocator.newEnv(env); // Add (newEnv.parallelStartTime, numIters) to map addToMapEntry(tasksDeltaAtTime, newEnv.parallelStartTime, numIters); envStack.push(newEnv); } } /** * Called upon entry to a {@code foreach} iteration */ public static void enterForeachIter() { if (on) { // // Get the current environment off the stack // Environment env = envStack.peek(); // // Starting a new branch: reset the parallel branch time // env.parallelBranchTime = 0; // // Start timing // env.startTiming(); } } /** * Called upon exit from a {@code foreach} iteration */ public static void exitForeachIter() { if (on) { // // Get the current environment off the stack and stop // timing // Environment env = envStack.peek(); env.stopTiming(); // // If the current branch took longer than the previous // longest branch, update the parallel time // if (env.parallelBranchTime > env.parallelTime) env.parallelTime = env.parallelBranchTime; // // Record end of iteration in map // // Add (env.parallelStartTime+env.parallelBranchTime,-1) // to map // addToMapEntry(tasksDeltaAtTime, env.parallelStartTime + env.parallelBranchTime, -1); } } /** * Called upon exit from a {@code foreach} statement. */ public static void exitForeach() { if (on) { // // Get the environment of the foreach off the stack and // pop out to the outer environment // Environment foreachEnv = envStack.pop(); // // Get the new current (outer) environment off the stack // Environment env = envStack.peek(); // // Serial and parallel times of the foreach contribute to // the serial and parallel branch times of the outer // environment. We use the parallel time of the foreach // environment because it's done, so we know its parallel // time. However, we update the parallel *branch* time of // the outer environment because it's not done yet so we // are computing the time of a parallel branch. For // example, it itself could be a foreach. // env.serialTime += foreachEnv.serialTime; env.parallelBranchTime += foreachEnv.parallelTime; // // Put the popped environment back in the allocator pool // allocator.freeEnv(foreachEnv); // // Start timing again in the outer environment // env.startTiming(); } } /** * Called upon entry to a {@code cobegin} statement. */ public static void enterCobegin() { if (on) { // // Stop timing in the current environment // Environment env = envStack.peek(); env.stopTiming(); // // Start a new environment // env = allocator.newEnv(env); envStack.push(env); // // Start timing // env.startTiming(); } } /** * Called between statements in a {@code cobegin} statement. */ public static void cobeginSeparator() { if (on) { // // Get the current environment off the stack and stop // timing // Environment env = envStack.peek(); env.stopTiming(); // // If the current branch took longer than the previous // longest branch, update the parallel time // if (env.parallelBranchTime > env.parallelTime) env.parallelTime = env.parallelBranchTime; // if (env.parallelBranchTime > 0) { addToMapEntry(tasksDeltaAtTime, env.parallelStartTime, 1); addToMapEntry(tasksDeltaAtTime, env.parallelStartTime + env.parallelBranchTime, -1); } // // Starting a new branch: reset the parallel branch time // env.parallelBranchTime = 0; // // Start timing // env.startTiming(); } } /** * Called upon exit from a {@code cobegin} statement. */ public static void exitCobegin() { if (on) { // // Get the environment of the cobegin off the stack and // pop out to the outer environment // Environment cobeginEnv = envStack.pop(); // // Get the new current (outer) environment off the stack // Environment env = envStack.peek(); // // Analogous to what we do for foreach; see comments // there. // env.serialTime += cobeginEnv.serialTime; env.parallelBranchTime += cobeginEnv.parallelTime; // // Put the popped environment back in the allocator pool // allocator.freeEnv(cobeginEnv); // // Start timing again in the outer environment // env.startTiming(); } } /** * Called upon entry to a {@code finish} statement. */ public static void enterFinish() { if (on) { // // Get the current environment off the stack and stop its // timer // Environment env = envStack.peek(); env.timer.stop(); // // Push a new environment to handle the finish and start // timing in that environment // env = allocator.newEnv(env); envStack.push(env); env.timer.start(); } } /** * Called upon exit from a {@code finish} statement. */ public static void exitFinish() { if (on) { // // Get the current environment off the stack, stop its // timer, and compute its elapsed time // Environment finishEnv = envStack.pop(); finishEnv.stopTiming(); if (finishEnv.parallelBranchTime > finishEnv.parallelTime) finishEnv.parallelTime = finishEnv.parallelBranchTime; Environment env = envStack.peek(); env.serialTime += finishEnv.serialTime; env.parallelBranchTime += finishEnv.parallelTime; // // Free the popped environment and restart the timer in // the outer scope // allocator.freeEnv(finishEnv); env.timer.start(); } } /** * Called upon entry to a {@code spawn} statement. */ public static void enterSpawn() { if (on) { // // Get the current environment off the stack and stop its // timer // Environment env = envStack.peek(); env.stopTiming(); // // Push a new environment to handle the finish and start // timing in that environment // Environment oldEnv = env; env = allocator.newEnv(env); // //Record the start time of the spawned task. // enterTimes.put(env.ID, env.parallelStartTime); envStack.push(env); // // Add (env.parallelStartTime, 1) to map // addToMapEntry(tasksDeltaAtTime, env.parallelStartTime, 1); env.startTiming(); } } /** * Called upon exit from a {@code spawn} statement. */ public static void exitSpawn() { if (on) { // // Get the current environment off the stack, stop its // timer, and compute its elapsed time // Environment spawnEnv = envStack.pop(); spawnEnv.stopTiming(); // // Update parallel time of spawn env // if (spawnEnv.parallelBranchTime > spawnEnv.parallelTime) spawnEnv.parallelTime = spawnEnv.parallelBranchTime; // // Update serial and parallel time of outer env // Environment env = envStack.peek(); env.serialTime += spawnEnv.serialTime; long spawnTime = env.parallelBranchTime + spawnEnv.parallelTime; if (spawnTime > env.parallelTime) env.parallelTime = spawnTime; // // Record end of spawn in map // // Add (env.parallelStartTime+spawnTime, -1) to map // addToMapEntry(tasksDeltaAtTime, env.parallelStartTime+spawnTime, -1); // // Record the end time of the spawned task. // exitTimes.put(spawnEnv.ID, env.parallelStartTime+spawnTime); // // Free the popped environment and restart the timer in // the outer scope // allocator.freeEnv(spawnEnv); env.startTiming(); } } /** * Called at the end of the computation. */ public static void end() { Environment env = envStack.pop(); env.stopTiming(); on = false; parallelTime = env.parallelBranchTime; serialTime = env.serialTime; pureSerialTime = env.pureSerialTime; addToMapEntry(tasksDeltaAtTime, parallelTime, 0); } // Output methods /** * Returns a map representing the program task graph. The map * takes program points to number of tasks. There is one entry in * the map for each point at which the number of tasks changed. * * @return Map representing program task graph */ public static Map<Long,Integer> getTasksMap() { return Collections. unmodifiableMap(partialSum(tasksDeltaAtTime)); } /** * Prints a string representation of the task map to the given * file. * * @param filepath Pathname of file to print to */ public static void printTasksMap(String filepath) throws IOException { PrintWriter outputStream = null; try { outputStream = new PrintWriter(new FileWriter(filepath)); Map<Long,Integer> tasksMap = getTasksMap(); for (Long time : tasksMap.keySet()) { outputStream.println(time + "\t" + tasksMap.get(time)); } } finally { if (outputStream != null) outputStream.close(); } } /** * Prints the start and end point of each task to the given file. * * @param filepath Pathname of file to print to */ public static void printTaskIntervals(String filepath) throws IOException { PrintWriter outputStream = null; try { outputStream = new PrintWriter(new FileWriter(filepath)); for (Integer taskID : enterTimes.keySet()) { outputStream.println(taskID + "\t" + enterTimes.get(taskID) + "\t" + exitTimes.get(taskID)); } } finally { if (outputStream != null) outputStream.close(); } } /** * Returns the average width of the parallelism graph, i.e., the * average number of tasks active at every point of the program. * Computed as the quotient of (1) the area under the curve of * number of tasks vs. time and (2) the total time. * * @return Average width of the parallelism graph */ public static double averageWidth() { Long[] taskTimes = Instrument.getTasksMap(). keySet().toArray(new Long[]{}); long totalWidth = 0; for (int i = 1; i < taskTimes.length; ++i) { totalWidth += (taskTimes[i] - taskTimes[i-1]) * numOfTasksAtTime.get(taskTimes[i-1]); } return ((double) totalWidth / taskTimes[taskTimes.length-1]); } /** * Returns the ideal speedup, computed as the serial time divided * by the parallel time. * * @return Ideal speedup */ public static double idealSpeedup() { return ((double) serialTime) / parallelTime; } /** * Returns the measured serial time of the computation. Within a * {@code cobegin} or {@code foreach} construct, the serial time * is the sum of the times of the individual tasks. * * @return Serial time */ public static long getSerialTime() { return serialTime; } /** * Returns the measured parallel time of the computation. Within * a {@code cobegin} or {@code foreach} construct, the parallel * time is the maximum of the times of the indiviudal tasks. * * @return Parallel time */ public static long getParallelTime() { return parallelTime; } /** * Returns the best speedup we could achieve under Amdahl's law, * assuming perfect speedup of the parallel parts. This is * accurate for programs that create all parallelism with {@code * cobegin} and {@code foreach}. It is not accurate for programs * that use {@code spawn} and {@code finish}. * * @return Bound given by Amdahl's law */ public static double amdahlBound() { return ((double) serialTime) / pureSerialTime; } }