Repository navigation
kernel: memcpy goes highest first where a forward copy would stall - #1139
Conversation
Review r1 of #1139 at 42fb869ArchitectureThe kernel is the right owner. I checked the following and found no defect:
Net: about 168 production lines (copy.rs 158, boot +9, mod +1) and 87 test lines in a new host package. The package follows the FindingsBLOCKER 1 —
BLOCKER 2 — commit message and PR body: the base is described wrongly, and the change is larger than stated.
BLOCKER 3 —
BLOCKER 4 —
BLOCKER 5 —
HELD FOR THE A/B (not judged here) —
Tests and evidence
Suggestions (optional)
SEND BACK |
…racts as they are - The rule's statics, set_rule and rule() exist only where memcpy does (target_os = "none"), so kernel-copies builds without dead code on an x86-64 host under -D warnings. - copy_by's contract is disjoint or identical ranges, as LLVM's memcpy allows. - The module header: copied highest first, the nearest older store with a load's bits 11:0 is 4 KiB less the distance back, not ahead. - The track issue records that memcpy replaced compiler_builtins' qword sequence on every forward copy, that each vendor's rule is measured on one microarchitecture, and that the AMD bench is family 0x17. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…racts as they are - The rule's statics, set_rule and rule() exist only where memcpy does (target_os = "none"), so kernel-copies builds without dead code on an x86-64 host under -D warnings. - copy_by's contract is disjoint or identical ranges, as LLVM's memcpy allows. - The module header: copied highest first, the nearest older store with a load's bits 11:0 is 4 KiB less the distance back, not ahead. - The track issue records that memcpy replaced compiler_builtins' qword sequence on every forward copy, that each vendor's rule is measured on one microarchitecture, and that the AMD bench is family 0x17. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Four boots per bench (base, head, head, base) of base b49e6ae (main and osbench kcopyat) against head dc53533 (base and this change at its first review). Evidence: ToyOSOrg/evidence branch pr-1139, kmemcpy/round-3. - Zen 2 (AMD bench, Ryzen 7 4800U): in this process, `rep movsb` waits at every distance under 32 bytes and at every length from 4 to 64 KiB (8 KiB: 1946 ns, 64 KiB: 20463 ns, against 88 and 962 at 0), and at none from 32 bytes up. The descending copy takes 260 ns at 8 KiB and 2009 ns at 64 KiB at every distance. Window 32, at every length, stays. - Tiger Lake (T14, i5-1135G7): the descending copy is slower than `rep movsb` at every distance and length measured (8 KiB: 241-256 ns against 58-197), so Intel's copies never go down. With one window left and no cap, the length cap and the distance-under-length test can no longer decide anything, and go. - The forward path, compiler_builtins' `rep movsb; rep movsq; rep movsb` replaced by one `rep movsb`, is a gain on both benches: a kernel tmpfs read of 4 or 8 KiB took 0.92-0.96 times base's at d = 0 and at every distance outside the window. No file, IPC or pipe row was slower in both pairs; the file metadata rows took 0.71-0.93 times as long. - The issue's exit holds: a kernel read 16 bytes above its source takes 1.17 (AMD) and 1.10 (T14) times the same read at 0 for 8 KiB. - Base did not stall at d < 32 on the AMD: its qword string did not wait there, where `rep movsb` does; the window keeps the byte string from it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A load whose address bits 11:0 match an older in-flight store waits on it: AMD's Family 17h judges store-to-load forwarding by those bits alone (56305 rev. 3.02, §2.12), and Intel documents the same as 4K aliasing. A forward string copy whose destination sits a short way above its source modulo 4 KiB stalls on every load: userland's `rep movsb` over 8 KiB at 16 bytes took 2605 ns against 90 at 0 on the AMD bench (Ryzen 7 4800U, Zen 2), and on the T14 (i5-1135G7, Tiger Lake) 750 ns from 16 to 64 bytes, 191 at 256, against 62 at 0 (osbench copyat, evidence pr-1133). The kernel's memcpy was compiler_builtins' `rep movsb; rep movsq; rep movsb`, the sysroot being built without `ermsb`; the A/B in the commits after this one measures what that sequence and this one each cost. The kernel does hit those distances. A measurement-only probe of every kernel memcpy under QEMU, by distance and caller, found osbench's file group (the kernel's tmpfs) moving 12.3% of its bytes at 1-511 bytes and 1.3% at 1-63, through the user copies' pieces, about what uniformly placed buffers give; the pipe and IPC groups' buffers sat clear of them. The kernel now defines memcpy itself, taking the place of compiler_builtins' weak symbol: a copy of at least 64 bytes whose distance is under the CPU's window and under its length goes down by general registers, 8 bytes a store, since the kernel touches no vector register; every other copy is one `rep movsb`. That changes every forward copy outside the window too, from the qword sequence to the byte string, on every CPU including Zen 2, which has no ERMS. The window is the CPU's, estimated here and set by that A/B: 32 bytes on Zen 2, and 192 up to 16 KiB on Tiger Lake. The rule is chosen once by CPUID's vendor at boot; another vendor's copies never go down. kernel-copies tests the copy on an x86-64 host against copy_within, and which copies each rule sends down. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…racts as they are - The rule's statics, set_rule and rule() exist only where memcpy does (target_os = "none"), so kernel-copies builds without dead code on an x86-64 host under -D warnings. - copy_by's contract is disjoint or identical ranges, as LLVM's memcpy allows. - The module header: copied highest first, the nearest older store with a load's bits 11:0 is 4 KiB less the distance back, not ahead. - The track issue records that memcpy replaced compiler_builtins' qword sequence on every forward copy, that each vendor's rule is measured on one microarchitecture, and that the AMD bench is family 0x17. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Four boots per bench (base, head, head, base) of base b49e6ae (main and osbench kcopyat) against head dc53533 (base and this change at its first review). Evidence: ToyOSOrg/evidence branch pr-1139, kmemcpy/round-3. - Zen 2 (AMD bench, Ryzen 7 4800U): in this process, `rep movsb` waits at every distance under 32 bytes and at every length from 4 to 64 KiB (8 KiB: 1946 ns, 64 KiB: 20463 ns, against 88 and 962 at 0), and at none from 32 bytes up. The descending copy takes 260 ns at 8 KiB and 2009 ns at 64 KiB at every distance. Window 32, at every length, stays. - Tiger Lake (T14, i5-1135G7): the descending copy is slower than `rep movsb` at every distance and length measured (8 KiB: 241-256 ns against 58-197), so Intel's copies never go down. With one window left and no cap, the length cap and the distance-under-length test can no longer decide anything, and go. - The forward path, compiler_builtins' `rep movsb; rep movsq; rep movsb` replaced by one `rep movsb`, is a gain on both benches: a kernel tmpfs read of 4 or 8 KiB took 0.92-0.96 times base's at d = 0 and at every distance outside the window. No file, IPC or pipe row was slower in both pairs; the file metadata rows took 0.71-0.93 times as long. - The issue's exit holds: a kernel read 16 bytes above its source takes 1.17 (AMD) and 1.10 (T14) times the same read at 0 for 8 KiB. - Base did not stall at d < 32 on the AMD: its qword string did not wait there, where `rep movsb` does; the window keeps the byte string from it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1139 Round 5 (four boots per bench, evidence round-5-ab): - The AMD's routed copies read 80-86 ns per 8 KiB against 2615 before. - At 4 MiB with d256, a distance its rep movsb does not stall at, the AMD loop read 597 us against rep movsb's 385. With the cap on every CPU, a copy over 1 MiB is base's rep movsb everywhere, measured in base3, and the ERMS reading goes. - What stays is the AMD's stall above 1 MiB, as before (64 MiB at d16: 2.0x of d0), and the T14's routed copies at 1.1-1.4x of rep movsb's best. The kernel-copy issue's premise was false (the old kernel copy was rep movsb; rep movsq; rep movsb and did not stall at those distances), and #1139 meets its exit; it never reached main, so it goes from this branch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1139 Round 5 (four boots per bench, evidence round-5-ab): - The AMD's routed copies read 80-86 ns per 8 KiB against 2615 before. - At 4 MiB with d256, a distance its rep movsb does not stall at, the AMD loop read 597 us against rep movsb's 385. With the cap on every CPU, a copy over 1 MiB is base's rep movsb everywhere, measured in base3, and the ERMS reading goes. - What stays is the AMD's stall above 1 MiB, as before (64 MiB at d16: 2.0x of d0), and the T14's routed copies at 1.1-1.4x of rep movsb's best. The kernel-copy issue's premise was false (the old kernel copy was rep movsb; rep movsq; rep movsb and did not stall at those distances), and #1139 meets its exit; it never reached main, so it goes from this branch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The comment cited 56305 by the wrong title: it is the Software Optimization Guide for AMD Family 17h Models 30h and Greater Processors, and its §2.12 (p. 39) says the processor uses address bits 11:0 to determine STLF eligibility. The comment also implied every distance under the 512 window stalls, which no bench showed on AMD. The issue now separates the two benches. On Zen 2, #1139's in-process rows stall at d = 8..31 and at nothing tried from 32 up, at 4 to 64 KiB. On the T14 the 512 window comes from copyat's rows alone; a heap pair 64 KiB + 16 apart and #1139's in-process copy at d = 16 did not stall there, so the Tiger Lake mechanism is recorded as not established. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Draft. The metal A/B has run and been judged. Ready for a focused check. Companion to #1133, which fixes the same stall in userland libc.
Problem
A load whose address bits 11:0 match an older in-flight store waits on it. AMD Family 17h judges store-to-load forwarding by those bits alone (56305 rev. 3.02, §2.12); Intel documents it as 4K aliasing.
The kernel's
memcpywascompiler_builtins'rep movsb; rep movsq; rep movsb, because the sysroot is built withoutermsb.Resulting behaviour
memcpyinkernel/src/arch/x86_64/copy.rs, which overridescompiler_builtins' weak symbol.memmoveandmemsetstaycompiler_builtins'. The fix doesn't belong in therust/fork: whether a copy goes down depends on the CPU, andcompiler_builtinschooses its copy at compile time only.rep movsb, except on AMD: there, a copy of at least 64 bytes whose destination is 1–31 bytes above its source modulo 4 KiB goes highest first.build/fpgate.rs).memcpy: highest first Rule { window }. Intel and other vendors never go down.Distances the kernel's copies hit
A measurement-only probe ran under QEMU; the distances depend only on software placement, so QEMU stands in for metal. Results: analysis.
Metal A/B
Base b49e6ae = main 190f3b1 + measurement-only
osbench kcopyat; head dc53533 = this change at its first review. Branchkernel-memcpy-ab, four boots per bench, all passed. Material: judgement, rule, readbacks. Each figure is the mean of its arm's two boots' p50s.rep movsbmade a kernel tmpfs read of 4 or 8 KiB take 0.92–0.96× as long, at d = 0 and at every distance outside the window. The file metadata rows took 0.71–0.93× as long, the T14's pipe throughput rose 7–17%, and no file, IPC or pipe row was slower in both pairs (K3, K6). The AMD's ipc rows were slower in one head boot only (2-head; 3-head was faster than base).rep movsbwaits at every distance under 32 bytes, at every length from 4 to 64 KiB: 1946 ns at 8 KiB and 20463 ns at 64 KiB, against 88 and 962 at d = 0. From 32 bytes up it never waits.rep movsbat every distance and length: at 8 KiB, 241–256 ns against 58–197 (K4, K5 failed). Following the rule, the follow-up commit sends no Intel copy down; with one window left, the length cap and the distance-under-length test go too.rep movsqdoesn't wait there andrep movsbdoes. The kernel's gain comes from the faster forward path; the AMD window is what keeps that path'srep movsbfrom its stall.rep movsbat d = 16 takes 136 ns at 8 KiB. libc: a forward copy a short way above its source modulo 4 KiB copies down #1133's copyat on the same bench and microcode measured 750. Here the T14's slow distances follow 64-byte misalignment, not a page-alias window. The AMD's stall reproduces (1946 against copyat's 2605).Checks at the head
Cargo.tomlmember conflict, where main had droppedkernel/dma): host CI passed, 109 steps, exit 0; x86-64 clippy with-D warningsand the x86-64 tests both exit 0 (evidence). Everything below was run at fe91241.cargo clippy -p kernel-copies --all-targets --target x86_64-apple-darwin -- -D warnings: exit 0.cargo test -p kernel-copiesfor x86-64 (Rosetta): exit 0. It checks the descending copy at every length to 300 and beyond, at every store offset, and each rule at every distance below 1 KiB and beyond, all againstcopy_within.cargo run -- --build-only: exit 0. The disassembly shows the kernel'smemcpywith no call or panic path.cargo run -- --ci hostat fe91241: 108 steps green, exit 0 (log). This host is aarch64; the x86-64 lines above coverkernel-copies.kcopyat_runsat dc53533: PASS. TCG reports AMD, so short-distance reads went down; every read and in-process copy was checked against its source (log). The follow-up only removes paths, so this check still applies.Risks and open decisions
rep movsbat d = 0 (260 against 88 ns at 8 KiB), against 22× stalled. Parity would mean letting the kernel copy save and use vector registers. The issue's exit holds without that, so the question is not raised.issues/a-kernel-copy-a-short-way-above-its-source-modulo-4-kib-stalls.mdexists only on libc: a forward copy a short way above its source modulo 4 KiB copies down #1133, and its premise (the kernel's copy was onerep movsb) is false. Its exit is met here (K1). Noted on #1133: whichever lands second deletes it.🤖 Generated with Claude Code