Repository navigation
x86-64 user copies go by memcpy's rule, and a failed copy is a failure - #1244
Conversation
On the AMD bench a pipe whose reader's buffer sat 16 bytes into a page moved 3 GB/s against 20 at 0: the kernel's copy into that buffer is a forward `rep movsb` whose destination is 16 bytes above its source modulo 4 KiB, the store-forwarding stall #1139 took out of the kernel's own `memcpy`. Cached reads stall the same way (a 1 MiB pread 97 against 392 us). x86-64's user copies now ask `copy::Rule` as `memcpy` does and, where it sends a copy down, run the same descending copy, now one instruction body (`copy_down_asm!`) both expand. Its fixup is the whole body's range: a faulting `mov` is rerun once the fault is served, and one nothing serves resumes at a shared exit that closes SMAP's door and answers failure. `uaccess::{copy_from_user, copy_to_user, clear_user}` answer `Result<(), Faulted>` on both architectures instead of a count of bytes left, which no caller used but to test for zero and the descending copy cannot give. The `copies-go-down` actuator sends every copy of 64 bytes or more at a nonzero page distance down, so the guest runner's Intel-ruled CPU takes the descending copy's fault paths: `user_copy_faults` gains a pipe round 16 bytes off the pipe's frames, and the row `a_user_copy_highest_first_faults_or_ends_as_bad_address` runs the job on that kernel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A pipe gives back the frames its reader empties and takes them again for the next write: 35% of 4 KiB writes on the T14 took the general path for it. A freshly written 8 MiB file reads back at two thirds of a 16 MiB file's rate on the AMD bench, and faster on the T14; cause not found. Each with its exit; the diagnosis is on evidence pr-1244. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of #1244 at c6e7bee (code at 69de741)Architecture
Changing the return type to Size: kernel +190/−90. That covers two exported descending routines, their fixup ranges, the macro conversion, the actuator arm, and Correctness, verified by reading
Test and controlUnder
I confirmed that 125dca5 is 69de741 plus the one-line mutation. Its run (43037875955) panics on user address The actuator cannot be silently inert: Evidence checked
Records (c6e7bee)
RequiredNone. Suggestions
What metal must show
The decision ruleThe rule is sound for the predicted, directional effects. The direction was fixed in advance, so the sign test alone passes by chance with probability 1/16, before the 3% filter.
Verdict: LAND, pending the metal above. |
The benches are down, so the user copies land on guest evidence and their speed-up is unmeasured on metal. The kernel copies' record names the A/B of main from before and after it, the rows, both benches and what main must show, and the shared boots. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Orchestrator verification, landing under the benches-down rule (metal is unavailable; landing on review + host + guest, metal owed):
|
Head 4e06cf1: code-identical to 69de741, at which every check below ran (later commits add issue records only). It lands on guest evidence: both metal benches are down, and the speed-up this PR exists for is not yet measured on metal.
Problem
On the AMD bench (Ryzen 7 4800U, Zen 2)
pipe.throughputsits near 3 GB/s at every write size from 4 KiB, against 15-20 GB/s when the reader's buffer starts on a page. The 64 KiB reader buffer the allocator hands osbench starts 16 bytes into a page, so the kernel's copy from the pipe's frames into it is a forwardrep movsbwhose destination is 16 bytes above its source modulo 4 KiB: the store-forwarding stall #1139 took out of the kernel's ownmemcpy(AMD 56305 rev. 3.02, §2.12) but not out of its user copies. Cached reads stall the same way (a 1 MiBpread97 µs into a buffer at a page's start, 392 µs at 16 bytes in).Diagnosis, two ToyOS boots and two Linux runs per bench with stage timers in a scratch kernel: tables and logs.
The T14 shows no such stall (64 KiB at 16 costs 9-12%), as #1139's rule for Intel says.
Change
copy::Ruleexactly as the kernel'smemcpydoes and, where it sends a copy down, run the same descending copy: one instruction body (copy_down_asm!) thatcopy_downand the two user routines expand, integer registers only. Its fixup covers the body's range: a faultingmovis rerun once the fault is served, and one nothing serves goes to a shared exit that closes SMAP's door and answers failure. A fault on a kernel address inside that range still ends the kernel: the page-fault handler takes a fixup only for a user address.uaccess::{copy_from_user, copy_to_user, clear_user}answerResult<(), Faulted>on both architectures instead of a count of bytes left. No caller used the count but to test for zero, and a descending copy has no meaningful one.copies-go-down(window 4096) sends every copy of 64 bytes or more at a nonzero page distance down, so the guest runner's CPU, ruled Intel or other, takes the descending copy and its fault paths.user_copy_faultsgains a pipe round 16 bytes off the pipe's frames (down by the AMD rule on that bench), and the new rowa_user_copy_highest_first_faults_or_ends_as_bad_addressruns the job on the actuator kernel: one boot of QEMU's x86-64 machine. On metal the AMD bench's sharedtestcasesboot takes the descending path by its own rule.Not changed: userland's
memcpy(agents/memperf's, #1198's record). The predicate this PR uses, offered for libc to share: a copy goes highest first when n >= 64 and 0 < (dest - src) mod 4096 < window, window 32 on AMD and 0 elsewhere.Checks
Targeted, per the owner's testing rules of 2026-11-11: the change is the x86-64 and AArch64 kernels' user-copy routines, their one caller (
user_ptr), an actuator and one guest job.build/clippy.rsthis change reaches (x86-64: default,boot-actuators,boot-actuators,test-actuators; AArch64: default,boot-actuators,test-actuators) at 69de741: exit 0 on all five (log).kernel-copies(the descending copy and every rule againstcopy_within, every page distance),cargo test -p kernel-copies --target x86_64-apple-darwinunder Rosetta on the tree committed as 69de741: exit 0, 3 passed (log). It testscopy.rs, whose body the user routines now expand.cargo run -- --build-onlyon the same tree: exit 0 (log).a_user_copy_highest_first_faults_or_ends_as_bad_address(x86-64, every copy down),a_user_copy_faults_in_mid_copy_or_ends_as_bad_address(x86-64, by the runner CPU's own rule) andvirt_user_copy_faults(AArch64) (log).pipeperf-usercopy-mutis this change with the descending copy's fixup disabled (patch); run 43037875955: failure, as required. The new row FAILs onKERNEL PANIC: read unmapped address(log). So the row reaches the descending copy's fault path, which nothing else on the guest runner does.kernel/pure, unchanged).Owed on metal
The speed-up is a prediction from the diagnosis, not a measurement of this change: on the AMD bench the pipe with osbench's own buffers is expected to go from 3.0 GB/s to about 7-20 GB/s by write size. Recorded in
issues/kernel-forward-copies-and-fills-are-one-rep-movsb-or-stosb-on-every-cpu.mdas Owed on metal, for when a bench returns:osbench pipeab(the measurement-only commit onpipeperf-ab-base, run beside A paged read takes two passes of its pager's lock and populates nothing ahead #1245's arm: 32 boots with the shared boots below). A row changes where all four paired boots move one way and their median ratio is beyond 1 ± 3%. Main after must show on the AMD benchab.pipe.natural.4096,.16384and.65536up from about 3.0 GB/s to about 7, 15 and 20 GB/s, and each cached read 16 bytes into a page at most 1.25 times the same read at a page's start (1.9 to 4 times before); on the T14, none of those rows changed. libc: a forward copy a short way above its source modulo 4 KiB copies down #1133'scopyat.placerows should not move.testcasesandshared-debugboots of main, both benches; on the AMD benchuser_copy_faultsthere copies down by its own rule.Risks
rep movsbstreams; at the distances it now takes (1-31 bytes) the forward copy stalls far worse, but a copy that sits there and is long enough to reach DRAM is measured only by the A/B's 1 MiB and 16 MiB rows.copies-go-downkeeps an actuator arm inboot.rs's rule choice; the shipping kernel folds it tofalse.🤖 Generated with Claude Code