"Zifencei": The Instruction-Fetch Fence

Part I Linux boot: required Vol. I (Unprivileged) pp. 45–46 · ~4 min read

  • fence.i
  • instruction-fetch coherence

Chapter 1 granted your fetch unit a sweeping license: prefetch anything, cache it forever, never snoop stores. Zifencei is the other half of that bargain — the one instruction software uses to close the gap. FENCE.I guarantees that instruction fetches after it (in program order, on this hart) observe every data store before it that is already visible to this hart. RISC-V promises no instruction-fetch coherence whatsoever without it.

Two sharp edges:

  1. Strictly local. FENCE.I says nothing about other harts’ fetches. Publishing code to a multiprocessor takes a data FENCE on the writer (so the stores are visible) and then each consuming hart executing its own FENCE.I.
  2. Fetch-before-data ordering is free. An instruction fetch is always ordered before the explicit accesses that instruction performs — you never fence between fetching a load and executing it.
funct12 = 03120rs1 = 019150011412rd = 0117MISC-MEM60FENCE.I
Click a field for its role.

The litmus test

The spec’s own self-modifying-code example (RV32IC): the producer patches a 16-bit instruction, fences its stores, then raises a flag; the consumer reads the flag, executes FENCE.I, then runs the patched instruction. The outcome a0=1, a1=0 — flag seen, patch not seen — is forbidden:

Producer hart

la   t0, patch_me
li   t1, 0x4585
sh   t1, (t0)    # patch_me := c.li a1, 1
fence w, w       # order the flag write
la   t0, flag
li   t1, 1
sw   t1, (t0)    # flag := 1

Consumer hart

la      t2, flag
lw      a0, (t2)
fence.i          # fetch barrier
patch_me:
c.li    a1, 0    # patched to c.li a1, 1

Initially flag = 0. Forbidden: a0=1, a1=0. (Illustrative — real code would loop on the flag before the fence.i.)

What FENCE.I costs, by microarchitecture

The instruction was designed to admit a whole spectrum:

  • Simple core, incoherent I$: flush the fetch pipeline and invalidate the entire instruction cache. Correct, brutal, fine for a first core.
  • IrefillssnoopacoherentD refills snoop a coherent D (or a unified inclusive private L2 invalidates I$ lines on local stores): I and D never diverge — FENCE.I reduces to a fetch-pipeline flush only.
  • Uncached instruction RAM: pipeline flush only, same reason.
  • Worst case — multi-level incoherent I and D hierarchies: complete flushes of both, every level, on every FENCE.I.

Hardware Designer Notes

For a Linux-bootable core the correctness bar is absolute (the kernel patches itself: alternatives, ftrace, eBPF JIT — all end in fence.i via the kernel’s own paths), but the performance bar is low — it executes rarely. Budget accordingly: whole-I$ invalidate + pipeline flush is a perfectly respectable v1. If you later add a coherent front side, the instruction quietly becomes a pipeline flush and your icache-flush state machine retires.

Minimal Linux-boot hart MUST

  • Implement FENCE.I so post-fence fetches see all pre-fence local stores — whatever flushing that takes in YOUR hierarchy
  • Ignore funct12/rs1/rd (reserved) — decode must not trap them
  • Flush the fetch pipeline in every implementation — even fully coherent ones (prefetched stale bytes may be in flight)

MAY simplify / trap-and-emulate

  • Invalidate the whole I$ rather than track anything — correctness over elegance
  • Keep I/D coherent via D$ snooping or an inclusive L2 and make FENCE.I nearly free
  • Ignore the performance question entirely at first: Linux userspace never issues FENCE.I; the kernel and firmware do, rarely

Check yourself — FENCE.I

1.Hart A writes new code, executes FENCE.I, and jumps to it. Hart B jumps to the same code without any action. What's guaranteed?

2.Why did the Linux ABI remove FENCE.I from user level?

3.Your core has incoherent split I/D caches with a private L2 in front of memory. What must FENCE.I do?

3 questions