1 | #!/usr/bin/env python3 |
2 | """Compiles tests/header_only.c to test size, tests/header_unit*.c to test speed""" |
3 |
|
4 | import sys |
5 | import os |
6 | import subprocess |
7 | import re |
8 | from statistics import median |
9 |
|
10 | PROJECT_PATH = os.path.abspath( |
11 | os.path.join(os.path.dirname(os.path.abspath(__file__)), "..") |
12 | ) |
13 | BIN_PATH = os.path.join(PROJECT_PATH, "bin") |
14 | INCLUDE_PATH = os.path.join(PROJECT_PATH, "src") |
15 | HEADER_ONLY = os.path.join(PROJECT_PATH, "tests/header_only.c") |
16 | HEADER_PERFORMANCE = os.path.join(PROJECT_PATH, "tests/header_memory.c") |
17 |
|
18 | CC = "gcc" |
19 | CFLAGS = ["-std=c11", "-pedantic", "-Wall", "-Wextra", "-Werror"] |
20 | COUTFLAG = "-o" |
21 | COPTIMIZATIONS = ["-O0", "-O1", "-O2", "-O3", "-Os", "-Ofast"] |
22 | CINCLUDES = ["-I", INCLUDE_PATH] |
23 |
|
24 | TABLE_HEADER_1 = "Optimization Level" |
25 | TABLE_HEADER_2 = "Header Binary Size (in bytes)" |
26 | TABLE_HEADER_3 = "Memory Test Runtime (in µs)" |
27 |
|
28 | RE_TRIAL_TIME = r"time elapsed \(us\): (\d*)" |
29 | TRIAL_PASSES = 100 |
30 |
|
31 |
|
32 | def cc_compile(out_file: str, in_files: list, optimization_level: str = "-O0"): |
33 | """Uses gcc subprocess to compile at a set optimization level""" |
34 | process_command = ( |
35 | [CC] |
36 | + CFLAGS |
37 | + CINCLUDES |
38 | + [COUTFLAG, out_file] |
39 | + in_files |
40 | + [optimization_level] |
41 | ) |
42 | process = subprocess.run(process_command, stdout=sys.stdout, stderr=sys.stderr) |
43 | if process.returncode: |
44 | print("[cc_compile] error in compilation") |
45 | return 0 |
46 | else: |
47 | print(f"[cc_compile] finished compiling to {out_file}") |
48 | return 1 |
49 |
|
50 |
|
51 | def execute_file(executable: str): |
52 | """Executes the input file and returns the stdout""" |
53 | process = subprocess.run([executable], capture_output=True, text=True) |
54 | return process.stdout |
55 |
|
56 |
|
57 | def get_size(file_name: str): |
58 | """Uses os.stat to get the file size in bytes of a specified file""" |
59 | return os.stat(file_name).st_size |
60 |
|
61 |
|
62 | if __name__ == "__main__": |
63 | try: |
64 | os.mkdir(BIN_PATH) |
65 | except FileExistsError: |
66 | pass |
67 | else: |
68 | print("[main] created bin/") |
69 | print("[main] compiling...") |
70 | print(f"[main/compile] using compiler {CC}") |
71 | for optimization in COPTIMIZATIONS: |
72 | header_only_out = os.path.join(BIN_PATH, f"c-header-only{optimization}") |
73 | if not cc_compile(header_only_out, [HEADER_ONLY], optimization): |
74 | sys.exit() |
75 | header_unit_out = os.path.join(BIN_PATH, f"c-performance{optimization}") |
76 | if not cc_compile(header_unit_out, [HEADER_PERFORMANCE], optimization): |
77 | sys.exit() |
78 | print("[main] getting filesize data of header_only binaries") |
79 | EXECUTABLE_SIZES = {} |
80 | for optimization in COPTIMIZATIONS: |
81 | header_only_out = os.path.join(BIN_PATH, f"c-header-only{optimization}") |
82 | EXECUTABLE_SIZES[optimization] = get_size(header_only_out) |
83 | print("[main] beginning time trials") |
84 | EXECUTABLE_RUNTIMES = {} |
85 | import matplotlib.pyplot as plt |
86 | plt.xlabel("trial") |
87 | plt.ylabel("time (µs)") |
88 | for optimization in COPTIMIZATIONS: |
89 | print(f"[main/trials] running trial on {optimization}") |
90 | header_unit_out = os.path.join(BIN_PATH, f"c-performance{optimization}") |
91 | trial_runtimes = [] |
92 | for trial_num in range(TRIAL_PASSES): |
93 | trial_output = execute_file(header_unit_out) |
94 | trial_time = re.search(RE_TRIAL_TIME, trial_output).group(1) |
95 | trial_runtimes.append(int(trial_time)) |
96 | plt.plot(sorted(trial_runtimes)) |
97 | trial_runtimes.sort() |
98 | trial_median = int(median(trial_runtimes)) |
99 | trial_mid = int(TRIAL_PASSES/2) |
100 | trial_iqr = int(median(trial_runtimes[trial_mid:TRIAL_PASSES]) - median(trial_runtimes[0:trial_mid])) |
101 | trial_moe = int(trial_iqr/2) |
102 | EXECUTABLE_RUNTIMES[optimization] = f"{trial_median} ± {trial_moe}" |
103 | plt.legend(COPTIMIZATIONS) |
104 | print("[main] finished trials:") |
105 | print(f"{TABLE_HEADER_1} | {TABLE_HEADER_2} | {TABLE_HEADER_3}") |
106 | print( |
107 | f"{len(TABLE_HEADER_1)*'-'} | {len(TABLE_HEADER_2)*'-'} | {len(TABLE_HEADER_3)*'-'}" |
108 | ) |
109 | for optimization in COPTIMIZATIONS: |
110 | executable_size = EXECUTABLE_SIZES[optimization] |
111 | executable_runtime = EXECUTABLE_RUNTIMES[optimization] |
112 | print( |
113 | f"{optimization.ljust(len(TABLE_HEADER_1))} | " |
114 | + f"{str(executable_size).ljust(len(TABLE_HEADER_2))} | {executable_runtime}" |
115 | ) |
116 | plt.show() |
117 |
|