Why this mattered
E-WarP predicts how much an application's execution time will slow down under a given DRAM bandwidth budget, and that prediction is only worth something if it holds under real contention. Validating it meant running an application alongside an interference source and checking whether the measured slowdown matched the predicted envelope.
The problem was the interference source itself. Tools like stress-ng, STREAM, or lmbench are built to measure the best achievable bandwidth a memory subsystem can sustain, which mostly means long sequential accesses, the pattern DRAM is happiest to serve. That's close to the opposite of what a worst-case validation needs. If my interference generator quietly produced DRAM-friendly traffic, I'd be validating E-WarP against a case that was never the hard one, and every run would look better than it should.
I needed something a generic memory benchmark doesn't expose as a knob: a way to choose exactly how adversarial the traffic pattern was, and get the same severity back on every run.
The idea
DRAM bandwidth isn't one number. A DRAM chip keeps one row per bank open in a row buffer; accessing an already-open row is cheap, but switching to a different row in the same bank means precharging the old row and activating the new one first. Sequential access patterns tend to stay inside an open row — that's why they're fast. A pattern that keeps bouncing between rows in the same bank forces a row conflict on nearly every access, which is close to the most expensive traffic DRAM can produce.
So instead of writing a loop that just touches a lot of memory, I wrote one that deliberately walks addresses so consecutive accesses land in different rows of the same bank. That turns the interference source from "generates a lot of bandwidth" into "generates a specific, understood, repeatable class of worst-case traffic" — which is what a validation experiment actually needs.
Interesting implementation
Huge pages, so the address math is actually true
Targeting a specific bank and row means reasoning about physical addresses, but user-space code only ever computes virtual ones, and a regular 4KB mapping gives the OS complete freedom over which physical page backs it. If the bits my code relies on to select a row don't survive virtual-to-physical translation, the whole geometry model is fiction — the buffer would still generate traffic, just not the pattern I'd designed.
buffer = (volatile unsigned char *)mmap(0, BUFSIZE, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS |
MAP_HUGETLB | MAP_HUGE_2MB, -1, 0);A 2MB huge page guarantees the low 21 bits of virtual and physical address are identical, which covers every bit this benchmark touches to pick column, bank, and row. That's the difference between a buffer that probably stresses the intended rows and one that provably does — worth the extra setup (reserving a hugepage pool ahead of time) for an experiment where the pattern is the whole point.
Turning the DRAM address map into arithmetic
Once you've worked out how the platform's DDR controller maps a physical address to column, bank, and row select lines, generating a chosen access pattern stops being DRAM-specific knowledge and becomes ordinary pointer arithmetic.
#define COL_BITS (10)
#define BANK_SHIFT (COL_BITS)
#define BANK_BITS (3)
#define ROW_SHIFT (COL_BITS+BANK_BITS)
#define ROW_BITS (15)
#define NEXT_ROW (1 << ROW_SHIFT)Stepping a pointer by NEXT_ROW changes only the row-address bits and leaves bank and column untouched. The DRAM datasheet's address decoding becomes a single shift, with no per-access lookup or branching required.
Pinning bank and column, sweeping only the row
volatile unsigned char *ptr = buffer + col_offset;
while (ptr < end) {
*ptr = j;
ptr += NEXT_ROW;
}Since col_offset only ever varies within the low column and bank bits, and NEXT_ROW starts exactly at the row-address bit, every step of this loop lands in a different row of the same bank at the same column offset. Nearly every access forces the controller to close whatever row is open and activate a new one — a row conflict on demand, rather than one you hope shows up often enough. The outer loop then sweeps that starting offset across every column and bank, so a full pass forces conflicts across the entire address range instead of one lucky bank.
Making the process itself part of the control
sp.sched_priority = prio;
sched_setscheduler(0, SCHED_FIFO, &sp);
/* ... */
mlockall(MCL_CURRENT | MCL_FUTURE);A carefully designed access pattern doesn't help if the process generating it gets descheduled mid-burst or stalls on a page fault — either one interrupts the interference at a moment I didn't choose, and the resulting contention signal stops being repeatable. Running at SCHED_FIFO priority and locking all pages resident turns the benchmark itself into a controlled variable, not just its memory access pattern.
Takeaway
Stress-testing and deliberately provoking a specific worst case are different skills. The first only needs volume; the second needs to understand the state machine you're fighting — here, the DRAM row buffer — well enough to write arithmetic that reliably lands on its worst transition. It's also a narrow tool by design: the geometry constants are specific to one platform's DDR controller, and the whole approach leans on huge-page guarantees that a 4KB mapping wouldn't give me. That specificity was the point — it's an instrument built for one experiment, not a general-purpose benchmark, and pretending otherwise would have made it worse at the one thing it needed to do.