Portable compiler-assisted memory traffic characterization.
ZRay reports how much memory traffic a marked region of your code generates, on which threads, and at what rate — without hardware performance counters and without the slowdown of binary instrumentation.
Its key idea is to separate what can be known statically from what must be counted at runtime. An LLVM IR pass summarizes the memory-relevant instruction mix of each developer-marked region; lightweight sampled counters recover how many times each block actually executed. Multiplying the two recovers per-region loads, stores, bytes moved, and bandwidth. Because no per-memory-access instrumentation is inserted, overhead stays low (geomean under 10%) while byte accuracy stays high (geomean 93%).
This is part of the research artifact for the IISWC 2026 paper ZRay: Portable Compiler-Assisted Memory Traffic Characterization.
| OS | Linux (x86-64 tested; AArch64, RISC-V, POWER8 demonstrated) |
| LLVM | 15 — llvm-15-dev and clang-15. A stock upstream LLVM is sufficient. |
| Build | make, a C++14 host compiler |
| Optional | gem5 (simulator runs), perf/PMU access (-DUSE_HW_PERF_COUNTERS) |
On Debian/Ubuntu:
sudo apt install llvm-15-dev clang-15No custom LLVM is required. Earlier versions of ZRay used a patched clang that added a
#pragma begin_instrument. Regions are now marked with ordinary inline asm macros (see Marking regions), which produce identical IR, so a stock toolchain works. ZRay pins LLVM 15 because the IR pass uses the legacy pass manager.
git clone https://github.com/multifacet/ZRay/ zray && cd zray
./setup.sh # or: LLVM_BIN=/path/to/llvm/bin ./setup.shsetup.sh locates LLVM, sources setupEnv.sh, and builds into ./bin:
| Artifact | |
|---|---|
bin/libzray.so |
the ZRay LLVM IR pass |
bin/zray_runtime.ll |
the ZRay runtime, linked into instrumented programs |
bin/zray_post_process |
offline reader for host-mode logs |
bin/libzray_noinline.so |
helper pass (marks ROI functions noinline) |
To build by hand instead:
export LLVM_BIN=/usr/lib/llvm-15/bin
. ./setupEnv.sh
make zrayMark a region, then build it through the four-step pipeline:
// kernel.cc
#include "zray.h"
#include <cstdlib>
void kernel(double *a, double *b, int n) {
ZRAY_BEGIN(15);
for (int i = 0; i < n; i++)
a[i] = b[i] * 2.0;
ZRAY_END(15);
}
int main() {
const int n = 100000;
double *a = (double *)malloc(n * sizeof(double));
double *b = (double *)malloc(n * sizeof(double));
for (int i = 0; i < n; i++) b[i] = i;
kernel(a, b, n);
return a[n - 1] == 0.0;
}. ./setupEnv.sh
export ZRAY_LOGFILE=$PWD/run.zlog # required: where the pass writes region metadata
# 1. source -> LLVM IR
$CUSTOM_CC -o tmp.ll kernel.cc -std=c++14 -O2 -S -emit-llvm \
-Xclang -disable-O0-optnone -I./include
# 2. run the ZRay pass
$CUSTOM_OPT -enable-new-pm=0 -O2 -mem2reg -load ./bin/libzray.so -zray -S \
< tmp.ll > instrumented.ll
# 3. link the ZRay runtime in
$CUSTOM_LINK -o linked.ll instrumented.ll ./bin/zray_runtime.ll
# 4. assemble
$CUSTOM_CC -o kernel linked.ll -std=c++14Run it, and ZRay writes zray_application_stats.csv next to the binary:
./kernelThe CSV has two Region rows here. At -O2 clang inlines kernel into main, so the
work is reported under main, and the kernel row is nearly empty:
Function Loads Stores Read Bytes Written Bytes
main 99992 49996 1199904 799936
_Z6kernelPdS_i 1 1 8 8
Written Bytes matches the expected 100000 × 8 = 800 KB. Stores is about half the
iteration count because the loop vectorized — two doubles per store instruction — which is
exactly the kind of thing ZRay is meant to surface.
Reading the CSV: expect several rows. At
-O2clang may inline the marked function into its caller, in which case the callee's region reports zeros and the traffic appears under the caller's region. Look at theEntry type = Regionsummary rows first; theBasic Blockrows break the same totals down per block, and many of them are legitimately zero.
A worked example lives in examples/ — make -C examples builds bin/mem.
Include zray.h and wrap the code you care about:
ZRAY_BEGIN(group_id);
...
ZRAY_END(group_id);Both macros expand to a volatile inline asm comment (#ZRAY_ROI_BEGIN <id>). They emit
no machine code — they are assembler comments the IR pass scans for — but they are not
optimized away.
The group ID labels a region. Regions may share an ID to form a group. Regions can nest; ZRay tracks the enclosing region.
Marking must be lexically balanced within a function. Regions spanning function boundaries are not supported — mark inside the callee instead.
| Variable | ||
|---|---|---|
ZRAY_LOGFILE |
required | Path where the pass writes region metadata and the runtime reads it back. |
ZRAY_SAMPLE_RATE |
default 1 |
Instrument every Nth region execution. 1 = every execution; 100 = every hundredth; 0 = disable. Higher values cut overhead; see the paper's sampling study. |
ZRAY_BIN_PATH |
set by setupEnv.sh |
Output directory for build artifacts. |
LLVM_BIN |
default /usr/lib/llvm-15/bin |
Which LLVM install to build against. |
GEM5_PATH |
optional | gem5 checkout, for the *_gem5 make targets. |
ZRAY_INST and ZRAY_PATCH_ID are read by the runtime but are leftovers from an earlier
XRay-based design; the handlers they configured are commented out. Treat them as
non-functional.
Passed to opt after -zray:
| Option | Default | |
|---|---|---|
--postdomset |
true |
Share one counter across blocks that provably execute equally often (post-dominator sets). |
--loophoist |
true |
Hoist a counter out of a loop and multiply by its trip count instead of counting each iteration. |
--functionclone |
true |
Clone functions called from a region so their traffic is attributed to that region. |
--full-scan |
false |
Instrument every function, ignoring region markers. |
--hostmonitor |
false |
Enable host-thread monitoring. |
--mirpass is accepted but inert; the machine-level pass it enabled has been removed.
zray_application_stats.csv, one row per region entry per thread iteration:
| Columns | |
|---|---|
App name, Thread Iter, Entry type, Region, Group ID, Counter Index, Function |
identity of the row |
Time Elapsed (ns) |
wall time in the region |
Total Insns, Counter Insns, ZRay Counters Incremented |
instruction totals and instrumentation cost |
Estimated Load BW (MB/s), Estimated Store BW (MB/s) |
achieved data rates |
Read Bytes, Written Bytes |
traffic volume |
Loads, Stores, Int Insns, FP Insns, Cast Inst |
instruction mix |
Global/Stack/Heap Read, Global/Stack/Heap Write |
traffic split by memory class |
Intrinsic Load, Instrinsic Store |
traffic from intrinsics (memcpy, vector ops) |
Entry type distinguishes two kinds of row: Basic Block rows give per-block detail, and
Region rows give the summary for a whole region. Start with the Region rows.
NA appears where a quantity was not measured — for example timing on a per-basic-block
row, where only whole-region timing is meaningful.
source (ZRAY_BEGIN/END)
│ clang -S -emit-llvm
▼
LLVM IR ── opt -zray ──▶ instrumented IR ── llvm-link zray_runtime.ll ──▶ binary
│ │
│ writes region metadata runtime counters
▼ ▼
$ZRAY_LOGFILE ─────────────────── combined at exit ────────────▶ stats CSV
The pass finds region markers, builds a post-dominator tree over each region's blocks, and places the smallest set of counters that still lets every block's execution count be recovered. For each block it records the static instruction mix. At exit, the runtime multiplies recorded counts by that mix and writes the CSV.
Source layout:
src/zray_pass.cc |
pass entry, region discovery, instrumentation driver |
src/zray_parse.cc |
IR parsing, per-block static instruction mix |
src/zray_loop.cc |
loop analysis, trip counts, hoisting |
src/zray_codegen.cc |
emits counter increments and timing events |
src/zray_dyn.cc |
runtime: counters, timing, sampling, CSV output |
src/post_process.cc |
offline log reader |
include/zray.h |
ZRAY_BEGIN / ZRAY_END |
- LLVM 15 only. The pass uses the legacy pass manager (
-enable-new-pm=0). - Regions must be lexically balanced within one function.
- Estimates, not measurements. Byte counts come from static mix × dynamic counts, so they reflect instruction-level traffic, not cache or DRAM traffic. Geomean accuracy is 93%; see the paper for the distribution.
- Sampling trades accuracy for overhead.
ZRAY_SAMPLE_RATE > 1captures roughly a proportional share of traffic. ZRay always samples the first execution of a region. - Inlining moves attribution. If the compiler inlines a marked function into its caller, the traffic is reported under the caller's region and the callee's region reports zeros. This is correct — the work really did happen in the caller — but it surprises people reading per-function rows.
- Indirect calls are attributed through function cloning, which cannot resolve every target.
- Some counting paths assume a single region of interest.
src/, include/ |
the pass and runtime |
examples/ |
small programs demonstrating region marking |
scripts/, util/ |
experiment and analysis helpers |
spec-cfg/ |
SPEC CPU 2017 config (SPEC itself is not redistributable) |
If you use ZRay, please cite the IISWC 2026 paper. See CITATION.cff.