Skip to content

Commit a79c5bd

Browse files
committed
bench(feat): Add benchmarking cobc -> clang executables.
1 parent cca6a75 commit a79c5bd

File tree

6 files changed

+157
-0
lines changed

6 files changed

+157
-0
lines changed

clean.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ rm -rf "$BUILD_DIR"
5959
echo -e "${CYAN}Cleaning generated benchmark artifacts...${NC}"
6060
BENCH_OUT_DIR="${SCRIPT_DIR}/bench_out"
6161
rm -rf $BENCH_OUT_DIR/*.o
62+
rm -rf $BENCH_OUT_DIR/*.c*
6263
rm -rf $BENCH_OUT_DIR/bench_bin
6364

6465
# Done!

crates/bench/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/bench/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ clap = { version = "4.5.4", features = ["derive"] }
1111
colored = "2.1.0"
1212
indicatif = "0.17.8"
1313
miette = { version = "7.2.0", features = ["fancy"] }
14+
regex = "1.10.4"
1415
sanitize-filename = "0.5.0"
1516
serde = { version = "1.0.199", features = ["derive"] }
1617
serde_json = "1.0.116"

crates/bench/src/cli.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ pub struct Cli {
3838
#[arg(long, short = 'g', action)]
3939
pub run_comparative: bool,
4040

41+
/// Whether to run comparative tests against GnuCobol C -> clang.
42+
/// Requires `run_comparative`.
43+
#[arg(long, short = 'r', requires("run_comparative"), action)]
44+
pub run_clang: bool,
45+
4146
/// Whether to run only a build test and not execute benchmarks.
4247
#[arg(long, short = 'g', action)]
4348
pub build_only: bool,
@@ -112,6 +117,25 @@ impl TryInto<Cfg> for Cli {
112117
}
113118
}
114119

120+
// Check that `clang` is available if required.
121+
if self.run_clang {
122+
match Command::new("clang").output() {
123+
Ok(_) => {}
124+
Err(e) => {
125+
if let ErrorKind::NotFound = e.kind() {
126+
miette::bail!(
127+
"Clang comparisons enabled, but `clang` was not found! Check your PATH."
128+
);
129+
} else {
130+
miette::bail!(
131+
"An error occurred while verifying if `clang` was available: {}",
132+
e
133+
);
134+
}
135+
}
136+
}
137+
}
138+
115139
// Verify that the output directory exists.
116140
let output_dir = if let Some(path) = self.output_dir {
117141
path
@@ -223,6 +247,7 @@ impl TryInto<Cfg> for Cli {
223247
cobc_force_platform_linker: self.force_platform_linker,
224248
disable_hw_security: self.disable_hw_security,
225249
run_comparative: self.run_comparative,
250+
run_clang: self.run_clang,
226251
build_only: self.build_only,
227252
output_dir,
228253
output_log,

crates/bench/src/log.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ pub(crate) struct BenchmarkExecution {
6969
/// Benchmark results for `cobc`, if present.
7070
/// Only generated when executed with `--run-comparative`.
7171
pub cobc_results: Option<BenchmarkResult>,
72+
73+
/// Benchmark results for `cobc` -> `clang`, if present.
74+
/// Only generated when executed with `--run-clang`.
75+
pub clang_results: Option<BenchmarkResult>,
7276
}
7377

7478
/// Output for the result of a single benchmark compile/execute pair.

crates/bench/src/runner.rs

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use std::{
77

88
use colored::Colorize;
99
use miette::Result;
10+
use regex::Regex;
1011

1112
use crate::{
1213
bench::Benchmark,
@@ -37,6 +38,9 @@ pub(crate) struct Cfg {
3738
/// Whether to run comparative tests against GnuCobol's `cobc`.
3839
pub run_comparative: bool,
3940

41+
/// Whether to run comparative tests against `cobc` -> `clang`.
42+
pub run_clang: bool,
43+
4044
/// Whether to only build and not execute benchmarks.
4145
pub build_only: bool,
4246

@@ -72,13 +76,18 @@ pub(crate) fn run_single(cfg: &Cfg, benchmark: &Benchmark) -> Result<BenchmarkEx
7276
.run_comparative
7377
.then(|| run_cobc(cfg, benchmark))
7478
.transpose()?;
79+
let clang_results = cfg
80+
.run_clang
81+
.then(|| run_clang(cfg, benchmark))
82+
.transpose()?;
7583

7684
Ok(BenchmarkExecution {
7785
benchmark: benchmark.clone(),
7886
started_at,
7987
ended_at: chrono::offset::Local::now().to_utc(),
8088
cobalt_results,
8189
cobc_results,
90+
clang_results,
8291
})
8392
}
8493

@@ -182,6 +191,122 @@ fn run_cobc(cfg: &Cfg, benchmark: &Benchmark) -> Result<BenchmarkResult> {
182191
})
183192
}
184193

194+
/// Executes a single benchmark using GnuCobol's `cobc`'s C output followed by
195+
/// binary generation using `clang`.
196+
fn run_clang(cfg: &Cfg, benchmark: &Benchmark) -> Result<BenchmarkResult> {
197+
// Calculate output file locations for benchmarking binary, C file.
198+
// We also need a bootstrapping file for `main` since GnuCobol doesn't create
199+
// one for us.
200+
let mut bench_bin_path = cfg.output_dir.clone();
201+
bench_bin_path.push(BENCH_BIN_NAME);
202+
let mut bench_c_path = cfg.output_dir.clone();
203+
bench_c_path.push(format!("{}.c", &benchmark.name));
204+
let mut bootstrap_path = cfg.output_dir.clone();
205+
bootstrap_path.push("bootstrap.c");
206+
207+
// Set up commands for transpiling then compiling from C.
208+
let mut cobc = Command::new("cobc");
209+
cobc.args(["-C", &format!("-O{}", cfg.cobc_opt_level), "-free"])
210+
// Required to generate `cob_init()` in the transpiled C.
211+
.arg("-fimplicit-init")
212+
.args(["-o", bench_c_path.to_str().unwrap()])
213+
.arg(&benchmark.source_file);
214+
let mut clang = Command::new("clang");
215+
clang
216+
.args(["-lcob", &format!("-O{}", cfg.cobc_opt_level)])
217+
.args(["-o", bench_bin_path.to_str().unwrap()])
218+
.arg(bootstrap_path.to_str().unwrap());
219+
220+
// We require the program ID of the COBOL file to generate the bootstrapper.
221+
// Attempt to grep for that from the source.
222+
let bench_prog_func = fetch_program_func(benchmark)?;
223+
224+
// Generate the bootstrapping file.
225+
let bootstrapper = format!(
226+
"
227+
#include \"{}.c\"
228+
int main(void) {{
229+
{}();
230+
}}
231+
",
232+
&benchmark.name, bench_prog_func
233+
);
234+
std::fs::write(bootstrap_path.clone(), bootstrapper)
235+
.map_err(|e| miette::diagnostic!("Failed to write bootstrap file for `clang`: {e}"))?;
236+
237+
let before = Instant::now();
238+
for _ in 0..100 {
239+
// First, transpile to C.
240+
let out = cobc
241+
.output()
242+
.map_err(|e| miette::diagnostic!("Failed to execute `cobc`: {e}"))?;
243+
if !out.status.success() {
244+
miette::bail!(
245+
"Failed benchmark for '{}' with `cobc` compiler error: {}",
246+
benchmark.source_file,
247+
String::from_utf8_lossy(&out.stderr)
248+
);
249+
}
250+
251+
// Finally, perform compilation & linkage with `clang`.
252+
let out = clang
253+
.output()
254+
.map_err(|e| miette::diagnostic!("Failed to execute `clang`: {e}"))?;
255+
if !out.status.success() {
256+
miette::bail!(
257+
"Failed benchmark for '{}' with `clang` compiler error: {}",
258+
benchmark.source_file,
259+
String::from_utf8_lossy(&out.stderr)
260+
);
261+
}
262+
}
263+
let elapsed = before.elapsed();
264+
println!(
265+
"clang(compile): Total time {:.2?}, average/run of {:.6?}.",
266+
elapsed,
267+
elapsed / 100
268+
);
269+
270+
// Run the target program.
271+
let (execute_time_total, execute_time_avg) = if !cfg.build_only {
272+
let (x, y) = run_bench_bin(cfg, benchmark)?;
273+
(Some(x), Some(y))
274+
} else {
275+
(None, None)
276+
};
277+
278+
Ok(BenchmarkResult {
279+
compile_time_total: elapsed,
280+
compile_time_avg: elapsed / 100,
281+
execute_time_total,
282+
execute_time_avg,
283+
})
284+
}
285+
286+
/// Attempts to fetch the output C function name of the given benchmark COBOL file.
287+
/// If not found, throws an error.
288+
fn fetch_program_func(benchmark: &Benchmark) -> Result<String> {
289+
// First, read in source of benchmark.
290+
let source = std::fs::read_to_string(&benchmark.source_file).map_err(|e| {
291+
miette::diagnostic!(
292+
"Failed to read COBOL source for benchmark '{}': {e}",
293+
benchmark.name
294+
)
295+
})?;
296+
297+
// Search for a pattern matching "PROGRAM-ID ...".
298+
let prog_id_pat = Regex::new(r"PROGRAM-ID\. [A-Z0-9a-z\-]+").unwrap();
299+
let prog_id_str = prog_id_pat.find(&source).ok_or(miette::diagnostic!(
300+
"Could not find program ID in sources for benchmark '{}'.",
301+
&benchmark.name
302+
))?;
303+
304+
// Extract the program ID, format into final function name.
305+
let mut prog_id = prog_id_str.as_str()["PROGRAM-ID ".len()..].to_string();
306+
prog_id = prog_id.replace("-", "__");
307+
Ok(prog_id)
308+
}
309+
185310
/// Executes a single generated benchmarking binary.
186311
/// Returns the total execution time and average execution time per iteration.
187312
fn run_bench_bin(cfg: &Cfg, benchmark: &Benchmark) -> Result<(Duration, Duration)> {

0 commit comments

Comments
 (0)