A reproducible benchmarking pipeline for twelve sorting algorithms written in C
with OpenMP parallelization, measured with Linux perf hardware counters and
RAPL energy domains. It collects performance, microarchitecture and energy data,
parses it into CSV, and produces a full set of analysis figures.
Each algorithm is compiled in two flavors (serial and parallel) and run many times
under tight reproducibility controls while perf records hardware events. The pipeline
then aggregates the runs and generates plots covering empirical complexity, parallel
scalability (speedup, efficiency, Amdahl serial fraction), microarchitectural behavior
(IPC, branch/cache misses), memory hierarchy pressure, run-to-run distributions, and
energy (package + DRAM, EDP/ED²P, energy–time Pareto).
Reproducibility controls (built into the runner):
:umodifier on every PMU event — counts user space only, removing kernel-attributed noise soinstructions:ubecomes exactly constant across runs.setarch -R— disables ASLR, fixing the memory layout.tasksetcore pinning + OpenMP placement (OMP_PROC_BIND,OMP_PLACES).- NMI watchdog disabled — frees one hardware counter the kernel would otherwise hold.
- Events split into multiplexing-free passes (≤ 4 general-purpose events each), so no counter is time-sliced and every count is exact, not estimated.
The twelve algorithms (two per family):
| Family | Algorithms |
|---|---|
| Insertion | insertionsort, shellsort |
| Exchange | combsort, quicksort (dual-pivot) |
| Selection | heapsort, smoothsort |
| Merge | mergesort (top-down), bottomupsort |
| Distribution | countingsort, radixsort (LSD) |
| Network | bitonicsort, oddevensort |
Network sorts require a power-of-two input size; other sizes are padded, which roughly doubles the work just above a power of two. Prefer power-of-two
Nfor those two.
*.c # algorithm sources (shared template: same main, distributions, flags)
run_perf.sh # benchmark runner: 3 passes per run (P1, P2 counters; P3 energy)
parse_perf.py # parses results/*.p1/.p2/.p3 -> CSV (raw + summary)
plot_perf.py # per-metric plots (mean +- 95% CI, std, speedup)
analyze_perf.py # analysis figures (complexity, scalability, microarch, memory, energy, ...)
README.md # this file
This pipeline is Linux-only (perf and RAPL are Linux features) and needs
sudo (perf accesses RAPL and toggles the NMI watchdog).
sudo apt update
sudo apt install -y build-essential \
linux-tools-common linux-tools-generic linux-tools-$(uname -r) \
util-linux python3 python3-pipbuild-essentialprovidesgcc(with OpenMP via-fopenmp).linux-tools-$(uname -r)providesperf.util-linuxprovidessetarchandtaskset(usually preinstalled).
pip3 install pandas numpy matplotlib scipyscipy is optional: plot_perf.py uses it for the t-distribution confidence interval and
falls back to 1.96 if it is missing; analyze_perf.py does not require it.
Check which RAPL domains your machine exposes:
sudo perf list | grep energyYou should see power/energy-pkg/ and ideally power/energy-ram/. The DRAM (energy-ram)
domain is often absent on consumer desktops (typically present on Xeon/server, varies on
AMD). If it is missing, the runner measures package only and the energy-breakdown figures are
skipped automatically — everything else still works.
The parser reads algo, mode and N from file names, so binaries must be named
<algo>_serial and <algo>_parallel. Build both flavors of every source:
for src in *.c; do
algo="${src%.c}"
gcc -O2 -fopenmp -o "${algo}_serial" "$src" -lm
gcc -O2 -fopenmp -DPARALLEL -o "${algo}_parallel" "$src" -lm
doneThe serial build also needs
-fopenmpbecausemaincallsomp_get_wtime().-DPARALLELswitches the parallel code path on.
Each program reads two numbers from its input file: a random seed (line 1) and the
size N (line 2). It generates the array internally (uniform by default). Name each input
file by its size, so the complexity plot gets the real N on its x-axis:
printf '42\n1000000\n' > 1000000.in
printf '42\n10000000\n' > 10000000.inTo change the input distribution, edit
mainin the.cfiles:fill_uniformis active by default;fill_normalandfill_exponentialare provided just below it, commented out.
./run_perf.sh <binary> <input> <threads> [nruns]
Run the serial binary at 1 thread (the speedup baseline) and the parallel binary at
2+ threads. Results go to results/ as .p1, .p2 and (if RAPL is available) .p3 files.
A full sweep:
for inp in 1000000.in 10000000.in; do
./run_perf.sh combsort_serial "$inp" 1 100 # serial baseline
for t in 2 3 4; do
./run_perf.sh combsort_parallel "$inp" "$t" 100
done
doneReplace combsort with each algorithm (or wrap the block in an outer loop over algorithms).
nruns defaults to 100; use a small number (e.g. 10) for a quick smoke test.
Run final measurements on an idle, dedicated machine. RAPL is per-socket, not per-process, so any background load contaminates the energy numbers. Energy is only meaningful for large
N(millions); for tiny inputs it is dominated by start-up noise.
python3 parse_perf.pyProduces:
results/results_raw.csv— one row per run.results/results_summary.csv— mean and std per (algo, input, mode, threads).
python3 plot_perf.py # per-metric plots: results/{by_algo,by_threads,std_*,speedup}/
python3 analyze_perf.py # analysis figures: results/analysis/<topic>/analyze_perf.py needs the summary CSV; the raw CSV is optional and unlocks the
distribution, correlation and reproducibility figures. Energy figures appear only if the
.p3 files were collected.
results/
├── *.p1 / *.p2 / *.p3 # raw perf output per run
├── results_raw.csv
├── results_summary.csv
└── analysis/
├── complexity/ # log-log time & instructions vs N (fitted exponent k)
├── scalability/ # efficiency heatmap, speedup vs threads, Amdahl fraction
├── microarch/ # IPC vs branch/cache miss, normalized counter portrait
├── memory/ # L1/LLC/dTLB pressure
├── distributions/ # boxplots over runs (needs raw)
├── correlation/ # counter-vs-time correlation (needs raw)
├── reproducibility/ # coefficient of variation (needs raw)
└── energy/ # energy & power vs threads, energy-time Pareto,
# EDP/ED2P heatmaps, package-vs-DRAM breakdown
| File | Variable | Purpose |
|---|---|---|
run_perf.sh |
nruns (4th argument) |
Repetitions per configuration (default 100). |
parse_perf.py |
— | Auto-detects numeric vs inputN file names. |
plot_perf.py |
BASELINE_MODE |
Speedup baseline: "serial" or "self". |
plot_perf.py |
CONF_LEVEL |
Confidence level for error bars (default 0.95). |
analyze_perf.py |
FIGFMT |
Figure format, "pdf" (default) or "png". |
analyze_perf.py |
BASE_MODE, BASE_THREADS |
Baseline used for the per-algorithm fingerprints. |
After collecting data, the runner prints two checks. No .p1/.p2 file should contain a
% in parentheses (that would mean multiplexing), and instructions:u / branches:u must
be identical across all runs of the same configuration:
grep -H '%)' results/*.p1 results/*.p2 || echo 'OK: no multiplexed events'
grep -h 'instructions:u' results/<algo>_<mode>_<input>_t<threads>_run*.p1perfpermission denied — the runner usessudo perf; make sure your user can runsudo. If you prefer not to use sudo, lowerkernel.perf_event_paranoid, but RAPL and the NMI-watchdog toggle still need privilege.- Energy columns are all zero / breakdown skipped — your machine does not expose
power/energy-ram/(and maybe notpower/energy-pkg/). Confirm withsudo perf list | grep energy. The pipeline degrades gracefully to package-only or no energy. - Complexity exponent looks wrong — make sure input files are named by their numeric size
(
10000000.in), notinput1/input2. The exponent is fitted against theNparsed from the file name. - Noisy parallel speedup — increase
nruns, run on a dedicated idle machine, and prefer the median over the mean for volatile metrics (long-tailed time distributions).