#! /usr/bin/env python """ Compute the size at which the current compiler will start to significantly scale back optimization. The CPP file being modified will need the following tags. // JGF_DUPLICATE_BEGIN - Put before start of function to duplicate // JGF_DUPLICATE_END - Put after end of function to duplcate // JGF_DUPE function_name(args); - Put anywhere where it's legal to put a function call but not in your timing section. The program will need to output the string: FOM: This will represent the program's performance """ import argparse, sys, os, doctest, subprocess, re, time VERBOSE = False ############################################################################### def parse_command_line(args, description): ############################################################################### parser = argparse.ArgumentParser( usage="""\n%s [--verbose] OR %s --help OR %s --test \033[1mEXAMPLES:\033[0m > %s foo.cpp 'make -j4' foo """ % ((os.path.basename(args[0]), ) * 4), description=description, formatter_class=argparse.ArgumentDefaultsHelpFormatter ) parser.add_argument("cppfile", help="Name of file to modify.") parser.add_argument("buildcmd", help="Build command") parser.add_argument("execmd", help="Run command") parser.add_argument("-v", "--verbose", action="store_true", help="Print extra information") parser.add_argument("-s", "--start", type=int, default=1, help="Starting number of dupes") parser.add_argument("-e", "--end", type=int, default=1000, help="Ending number of dupes") parser.add_argument("-n", "--repeat", type=int, default=10, help="Number of times to repeat an individial execution. Best value will be taken.") parser.add_argument("-t", "--template", action="store_true", help="Use templating instead of source copying to increase object size") parser.add_argument("-c", "--csv", action="store_true", help="Print results as CSV") args = parser.parse_args(args[1:]) if (args.verbose): global VERBOSE VERBOSE = True return args.cppfile, args.buildcmd, args.execmd, args.start, args.end, args.repeat, args.template, args.csv ############################################################################### def verbose_print(msg, override=None): ############################################################################### if ( (VERBOSE and not override is False) or override): print msg ############################################################################### def error_print(msg): ############################################################################### print >> sys.stderr, msg ############################################################################### def expect(condition, error_msg): ############################################################################### """ Similar to assert except doesn't generate an ugly stacktrace. Useful for checking user error, not programming error. """ if (not condition): raise SystemExit("FAIL: %s" % error_msg) ############################################################################### def run_cmd(cmd, ok_to_fail=False, input_str=None, from_dir=None, verbose=None, arg_stdout=subprocess.PIPE, arg_stderr=subprocess.PIPE): ############################################################################### verbose_print("RUN: %s" % cmd, verbose) if (input_str is not None): stdin = subprocess.PIPE else: stdin = None proc = subprocess.Popen(cmd, shell=True, stdout=arg_stdout, stderr=arg_stderr, stdin=stdin, cwd=from_dir) output, errput = proc.communicate(input_str) output = output.strip() if output is not None else output stat = proc.wait() if (ok_to_fail): return stat, output, errput else: if (arg_stderr is not None): errput = errput if errput is not None else open(arg_stderr.name, "r").read() expect(stat == 0, "Command: '%s' failed with error '%s'" % (cmd, errput)) else: expect(stat == 0, "Command: '%s' failed. See terminal output" % cmd) return output ############################################################################### def build_and_run(source, cppfile, buildcmd, execmd, repeat): ############################################################################### open(cppfile, 'w').writelines(source) run_cmd(buildcmd) best = None for i in xrange(repeat): wait_for_quiet_machine() output = run_cmd(execmd) current = None fom_regex = re.compile(r'^FOM: ([0-9.]+)$') for line in output.splitlines(): m = fom_regex.match(line) if (m is not None): current = float(m.groups()[0]) break expect(current is not None, "No lines in output matched FOM regex") if (best is None or best < current): best = current return best ############################################################################### def wait_for_quiet_machine(): ############################################################################### while(True): time.sleep(2) # The first iteration of top gives garbage results idle_pct_raw = run_cmd("top -bn2 | grep 'Cpu(s)' | tr ',' ' ' | tail -n 1 | awk '{print $5}'") idle_pct_re = re.compile(r'^([0-9.]+)%id$') m = idle_pct_re.match(idle_pct_raw) expect(m is not None, "top not returning output in expected form") idle_pct = float(m.groups()[0]) if (idle_pct < 95): error_print("Machine is too busy, waiting for it to become free") else: break ############################################################################### def add_n_dupes(curr_lines, num_dupes, template): ############################################################################### function_name = None function_invocation = None function_lines = [] function_re = re.compile(r'^.* (\w+) *[(]') function_inv_re = re.compile(r'^.*JGF_DUPE: +(.+)$') # Get function lines record = False definition_insertion_point = None invocation_insertion_point = None for idx, line in enumerate(curr_lines): if ("JGF_DUPLICATE_BEGIN" in line): record = True m = function_re.match(curr_lines[idx+1]) expect(m is not None, "Could not find function in line '%s'" % curr_lines[idx+1]) function_name = m.groups()[0] elif ("JGF_DUPLICATE_END" in line): record = False definition_insertion_point = idx + 1 elif (record): function_lines.append(line) elif ("JGF_DUPE" in line): m = function_inv_re.match(line) expect(m is not None, "Could not find function invocation example in line '%s'" % line) function_invocation = m.groups()[0] invocation_insertion_point = idx + 1 expect(function_name is not None, "Could not find name of dupe function") expect(function_invocation is not None, "Could not find function invocation point") expect(definition_insertion_point < invocation_insertion_point, "fix me") dupe_func_defs = [] dupe_invocations = ["int jgf_rand = std::rand();\n", "if (false) {}\n"] for i in xrange(num_dupes): if (not template): dupe_func = list(function_lines) dupe_func[0] = dupe_func[0].replace(function_name, "%s%d" % (function_name, i)) dupe_func_defs.extend(dupe_func) dupe_invocations.append("else if (jgf_rand == %d) " % i) if (template): dupe_call = function_invocation.replace(function_name, "%s<%d>" % (function_name, i)) + "\n" else: dupe_call = function_invocation.replace(function_name, "%s%d" % (function_name, i)) + "\n" dupe_invocations.append(dupe_call) curr_lines[invocation_insertion_point:invocation_insertion_point] = dupe_invocations curr_lines[definition_insertion_point:definition_insertion_point] = dupe_func_defs ############################################################################### def report(num_dupes, curr_lines, object_file, orig_fom, curr_fom, csv=False, is_first_report=False): ############################################################################### fom_change = (curr_fom - orig_fom) / orig_fom if (csv): if (is_first_report): print "num_dupes, obj_byte_size, loc, fom, pct_diff" print "%s, %s, %s, %s, %s" % (num_dupes, os.path.getsize(object_file), len(curr_lines), curr_fom, fom_change*100) else: print "========================================================" print "For number of dupes:", num_dupes print "Object file size (bytes):", os.path.getsize(object_file) print "Lines of code:", len(curr_lines) print "Field of merit:", curr_fom print "Change pct:", fom_change*100 ############################################################################### def obj_size_opt_check(cppfile, buildcmd, execmd, start, end, repeat, template, csv=False): ############################################################################### orig_source_lines = open(cppfile, 'r').readlines() backup_file = "%s.orig" % cppfile object_file = "%s.o" % os.path.splitext(cppfile)[0] os.rename(cppfile, backup_file) orig_fom = build_and_run(orig_source_lines, cppfile, buildcmd, execmd, repeat) report(0, orig_source_lines, object_file, orig_fom, orig_fom, csv=csv, is_first_report=True) i = start while (i < end): curr_lines = list(orig_source_lines) add_n_dupes(curr_lines, i, template) curr_fom = build_and_run(curr_lines, cppfile, buildcmd, execmd, repeat) report(i, curr_lines, object_file, orig_fom, curr_fom, csv=csv) i *= 2 # make growth function configurable? os.remove(cppfile) os.rename(backup_file, cppfile) ############################################################################### def _main_func(description): ############################################################################### if ("--test" in sys.argv): test_results = doctest.testmod(verbose=True) sys.exit(1 if test_results.failed > 0 else 0) cppfile, buildcmd, execmd, start, end, repeat, template, csv = parse_command_line(sys.argv, description) obj_size_opt_check(cppfile, buildcmd, execmd, start, end, repeat, template, csv) ############################################################################### if (__name__ == "__main__"): _main_func(__doc__)