Skip to content

issues: a userland forward copy a short way above its source modulo 4 KiB stalls on AMD - #1198

Merged
Japabu merged 2 commits into
mainfrom
memcpy-alias-finding
Nov 10, 2026
Merged

Japabu merged 2 commits into
mainfrom
memcpy-alias-finding

Conversation

@Japabu

@Japabu Japabu commented Nov 5, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

#1133 measured a real stall in userland's memcpy on the AMD bench (Zen 2). It is closed, because round 12's binding outcome rejected every routed build. The stall it measured, and the cost that sank its fix, need a record so they don't vanish with the PR.

Change

Record-only: adds issues/a-userland-forward-copy-a-short-way-above-its-source-modulo-4-kib-stalls-on-amd.md (kind: defect). It holds:

  • the vendor fact (56305 rev. 3.02 §2.12 p. 39), our inference from it, and the window as measured;
  • the AMD stall, from 256 B to 64 MiB, with the T14's unexplained readings;
  • what libc: a forward copy a short way above its source modulo 4 KiB copies down #1133's descending copy fixed;
  • the cost every routed build carried on the 256 KiB placement rows: about 7%, with or without the AVX loop. Moving base's memcpy did not reproduce it on those rows. On mem.memcpy.262144 it did once (round 10: 0.80 of base) and did not in round 12, so that row is recorded as not a clean witness;
  • the revision that carries the copyat instrument, b76abc3 on amd-memcpy-ab8;
  • an exit: the stall bound from 256 B to 1 MiB, with the placement median no more than 5% below main's in at least 4 of 5 paired boots. That is stricter than round 12's rule, which cleared an arm unless it regressed in at least 4 of 5 blocks.

Checks

  • cargo test --lib -- sourcegate::tests::no_tracked_file_identifies_a_machine_or_its_network at fe18b74: exit 0 (log).
  • No code changes, so no host CI or guest run.
  • Every number in the file cites its round on ToyOSOrg/evidence.

🤖 Generated with Claude Code

… KiB stalls on AMD

#1133 measured libc's forward rep movsb stalling on the AMD bench (Zen 2)
when the destination sits 1-31 bytes above the source modulo 4 KiB: 3x at
256 B, 22x at 8 KiB, 3.5x at 1 MiB. 56305 rev. 3.02 §2.12 explains why: it
judges store-to-load forwarding by address bits 11:0.

Its route through a descending loop fixed the stall. Round 12, whose outcome
was binding, closed #1133 instead: every routed build, with or without the
AVX loop, ran never-routed 256 KiB copies about 7% slower in 5 of 5 paired
boots. A base whose memcpy was merely moved did not, so the cost is in the
routed code and its mechanism is unknown. This records the stall, the cost
and an exit for a future approach.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu

Japabu commented Nov 10, 2026

Copy link
Copy Markdown
Collaborator Author

Review, round 1: head 77adbf4 against main

Assessment. Record-only: one new file, +68 lines, no production or test code. Recording the stall and the cost that closed #1133 is right, and the record sits where it belongs (issues/, one file, slug equal to its heading). Most of it checks out against the published evidence. Three statements do not, and each is a sentence or two to fix. No build or guest run is owed.

"The record" below is issues/a-userland-forward-copy-a-short-way-above-its-source-modulo-4-kib-stalls-on-amd.md.

What I checked and found correct

  • Format (issues/README.md): status: open with kind: finding is a permitted pair; opened: 2026-11-06 is the commit's date; no severity, correctly, since performance is not a flag; owner and exit are present. Round 12's rule, written before the run, names the outcome "a finding", so the kind is not the implementer's invention.
  • Table, against amd-memcpy/round-5-ab/table.txt, round-8/ab/judgement.md and round 12's raw B boots: 256 B 30/90, 1 KiB 31/270, 2 KiB 40/511, 4 KiB 60/992, 8 KiB 90/1953 (rounds 8-12) to 2615 (round 5), 64 KiB 962/20449, 1 MiB 93.1-93.7/330 µs, 64 MiB 10.94/21.7 ms. All match. The headline 3x, 22x and 3.5x follow.
  • Fix figures: 8 KiB at d = 16 in 80 ns and 1 MiB in 20.9 µs (round 12, H); the SSE2-only arm's 140 ns at 8 KiB (round 12, N, every routed row).
  • Round 12: binding, twenty boots in five rotated blocks; H regresses in 5 of 5 (0.930, 0.928, 0.798, 0.935, 0.922), N in 5 of 5 (0.939, 0.935, 0.892, 0.887, 0.928), S in 0 of 5 (1.025-1.034); outcome "close libc: a forward copy a short way above its source modulo 4 KiB copies down #1133; record the measured stall and the 256 KiB cost as a finding". "About 7%" and "1.03 of base" are fair. mem.memcpy.262144 at 0.84-0.86 with the AVX loop holds in rounds 8, 9, 10, 11 and 12.
  • T14 figures: 714-773 ns at d = 16-64, 206 at 256, 74 at d = 0 (round 5 base).
  • Vendor document, read from AMD's own PDF (56305-3.02.pdf): publication 56305, rev. 3.02, March 2020, §2.12 Load-Store Unit, printed page 39. The quoted words are verbatim, and the paraphrase of the advice is accurate.
  • Code references: copy_forward in sdk/libc/src/arch/x86_64.rs is one rep movsb; the kernel's copy goes down inside a 32-byte window on AMD (kernel/src/arch/x86_64/copy.rs).
  • Identifiers: none. The file holds no MAC, address, serial or hostname shape. The two CPU model names are already in the tree and the manual. The evidence extracts I opened carry none either.

BLOCKER

  1. The record:44-48 — the 256 KiB mem.memcpy row is reported as cleaner than the cited rounds show, and the exit then judges by it.

    • The record says a base whose memcpy was moved "did not show it (round 12: 1.03 of base)". That holds in round 12, and for the placement rows in round 10 as well.
    • Round 10, which the record cites for this cost, shows the opposite on mem.memcpy.262144: the shifted base S, byte-identical memcpy at the head's address, read 32030-35364 MB/s in 3 of 3 boots, 0.80 of B and lower than the head itself (amd-memcpy/round-10/ab/judgement.md). Round 12's judgement says so in terms: "this round, unlike round 10, the shifted base reads base's level".
    • An open defect already records that row with a 45% unexplained run-to-run range on main (issues/a-small-memcpy-on-the-amd-bench-runs-at-as-little-as-six-tenths-of-linuxs.md). Round 5's base read 19390 and 29365 on it.
    • Consequence: the exit binds a future change to "every mem.memcpy.* row within 5% of main's", and a reader is told that an inert shift does not move that row. The evidence says it moved a fifth once. What is established is narrower: the placement rows' cost follows the routed code in rounds 10 and 12.
    • Fix: state round 10's reading on that row beside round 12's, and name the existing defect. Then either keep the row in the exit knowingly or judge the exit by the placement rows only.
  2. The record:15-16 — "does exactly that" is not what the document describes, and the mechanism is an unlabelled inference.

    • The advice quoted is about several in-flight stores sharing bits 11:0 while a load needs forwarding from one. A disjoint forward copy has neither: its in-flight stores are consecutive, and none of its loads needs forwarding.
    • What bears on the copy is the first sentence: forwarding eligibility is judged by bits 11:0, and the load at src + i shares them with the store to dest + i - d. That the load then waits is ToyOS's inference from it.
    • The document does not give the window either. By the same reading the match exists at d = 32-256 too (my inference: that store is well inside the 48-entry store queue the same section describes), and nothing stalls there. The under-32 bound is measured, not derived.
    • Fix: give the eligibility sentence as the vendor fact, the wait as the inference (the kernel header's wording does this), and the window as measured.
    • Same lines: rev. 3.02's title is "Software Optimization Guide for AMD EPYC 7002 Processors". "Family 17h Models 30h and Greater" is its chapter 2 and the title of earlier revisions. Use the right title, or cite number, revision, section and page alone as the kernel header does.
  3. The record:62-66 — the exit cannot be run from the record, and its second clause closes the record with the stall in place.

    • osbench copyat and its copyat.place.* rows exist nowhere on main (git grep copyat finds only this file). They live on measurement-only commits: b76abc3, reachable today from amd-memcpy-ab8, -ab9n and -ab9s. libc: a forward copy a short way above its source modulo 4 KiB copies down #1133's closing comment promises to keep only amd-memcpy and amd-memcpy-sse2, neither of which contains it. Name the revision that carries the instrument, so the exit survives a branch cleanup.
    • "Alternatively, the routed builds' 256 KiB cost is explained and shown to be independent of the copy" requires no fix to the stall the heading names, and "independent of the copy" is not defined. If the intent is that libc: a forward copy a short way above its source modulo 4 KiB copies down #1133's route may then land, say that the first clause's stall bound still has to be met.

SUGGESTION

  • The record:51, 60 — 776c503ac, kmemcpy/round-3/, is on evidence branch pr-1139, not pr-1133; the two share no ancestor. The hash resolves either way. Fix the branch name while the file is open.
  • The record:32-33 — "at no distance tried from 32 to 2048" is true of the 20x stall, not of every slowdown. kernel: memcpy goes highest first where a forward copy would stall #1139's 64 KiB rows read 1553 and 1550 ns at d = 64 and 96 against 962 at d = 0, which is 1.6x and over the exit's own 1.5x bar. 16 KiB reads 1.3-1.5x at d = 32-128. "No stall of that size" would be exact.
  • The record:38-40 — "did not stall there" for the T14: the heap pair read 121-124 ns and kernel: memcpy goes highest first where a forward copy would stall #1139's in-process copy 136-140 against 58-74 at d = 0, so 1.6-2.4x. Giving the figures keeps "not established" honest; kmemcpy/round-3/judgement.md lists that reading as unexplained.
  • The record:34 — "took 1953-2570 ns against 80-90" mixes the routed head's 80-81 with base's 90 at d = 0. Say which.
  • The record:62-64 — "within 5%" reads two-sided; S read 1.03 of base. "No more than 5% below" is what is meant. The exit also stops at 1 MiB while the table records 2x at 64 MiB; say that the range above 1 MiB is left out on purpose (round 5: the descending loop lost at 4 MiB) so a close does not drop it silently.
  • The record:44-47 — "about 7% slower ... and on mem.memcpy.262144 ... (0.84-0.86)" reads as 7% for a row that lost 14-16%. Line 47 is also unwrapped.
  • By issues/README.md's definitions this reads as a defect (real, reproducible, an exit that asks for a fix). Round 12's rule called it a finding; the orchestrator's call.

PR description and commit message (for the orchestrator)

  • "not reproduced by moving base's memcpy" and the commit's "so the cost is in the routed code" hold for the placement rows only; see blocker 1.
  • "judged on the same paired-boot rule" is not so. Round 12 cleared an arm unless it regressed in at least 4 of 5 blocks; the record's exit requires within 5% in at least 4 of 5, which is stricter, and it judges mem.memcpy.*, which round 12 reported and did not judge.
  • The sourcegate test's exit 0 has no log, and host CI is skipped on the draft. I did not run it; by inspection the file contains none of the gate's shapes.

SEND BACK

…s exit can be run

- The vendor's sentence is the fact, the wait our inference and the
  under-32 window a measurement; 56305 is cited by number, revision, section
  and page.
- mem.memcpy.262144 is not a clean witness: round 10's shifted base read
  0.80 of base on it, round 12's read base's level, and the row already has
  a 45% range on main. The placement rows are what follow the routed code,
  and the exit judges by them alone.
- The copyat instrument is named by revision (b76abc3, on
  amd-memcpy-ab8), and the exit's second clause keeps the stall bound.
- The suggestions: #1139's evidence branch, the slowdowns at 32 and above,
  the T14 figures, which d = 0 the heap pair is set against, a one-sided 5%,
  and the range over 1 MiB left out on purpose. It is a defect, not a finding.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
@Japabu

Japabu commented Nov 10, 2026

Copy link
Copy Markdown
Collaborator Author

Orchestrator verification of the fix (77adbf4..fe18b74), which is record-only (one issue file):

  • Blocker 1: the placement rows and mem.memcpy.262144 are stated separately. Round 10's 0.80 reading on the shifted base is given, the existing 45%-range defect is named, and the exit judges by the placement rows.
  • Blocker 2: the vendor fact, our inference and the measured window are separated, and 56305 is cited by number, revision, section and page.
  • Blocker 3: b76abc3 and the kept branch amd-memcpy-ab8 are named, and the exit's second clause still requires the stall bound.
  • The record is now kind: defect, and the suggestions were taken.

Review: #1198 (comment)

@Japabu
Japabu marked this pull request as ready for review November 10, 2026 21:50
@Japabu
Japabu added this pull request to the merge queue Nov 10, 2026
Merged via the queue into main with commit fa30656 Nov 10, 2026
10 checks passed
@Japabu
Japabu deleted the memcpy-alias-finding branch November 10, 2026 21:55
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.

1 participant