Skip to content

Latest commit

 

History

13 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Plumb

Plumb the depth of your IR.

A static cost-model analysis pass for LLVM that knows the difference between an add and a chase through a function pointer.

LLVM C++ CMake macOS Linux License

Plumb pipeline: clang emits LLVM IR, opt loads libPlumb to produce stdout, CSV, and JSON; JSON drives the dashboard which exports a PDF
./build.sh && ./run.sh

./run.sh analyses the testcases, then starts the live dashboard at http://localhost:8420/dashboard.html (so recommendation Compare works). Use ./run.sh --no-open in CI, or ./run.sh --file for a plain file:// open.


Why this exists

A naive instruction count ranks two functions equal if they have the same number of instructions, even when one is a tight integer loop and the other is a chain of memory-bound calls. Plumb assigns a cost to every IR instruction, multiplies it by loop nesting depth, surcharges indirect calls, then traces the worst-case path through every CFG.

Every cost number traces back to (group, count, weight, depth) — no black-box scores.

Real example. On the matmul() from testcases/test_floatmm.c, raw instruction count says 78. Plumb at default weights says 335 — and pinpoints bb.6 (the depth-3 inner accumulator) as the hot block carrying ~45% of total cost. After -O2, that drops to 81 (−76%), with the hotspot moving cleanly out of the inner body. See §3.4 in EVALUATION.md

Validated at scale. Across 83 programs from LLVM's official test-suite (1,165 functions, 166 runs), Plumb's median -O2 cost reduction is 55%, and its top hot function lines up with the canonical kernel name (Jacobi, FFT, kernel_*) on every numerical workload. Full sweep in benchmarks/SUMMARY.md


Table of contents


At a glance

What you get

  • LLVM FunctionPass (legacy PM)
  • Per-instruction classification into 10 groups
  • Configurable weights (config/weights.cfg)
  • Loop-depth cost multiplier
  • Indirect-call ×1.6 surcharge
  • Critical-path discovery (DP over RPO)
  • Cyclomatic complexity (McCabe)
  • Energy estimate (Horowitz pJ/op table)
  • 5 recommendation tags
  • CSV + JSON output
  • Single-file interactive dashboard

Recommendation tags

Tag When
HOTSPOT total cost > threshold
INLINE_CANDIDATE small leaves
VECTORIZABLE loops with no calls
RECURSIVE direct self-call detected
HIGH_COMPLEXITY cyclomatic ≥ 10

Every tag above is implemented, not just detected: ./scripts/validate_recommendations.sh applies the real LLVM transform (or hand-written refactor) each one implies and measures whether it actually helped — see EVALUATION.md §6.

Verified on

  • macOS (Apple Silicon, Homebrew LLVM 14)
  • Ubuntu 22.04 (clang-14 / opt-14)

Architecture

The pipeline is three stages: produce IR, run the analysis pass, render the report. Every stage is decoupled — Plumb.cpp doesn't know about the dashboard, and the dashboard doesn't know about LLVM.

End-to-end pipeline diagram

Inside the pass itself, each function is processed through a fixed sequence of stages:

Per-function pipeline inside the Plumb pass

Cost model in 30 seconds

Cost model: classify, look up weight, apply indirect-call surcharge, multiply by loop depth, sum across basic blocks

The 10 groups (with default weights)

Group Weight Energy¹ pJ/op LLVM opcodes
add 1 0.4 Add, Sub, FAdd, FSub, And, Or, Xor, shifts
mul 2 3.4 Mul, FMul, *Div, *Rem
memory 3 50 Load, Store, atomics, Fence
call 5 100 Call, Invoke (×1.6 if indirect)
branch 1 0.1 Br, Switch, IndirectBr
compare 1 0.1 ICmp, FCmp
cast 1 0.5 every CastInst opcode
alloca 1 5 Alloca
phi 0 0.05 PHI
other 0 1.0 Ret, GetElementPtr, Select, ...

¹ pJ figures inspired by Horowitz, ISSCC 2014 — order-of-magnitude only, not absolute device wattage.

Why static depth and not block frequency? Because every cost number must trace back to (count, weight, depth). A BlockFrequencyInfo multiplier is a heuristic guess; a depth integer is auditable. Full rationale in DESIGN.md §3.


Quick start

Prerequisites

Platform Install command
macOS (Homebrew) brew install llvm@14 cmake
Ubuntu / Debian sudo apt install llvm-14 llvm-14-dev clang-14 cmake

Plumb deliberately targets the legacy pass-manager API (FunctionPass / RegisterPass) which was removed in LLVM 17. Versions 14, 15, and 16 are supported. The build script auto-detects all three.

Build and run

./build.sh        # -> build/libPlumb.{so,dylib}
./run.sh          # -> ir/, results/, opens dashboard
./run.sh --no-open   # skip browser launch (CI-friendly)

If LLVM auto-detection ever fails, pin a specific install:

LLVM_DIR=$(brew --prefix llvm@14)/lib/cmake/llvm ./build.sh   # macOS
LLVM_DIR=/usr/lib/llvm-14/lib/cmake/llvm        ./build.sh   # Ubuntu

What you'll see

pass built: build/libPlumb.dylib
-- test_floatmm @ -O0 -----------------------------------
[Plumb] Loaded weights from: config/weights.cfg
+==========================================================+
  Plumb  >>  Function: matmul
+==========================================================+
  Total weighted cost     : 335
  Loop count / max depth  : 1 / 3
  Most expensive group    : memory (cost=246)
  Critical Path (worst-case):  cost = 264
    bb.0 -> bb.1 -> bb.2 -> bb.3 -> bb.4 -> bb.5 -> bb.6 -> bb.7
  Recommendations         : HOTSPOT
  *** HOTSPOT WARNING: cost 335 exceeds threshold 30 ***

...then the dashboard opens with twelve JSON reports ready to load.


Testcase suite

Six C programs, each engineered to stress a different cost class. Each is run at both -O0 and -O2, producing 12 reports.

# Testcase Stresses Expected dominant group Top-line at O0
1 test_arith.c mixed arithmetic + nested loops + calls spread 493
2 test_branchy.c switch + nested ifs (high cyclomatic complexity) branch + mem 375
3 test_callchain.c direct + indirect (function pointer) calls call 231
4 test_floatmm.c triple-nested float matmul (depth 3) memory 514
5 test_memheavy.c 5-point stencil + indirect-load reduction memory 357
6 test_recursive.c self-recursion + leaf functions call 164

Per-testcase findings, per-function deltas, and the failure cases live in EVALUATION.md.

A peek at matmul (test_floatmm.c, -O0)

The depth-3 inner loop (bb.5 → bb.6 → bb.7) carries the cost. bb.6 alone is 150 / 335 = 45% of the function — exactly because every load gets a ×3 depth multiplier on top of its weight 3.

Control-flow graph of matmul highlighting the depth-3 hottest block bb.6 and the critical path through bb.0 to bb.7

Plumb on real code

The six testcases in testcases/ are designed to stress specific cost classes. To validate Plumb against actual workloads, benchmarks/ sweeps the official LLVM test-suite SingleSource/Benchmarks subset (Stanford, Misc, Polybench, Shootout).

   ┌────────────────────────────────────────────────────────────┐
   │   83 programs · 1,165 functions · 166 runs (O0 + O2)       │
   │                                                            │
   │   Median cost reduction at -O2     ─→   55 %               │
   │   Programs whose hottest fn          memory-dominated 99 % │
   │   Inliner-inflation failure mode    ─→   4 % of programs   │
   │                                                            │
   │   Top-3 hottest at -O0:                                    │
   │     1.  Misc/himenobmtxpa  →  jacobi          (cost 10669) │
   │     2.  Misc/oourafft       →  cftmdl          (cost  3181) │
   │     3.  Polybench/deriche  →  kernel_deriche  (cost  1587) │
   └────────────────────────────────────────────────────────────┘

Plumb's hot-function detection lines up with what an HPC engineer would call out as the kernel: Jacobi solvers, FFT inner loops, Polybench's kernel_* functions all surface at the top of the cost ranking. The full report — top-15 hottest programs, per-suite breakdown, and the 3 programs where -O2 actually increased reported cost (the failure mode from EVALUATION.md §5.1 confirmed in the wild) — lives in benchmarks/SUMMARY.md.

To reproduce:

./build.sh
./benchmarks/fetch.sh            # one-time sparse-clone of test-suite
./benchmarks/run_bench.sh        # ~20 s end-to-end on Apple Silicon
python3 benchmarks/analyze.py    # regenerates SUMMARY.md

The dashboard

Open dashboard/dashboard.html. Single self-contained HTML, no build step, CDN-loaded libs (Chart.js, Cytoscape.js, html2canvas, jsPDF).

Recommendation validation — live, against whatever you load

./scripts/validation_server.sh   # opens http://localhost:8420/dashboard.html

Pick any already-analyzed program from the header, or upload a brand-new .c file the project has never seen — the dashboard compiles it, runs Plumb, and every recommendation tag already shown in "Optimisation Recommendations" grows a ▶ Validate button right on the chip. Click one and the dashboard shells out to the real opt (-always-inline, -loop-vectorize, -tailcallelim) against that exact function, live, and the result lands in "Recommendation Validation" right below. This is not replayed data: below, main from test_floatmm.c gets vectorized live and its cost actually drops 112 → 78 (−30%) — a different, better outcome than the fixed case study's matrix_add example, because it's a different function, measured for real.

Dashboard's Optimisation Recommendations cards with clickable Validate buttons on each tag, and the Recommendation Validation section below showing a live result: main getting vectorized (12 vector instructions, cost 112 to 78, -30%)

Opened without the server (just double-clicking the HTML file), the same section still works, but falls back to a clearly-labeled fixed case study — EVALUATION.md §6 rendered, covering Plumb's own 3 testcases only. It never pretends to reflect a file it hasn't actually analyzed.

The rest of the dashboard is the per-run analysis tooling, populated after loading a results.json (or automatically, once you pick/upload a program above):

KPI strip

Total cost, functions analysed, hottest function, estimated energy. Shows A↔B deltas when both runs are loaded.

Cost donut

Aggregate cost per instruction group. Single-hue ramp for readability without competing colors.

Function ranking

Horizontal bars. Click any bar to refocus the BB heatmap and CFG graph on that function.

BB heatmap

One cell per basic block. Color depth = weighted cost. Critical-path cells are outlined.

CFG graph

Full control-flow graph (Cytoscape). Node size = cost. Critical-path nodes are highlighted.

Live weight tuner

Drag any slider, every chart re-renders client-side. Explore "what if memory was free?".

Comparison mode

Load A = O0, B = O2. Diff cards per function. Reductions in green, regressions in red.

Recommendations

Per-function chips: HOTSPOT, INLINE_CANDIDATE, VECTORIZABLE, RECURSIVE, HIGH_COMPLEXITY.

Sortable table and PDF

Every function with cost / insts / CC / depth / energy / crit-path. One-click PDF export.

Demo screenshots live in docs/screenshots/.


Output formats

Terminal — ASCII tables, BB cost bar chart, critical path
+==========================================================+
  Plumb  >>  Function: matmul
+==========================================================+

  Instruction-Type Analysis:
  +----------+-------+--------+--------+--------------+
  | Group    | Count | Weight |  Cost  | Contribution |
  +----------+-------+--------+--------+--------------+
  | memory   |  38   |   3    | 246    |    73%       |
  | branch   |  12   |   1    |  22    |     6%       |
  | mul      |   3   |   2    |  16    |     4%       |
  | ...                                              |
  +----------+-------+--------+--------+--------------+

  BasicBlock cost bar chart:
  bb.6  |##############################| 150  [CRIT]
  bb.8  |#########                     |  46
  bb.0  |####                          |  24
  ...

  Critical Path (worst-case):  cost = 264
  bb.0 -> bb.1 -> bb.2 -> bb.3 -> bb.4 -> bb.5 -> bb.6 -> bb.7
CSV — for spreadsheets / awk / CI assertions
function,group,count,weight,cost,pct
matmul,memory,38,3,246,73.4
matmul,branch,12,1,22,6.6
matmul,mul,3,2,16,4.8
matmul,call,1,5,15,4.5
matmul,add,6,1,14,4.2
...
JSON — structured, dashboard-ready
{
  "metadata": {
    "tool": "Plumb", "runLabel": "O0",
    "weights":      { "add":1, "mul":2, "memory":3, "call":5 },
    "energyModelPj": { "add":0.4, "mul":3.4, "memory":50 },
    "totals":       { "totalCost": 514, "functionCount": 3 }
  },
  "functions": [
    {
      "name": "matmul",
      "totalCost": 335,
      "totalInstructions": 78,
      "cyclomaticComplexity": 4,
      "maxLoopDepth": 3,
      "loopCount": 1,
      "isRecursive": false,
      "energyPj": 4487.9,
      "mostExpensiveGroup": "memory",
      "recommendations": ["HOTSPOT"],
      "criticalPath":     ["bb.0","bb.1","bb.2","bb.3","bb.4","bb.5","bb.6","bb.7"],
      "criticalPathCost": 264,
      "groups":      [ /* per-group {count,weight,cost,pct,indirect} */ ],
      "basicBlocks": [ /* per-BB {label,cost,instructions,loopDepth,isCritical,successors} */ ]
    }
  ]
}

The dashboard's Live Weight Tuner re-derives all costs client-side from groups[].count — which is why the JSON emits counts and weights separately rather than only the multiplied result.


Pass options

All flags use the plumb- prefix to avoid clashing with LLVM's own command-line options (notably the inliner's built-in -inline-threshold, which would trip CommandLineParser::addOption at dlopen time without the prefix). Full story in IMPLEMENTATION.md §8

Flag Default Purpose
-plumb-weight-file=PATH (built-in) Path to key=value weight table
-plumb-hot-threshold=N 30 Functions with cost > N get HOTSPOT
-plumb-inline-threshold=N 20 Cost < N (and not main) gets INLINE_CANDIDATE
-plumb-out-file=PATH Write CSV results
-plumb-json-file=PATH Write structured JSON
-plumb-run-label=STR default Embedded in JSON metadata (O0 / O2 for compare)

Manual invocation (without run.sh)

opt -enable-new-pm=0 \
    -load build/libPlumb.dylib \
    -plumb \
    -plumb-weight-file=config/weights.cfg \
    -plumb-hot-threshold=30 \
    -plumb-inline-threshold=20 \
    -plumb-run-label=O0 \
    -plumb-out-file=results.csv \
    -plumb-json-file=results.json \
    -disable-output input.ll

Repository layout

plumb/
├── README.md              <- this file
├── LICENSE                <- MIT
├── build.sh               <- compiles the pass
├── run.sh                 <- runs analysis, opens dashboard
├── src/
│   ├── Plumb.cpp          <- the LLVM pass (~890 LoC)
│   └── CMakeLists.txt     <- LLVM-14/15/16 build glue
├── config/
│   └── weights.cfg        <- editable cost table
├── testcases/             <- 6 C programs, each stressing a different class
│   ├── test_arith.c
│   ├── test_branchy.c
│   ├── test_callchain.c
│   ├── test_floatmm.c
│   ├── test_memheavy.c
│   └── test_recursive.c
├── benchmarks/            <- Plumb sweep across 83 LLVM-test-suite programs
│   ├── README.md
│   ├── fetch.sh           <- sparse-clone llvm-test-suite
│   ├── run_bench.sh       <- compile + analyse every program
│   ├── analyze.py         <- aggregate JSONs into SUMMARY.md
│   └── SUMMARY.md         <- the headline report (auto-gen, committed)
├── dashboard/
│   └── dashboard.html     <- interactive UI (single file, CDN libs)
├── scripts/
│   ├── _llvm_env.sh                  <- shared LLVM toolchain detection
│   ├── _plumb_lib.py                 <- shared IR-transform helpers (both scripts below import this)
│   ├── validate_recommendations.sh   <- fixed case study: applies + measures all 5 tags on Plumb's own testcases
│   ├── validate_recommendations.py   <- IR surgery + JSON analysis behind the above
│   ├── validation_server.sh          <- live validation: local HTTP server for the dashboard
│   └── validation_server.py          <- compiles/analyzes uploads, runs transforms on-demand
└── docs/
    ├── DESIGN.md          <- approach, alternatives, tradeoffs
    ├── IMPLEMENTATION.md  <- LLVM-API specifics, build details
    ├── EVALUATION.md      <- measured results across 6 testcases
    ├── CHANGELOG.md       <- bugs found and fixed, each with a reproducible before/after
    ├── diagrams/          <- README diagrams (matplotlib-rendered PNGs)
    └── screenshots/       <- demo captures

After running ./run.sh, a few more directories appear (all .gitignored):

build/                          compiled pass library
ir/                             LLVM IR for every testcase x {O0,O2}
results/                        one CSV + one JSON per (testcase, opt-level) pair
validation/                     IR/JSON from ./scripts/validate_recommendations.sh
uploads/                        programs analyzed live via ./scripts/validation_server.sh
benchmarks/llvm-test-suite/     sparse clone of llvm/llvm-test-suite (~30 MB)
benchmarks/ir/                  IR for every benchmark program
benchmarks/results/             166 JSONs (one per program × opt-level)

Documents

Doc What's inside Read it for
README.md This file overview, quick start, dashboard tour
docs/DESIGN.md 10 sections approach, alternatives considered, tradeoff decisions, what's out of scope
docs/IMPLEMENTATION.md 11 sections LLVM API specifics, classification table, the macOS dynamic_lookup linker workaround, the flag-prefix collision story
docs/EVALUATION.md 7 sections per-testcase findings, three-model baseline comparison, §5 four honest failure modes, §6 recommendation validation
docs/CHANGELOG.md 3 entries every bug found in the pass/scripts/dashboard, each with a reproducible before/after
benchmarks/SUMMARY.md auto-gen Plumb across 83 LLVM test-suite programs (1,165 functions, 166 runs)

Tech stack

Layer Stack
Pass C++14, LLVM 14 (legacy PM), CMake ≥ 3.13
Config key=value text, hot-reload via dashboard
Output stdout (ASCII), CSV, JSON
Dashboard vanilla JS, Chart.js, Cytoscape.js, html2canvas, jsPDF (all CDN, cached)
Portability bash 3.2 compatible scripts; LLVM auto-detect on $PATH + Homebrew + apt

Verified end-to-end on:

  • macOS — Apple Silicon, Homebrew LLVM 14, bash 3.2
  • Linux — Ubuntu 22.04, clang-14 / opt-14, bash 5

License

MIT. See LICENSE.


Plumb the depth of your IR — one critical path at a time.

Releases

Packages

Contributors

Languages