Introduction

Every time your CPU asks for a byte of data, memory makes it wait. A modern DRAM chip needs tens of nanoseconds just to open a row and deliver its contents — and while it recovers, nothing else can happen. At gigahertz clock speeds that pause is an eternity.

Memory interleaving is the answer the hardware industry settled on decades ago. Instead of one giant bank, the address space is sliced across kk independent banks, each with its own data and control lines. Consecutive addresses land in consecutive banks: address 0 in bank 0, address 1 in bank 1, … address k−1k-1 in bank k−1k-1, then address kk wraps back to bank 0.

The payoff appears the moment you read a sequential burst. While bank 0 is still recovering from the first request, bank 1 already starts serving the second, bank 2 the third, and so on. The banks work in pipeline — and if there are enough of them, by the time bank 0 is asked for its next word it has already recovered. Latency hasn't shrunk at all; throughput has multiplied by kk.

This is the same insight behind CPU pipelines and instruction-level parallelism: overlap independent work so that idle hardware time disappears. Memory interleaving just applies it to the memory bus.

Try It: Burst Access

Choose how many banks you want and how long each bank takes to respond. Hit Run burst to watch a 8-word sequential read complete — either serialized through one bank or pipelined across all of them.

<!-- Demo: interleaved vs single-bank burst read — 8 sequential words -->
<div class="controls">
  <label>Banks:
    <select id="bankCount">
      <option value="1">1</option>
      <option value="2">2</option>
      <option value="4" selected>4</option>
      <option value="8">8</option>
    </select>
  </label>
  <label>Latency (cycles):
    <select id="latency">
      <option value="2">2</option>
      <option value="4" selected>4</option>
      <option value="6">6</option>
      <option value="8">8</option>
    </select>
  </label>
  <button id="runBtn" type="button">Run burst</button>
</div>
<div id="bankViz" class="bank-viz" aria-label="Memory bank visualization"></div>
<div class="result-area">
  <div class="result-row">
    <span class="label-col">1 bank</span>
    <div class="bar-wrap"><div id="barSingle" class="bar bar-single"></div></div>
    <span id="timeSingle" class="time-label">-</span>
  </div>
  <div class="result-row">
    <span class="label-col">k banks</span>
    <div class="bar-wrap"><div id="barInterleaved" class="bar bar-interleaved"></div></div>
    <span id="timeInterleaved" class="time-label">-</span>
  </div>
</div>
<p id="status" class="status"></p>
/* Layout: controls + bank columns + comparison bars */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; color: #222; padding: .6rem; }
.controls { display: flex; flex-wrap: wrap; gap: .6rem; align-items: center; margin-bottom: .8rem; }
label { font-size: .85rem; display: flex; align-items: center; gap: .3rem; }
select { font-size: .85rem; padding: .2rem .4rem; border: 1px solid #aaa; border-radius: 6px; }
button { font: 600 14px system-ui; padding: .4rem .9rem; background: #1d3557; color: #fff;
         border: none; border-radius: 8px; cursor: pointer; }
button:disabled { opacity: .5; cursor: default; }
/* Bank columns — each column shows the addresses that map to it */
.bank-viz { display: flex; gap: 6px; margin-bottom: .8rem; flex-wrap: wrap; }
.bank { border: 1.5px solid #adb1b8; border-radius: 8px; overflow: hidden;
        width: 56px; min-width: 56px; }
.bank-header { font-size: .7rem; font-weight: 700; text-align: center;
               padding: 2px 0; background: #e8eef3; color: #1d3557; }
.bank-cells { display: flex; flex-direction: column; }
.bank-cell { height: 22px; display: flex; align-items: center; justify-content: center;
             font-size: .72rem; font-family: ui-monospace, monospace; color: #444;
             border-top: 1px solid #e0e4e8; transition: background .18s; }
.bank-cell.active { background: #f4a261; color: #222; font-weight: 700; }
.bank-cell.done { background: #52b788; color: #fff; }
/* Comparison bars for single-bank vs interleaved time */
.result-area { margin-bottom: .6rem; }
.result-row { display: flex; align-items: center; gap: .5rem; margin-bottom: .4rem; }
.label-col { font-size: .78rem; width: 80px; flex-shrink: 0; font-weight: 600; color: #444; }
.bar-wrap { flex: 1; background: #eee; border-radius: 4px; height: 18px; overflow: hidden; }
.bar { height: 100%; width: 0; border-radius: 4px; transition: width .4s ease; }
.bar-single { background: #e63946; }
.bar-interleaved { background: #2a9d8f; }
.time-label { font-size: .8rem; width: 52px; flex-shrink: 0; text-align: right; }
.status { font-size: .88rem; font-weight: 600; min-height: 1.3em; color: #1d3557; }
/* Memory interleaving demo — compare single-bank serial access vs k-bank pipelined burst */
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
const WORDS = 8; // number of words in the burst

/* Bank mapping: address a goes to bank (a mod k) */
function bankOf(addr, k) { return addr % k; }

/* Single-bank total time: every word queues behind the previous one */
function singleBankTime(n, lat) { return n * lat; }

/* Interleaved total time: each word starts as soon as its bank is free */
function interleavedTime(n, k, lat) {
  // pipeline: each word i goes to bank (i mod k); banks work in parallel
  const bankFree = new Array(k).fill(0); // next cycle each bank is free to accept a new request
  let last = 0;
  for (let i = 0; i < n; i++) {
    const b = i % k;
    const end = bankFree[b] + lat; // this bank starts immediately when free and finishes one latency later
    bankFree[b] = end;
    last = Math.max(last, end);
  }
  return last;
}

/* Render bank columns with address slots */
function renderBanks(k) {
  const viz = document.getElementById('bankViz');
  viz.innerHTML = '';
  for (let b = 0; b < k; b++) {
    const col = document.createElement('div');
    col.className = 'bank';
    col.id = 'bank-' + b;
    const hdr = document.createElement('div');
    hdr.className = 'bank-header';
    hdr.textContent = i18nFmt('B%n%', { n: b });
    col.appendChild(hdr);
    const cells = document.createElement('div');
    cells.className = 'bank-cells';
    // fill in addresses that map to this bank
    for (let addr = b; addr < WORDS; addr += k) {
      const cell = document.createElement('div');
      cell.className = 'bank-cell';
      cell.id = 'cell-' + addr;
      cell.textContent = i18nFmt('a%a%', { a: addr });
      cells.appendChild(cell);
    }
    col.appendChild(cells);
    viz.appendChild(col);
  }
}

/* Animate the burst access step by step */
async function animateBurst(k, lat) {
  const delay = ms => new Promise(r => setTimeout(r, ms));
  const STEP_MS = 120; // milliseconds per simulated cycle

  // reset all cells to idle
  for (let a = 0; a < WORDS; a++) {
    const el = document.getElementById('cell-' + a);
    if (el) el.className = 'bank-cell';
  }

  // build per-word schedule: when does each address start/end?
  const schedule = [];
  const bankFreeAt = new Array(k).fill(0);
  for (let i = 0; i < WORDS; i++) {
    const b = bankOf(i, k);
    // word i starts when its bank is free
    const start = bankFreeAt[b];
    const end = start + lat;
    bankFreeAt[b] = end;
    schedule.push({ addr: i, bank: b, start, end });
  }
  const totalT = Math.max(...schedule.map(s => s.end));
  for (let t = 0; t <= totalT; t++) {
    for (const s of schedule) {
      const el = document.getElementById('cell-' + s.addr);
      if (!el) continue;
      if (t >= s.start && t < s.end) el.className = 'bank-cell active';
      else if (t >= s.end) el.className = 'bank-cell done';
    }
    await delay(STEP_MS);
  }
}

/* Update comparison bars */
function updateBars(tSingle, tInterleaved) {
  const maxT = Math.max(tSingle, 1);
  document.getElementById('barSingle').style.width = '100%';
  document.getElementById('barInterleaved').style.width = (tInterleaved / maxT * 100) + '%';
  document.getElementById('timeSingle').textContent = i18nFmt('%n% cy', { n: tSingle });
  document.getElementById('timeInterleaved').textContent = i18nFmt('%n% cy', { n: tInterleaved });
}

/* Main run handler */
async function run() {
  const k = parseInt(document.getElementById('bankCount').value, 10);
  const lat = parseInt(document.getElementById('latency').value, 10);
  document.getElementById('runBtn').disabled = true;
  document.getElementById('status').textContent = 'Simulating burst…';
  renderBanks(k);
  await new Promise(r => setTimeout(r, 80));
  const tSingle = singleBankTime(WORDS, lat);
  const tInterleaved = interleavedTime(WORDS, k, lat);
  updateBars(tSingle, tInterleaved);
  await animateBurst(k, lat);
  const speedup = (tSingle / tInterleaved).toFixed(2);
  document.getElementById('status').textContent =
    i18nFmt('%k% banks: %tI% cycles vs %tS% (×%sp% speedup)', { k, tI: tInterleaved, tS: tSingle, sp: speedup });
  document.getElementById('runBtn').disabled = false;
}

document.getElementById('runBtn').addEventListener('click', run);
// show 4-bank layout on load
renderBanks(4);
Result
Console

With a single bank every request must wait for the previous one to finish, so total time grows linearly. With kk banks the requests pipeline: the burst completes in roughly latency+(n−1)⋅⌈latency/k⌉\text{latency} + (n-1) \cdot \lceil \text{latency}/k \rceil cycles instead of n⋅latencyn \cdot \text{latency}. Add more banks and watch the bar for the interleaved case shrink toward the minimum.

The Real Complexity

Interleaving is elegant, but it has a structural Achilles heel rooted in the address mapping itself.

With kk banks, address aa maps to bank a mod ka \bmod k. Sequential access — stride 1 — hits every bank in round-robin order, which is exactly what the design wants. But a stride-kk pattern (addresses 0,k,2k,…0, k, 2k, \ldots) maps every address to bank 0. All requests queue on the same bank; the other k−1k-1 banks sit idle while latency multiplies.

  • Best case (stride 1): throughput ≈k×\approx k \times a single bank, latency effectively hidden.
  • Worst case (stride kk): throughput identical to a single bank — no gain at all.
  • General stride ss: only gcd⁡(s,k)−1⋅k\gcd(s, k)^{-1} \cdot k distinct banks are touched. Choosing kk as a power of two makes strides that are also powers of two catastrophically conflict-prone.

This is why many high-performance systems use a prime number of banks: no non-trivial stride divides a prime, so every regular pattern distributes evenly. The trade-off is that power-of-two address arithmetic becomes slightly messier.

The same tension appears in hash tables and load balancing: a mapping that distributes one access pattern perfectly can be adversarial for another. Good interleaving design — like good hashing — must reason about the distribution of accesses, not just the average case.

Where It Matters

The interleaving idea surfaces in almost every layer of the memory hierarchy:

  • Multi-channel DRAM: modern CPUs ship with two, four, or eight memory channels. Each channel is an independent bank group; the memory controller interleaves across channels to double or quadruple peak bandwidth.
  • GPU memory controllers: graphics cards pair a very wide bus (256–512 bits) with many interleaved GDDR or HBM banks, because rendering needs enormous sequential bandwidth for textures and framebuffers.
  • RAID-0 disk striping: data is striped across drives in fixed-size chunks — exactly interleaving, at disk scale. Sequential reads and writes fan out across all drives simultaneously.
  • Cache banks: on-chip caches are themselves split into banks so that simultaneous load/store instructions can access different banks without stalling each other.
  • Prefetch and cache performance: hardware prefetchers exploit the regularity of stride-1 access to issue requests to future banks before the CPU asks, hiding latency entirely.

Understand interleaving and you understand why doubling memory channels can matter more than buying faster sticks — and why measuring bandwidth with the wrong stride can make a 4-channel system look no better than a 1-channel one.

Conclusion

Memory interleaving is one of the oldest tricks in computer architecture, and it works because it turns an unavoidable wait into overlapping work. Map consecutive addresses to independent banks, and a burst that would take n⋅latencyn \cdot \text{latency} on a single bank shrinks toward just one latency plus a little pipeline fill time.

The catch — stride-kk conflicts — is a reminder that every clever address mapping has a dual: the access pattern that breaks it. The fix, choosing a prime number of banks, is itself a lesson in load balancing: the best distribution scheme is the one that leaves no regularity for an adversary to exploit.

Next time you see "dual-channel" or "quad-channel" on a memory spec sheet, you are looking at interleaving in production — and next time a benchmark delivers surprisingly low bandwidth, a stride mismatch is the first place to look.

Share this article

Pick a channel — or use your device's native share sheet.

Comments

Loading comments...

https://www.kipuhub.com/en/article/memory-interleaving/Content licensed under CC BY-NC 4.0.