Skip to content

Fix GCC-12.x prefetch-loop collapse in greedy_search neighbor prefetch - #361

Open
yuejiaointel wants to merge 6 commits into
intel:mainfrom
yuejiaointel:fix/gcc12-svs-neighbor-prefetch
Open

Fix GCC-12.x prefetch-loop collapse in greedy_search neighbor prefetch#361
yuejiaointel wants to merge 6 commits into
intel:mainfrom
yuejiaointel:fix/gcc12-svs-neighbor-prefetch

Conversation

@yuejiaointel

@yuejiaointel yuejiaointel commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

What this fixes

svs::lib::prefetch_l0(std::span) (include/svs/lib/prefetch.h) issues one software prefetch per cacheline to warm the next neighbor vector before the distance kernel reads it, during greedy_search graph traversal.

GCC 12 drops all but the first prefetch from this loop in the built greedy_search hot path — so only the first cacheline of each vector is warmed and the rest load cold from DRAM. Result: a large cache-miss / memory-stall regression in SVS search. GCC 11 and GCC ≥13 are unaffected; Clang and Intel ICX are unaffected.

The loop survives GCC's GIMPLE passes intact in both gcc11 and gcc12 — the collapse is an RTL-backend decision, so #pragma GCC unroll (a GIMPLE-stage directive) does not fix it. A per-iteration std::atomic_signal_fence — standard ISO C++, generates no code at runtime — stops the backend folding the prefetches away and restores the full loop. Portable (no inline asm; no effect on non-GCC or non-x86).

The change

One line in the loop:

for (size_t i = 0; i < num_prefetches; ++i) {
    prefetch_l0(base + CACHELINE_BYTES * i);
    std::atomic_signal_fence(std::memory_order_seq_cst);   // stop GCC 12 dropping the prefetches
}

Impact (measured, cohere-768 fp16 IP, iso-recall 0.95, 5-rep median)

Codegen — prefetches in the fp16-IP greedy_search worker: gcc11 = 7 (stride loop present), gcc12 stock = 3 (collapsed), gcc12+fence = 6–7 (restored).

VecSim standalone, 1 thread (SVS QPS):

gcc11 gcc12 stock gcc12 + fence
946 729 925 (recovers 90% of the gap)

End-to-end Redis, parallel=100 (SVS RPS):

build SVS HNSW HNSW/SVS
gcc11 9332 9550 1.02
gcc12 stock 8251 10120 1.23
gcc12 + fence 9449 9873 1.04

End-to-end, the fence brings SVS back to gcc11 parity (full recovery) and collapses the HNSW-over-SVS gap from 1.23× → 1.04×. HNSW is compiler-insensitive across builds (control), confirming the e2e gap was the SVS prefetch collapse, not an intrinsic HNSW advantage.

Verification

Confirmed by objdump (loop restored) and QPS/RPS at bit-identical recall on real VecSim + RediSearch builds. The regression was also causally isolated by re-adding the prefetch as the only change and observing full recovery. Bug reproduces across all GCC 12.x point releases (12.1–12.4); GCC 11, 13, 14, 15, 16, Clang 17, and Intel ICX 2023 are all unaffected — so an alternative to this patch is simply building with a non-GCC-12 compiler.

svs::lib::prefetch_l0(std::span) issues one software prefetch per cacheline
to warm the next neighbor vector before the distance kernel reads it, during
greedy_search graph traversal.

GCC 12.x (all point releases 12.1-12.4, verified) collapses this counted loop
to a SINGLE prefetch: only the first cacheline of each vector is warmed and the
remaining cachelines are demand-loaded cold from DRAM. GCC 11 and GCC >=13 emit
the full loop. This is a compiler codegen regression, not an SVS logic bug.

Measured impact (VecSim standalone knn_query, cohere-768 fp16 IP, 1 thread,
iso-recall 0.95, node = SPR Xeon 8480L, 5-rep median), gcc11 vs gcc12 vs this fix:
  QPS            744 / 613 / 735   (gcc12 -18%; fix recovers ~93% of the gap)
  L3 misses      24.7M / 856.6M / 24.3M   (gcc12 ~35x; fix back to baseline)
  IPC            0.66 / 0.37 / 0.66
Prefetch instrs in the search worker (objdump): 7 / 3 / 5.
gcc12 runs FEWER instructions yet is slower -> pure memory-latency stall from the
dropped prefetch; restoring the prefetch loop removes it.

Fix: force every iteration's prefetch to be emitted via volatile inline asm on x86;
keep the portable _mm_prefetch path for non-x86 (#if defined(__SSE__)).

Alternative for users: build with GCC != 12.x (11.x or >=13.x are unaffected).
Replaces the x86-only inline-asm workaround with `#pragma GCC unroll` above the
prefetch loop. This keeps the portable _mm_prefetch path (a no-op on non-x86),
so the fix works on ALL architectures, and is a harmless hint on compilers that
don't recognize the pragma. Verified: gcc12 now emits the full prefetch loop
(collapsed -> restored); gcc11/13/15 unaffected; clang still emits prefetches.
The #pragma GCC unroll variant fixes the isolated loop but does NOT survive the
real inlining chain (accessor.prefetch -> SimpleData::prefetch -> lib::prefetch):
the built module's search worker still shows the collapsed prefetch (3, same as
stock gcc12). Only the volatile inline-asm version restores the prefetches in the
actual binary and recovers the perf (measured). Comment updated to document this.
Replaces the x86 inline-asm workaround with a standard ISO C++ signal fence
(std::atomic_signal_fence) inside the prefetch loop. Generates no code at runtime
but stops GCC 12.x's RTL backend from folding away the per-cacheline prefetches.

Why not #pragma GCC unroll: the loop survives GCC's GIMPLE passes intact in both
gcc11 and gcc12 (verified via -fdump-tree); the collapse is an RTL-backend decision,
so the GIMPLE-stage unroll pragma is a no-op on the hot greedy_search clone.

Verified on the real VecSim/SVS module (cohere-768 fp16 IP, 1 thread, iso-recall
0.95418, 5-rep median): restores the gcc11 stride prefetch loop (worker prefetches
3->7) and QPS gcc12 721 -> 930 (gcc11 = 938), recovering ~97% of the gap. Portable
across arches (no asm); Clang/ICX unaffected by the original bug.
@yuejiaointel
yuejiaointel requested a review from mihaic August 8, 2026 00:35

@rfsaliev rfsaliev left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGFM

@mergify

mergify Bot commented Aug 10, 2026

Copy link
Copy Markdown

Tick the box to add this pull request to the merge queue (same as @mergifyio queue).

  • Queue this pull request

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants