Repository navigation
libc: a short copy is one rep movsb only where the CPU declares it fast - #1257
Conversation
Review of a7e5b1cWhat I read: the three commits and the full diff, Architecturelibc owns userland's Correctness for overlapping and unaligned buffers
The FSRM predicate
The once-only CPUID read is thread-safe
Code placement (the risk that closed #1133)#1133's head dropped to 0.80–0.94 of base on the AMD 256 KiB placement rows in 5 of 5 blocks. Base with
The risk is real. The A/B carries the right rows: The rule's power here is limited. With two boots per arm, "both head boots beyond both base boots" happens by chance in a third of rows, counting both directions. The 2% gate filters only rows steadier than that. The AMD 256 KiB row is bimodal from boot to boot (35.2–41.6 GB/s), so two boots per arm cannot rule out a drop of 6–20%. Before the results are read:
Two more points on the rule:
FindingsBLOCKER: none in the code. Required with the metal result:
The PR description does not list either update. SUGGESTION
What metal must show
Guest rowsNone. The metal boots run every program's libc on a CPU without FSRM (AMD) and on one with it (T14), which no QEMU row adds to. The aarch64 change is only the return value, and the aarch64 host tests cover it. LAND, pending metal and the record update above. |
…ord's comment cites the leaf The small-memcpy record said libc's `copy_forward` is one `rep movsb` at every length, and the memset record pointed `memcpy`'s `rep movsb` at the same start-up; both now say how a copy chooses. The comment on the word says "the rest" for every copy it does not send to moves, and cites the SDM's leaf table and the Optimization Reference Manual for FSRM. Comments and records only: the build the queued A/B measures is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Re-review of 0071258Scope: whether round 1's findings are resolved, whether any code changed since a7e5b1c, and whether the records describe the path choice truthfully. No code changed since a7e5b1c
The records are true
Both required record updates from round 1 are done. Adding the A/B result to the memcpy record remains a landing condition. FindingsBLOCKER: none. SUGGESTION
Evidence
Landing conditions
LAND, pending metal and the record update. |
With no bench booting, the record names the A/B on both benches the choice by FSRM still owes: the small rows on the AMD bench, no slower row on the T14, and the 256 KiB placement rows on both, judged as #1133's closing asked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
x86-64's `memcpy` and `memmove`'s forward branch were one `rep movsb` at every length. On a CPU that declares fast short `rep movsb` (FSRM, CPUID.(EAX=7,ECX=0):EDX[4]) that stays the whole copy. Elsewhere a copy of up to 256 bytes is moves from each end that overlap in the middle: two of 1, 2, 4 or 8 bytes under 16, and one to eight SSE2 loads from each end above, every load before any store, so any overlap copies right. CPUID is asked once per process. The 2026-11-11 diagnosis timed candidate copies on both benches: on the AMD bench, which lacks FSRM, a 64-byte `rep movsb` took 15.8 ns against 5.4 for a 16-byte vector loop, and 256 bytes 17.1 against 10.0 for a 32-byte one; on the T14, which has FSRM, `rep movsb` was fastest or within half a nanosecond at every size. At 512 bytes the 32-byte loop still led `rep movsb` by a tenth on the AMD bench, and from 1 KiB `rep movsb` led on both; 256 bytes is as far as the sixteen SSE registers reach with every load first. The copies crate checks the short copy directly, whatever the host's CPU, at every length to 256, every overlap either way and four alignments. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The guest build's `memcpy` called the FSRM question on every copy of up to 256 bytes, and so saved three registers in its prologue even on a CPU that keeps `rep movsb`. The question is now asked by the first short copy alone, out of line, which then copies; every later copy reads the answer inline. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… compare The previous commit still left the guest build's `memcpy` saving a register and testing two states before `rep movsb`, because a copy had to keep `dest` across the first copy's call. Now the CPU's answer is one word, the length below which a copy is moves: 0 with FSRM, 257 without, and usize::MAX until the first copy asks. One compare of `n` against it chooses, and the first copy is a tail call: `copy_forward` answers `dest`, as `memcpy` does, on both architectures, so the call need not keep it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ord's comment cites the leaf The small-memcpy record said libc's `copy_forward` is one `rep movsb` at every length, and the memset record pointed `memcpy`'s `rep movsb` at the same start-up; both now say how a copy chooses. The comment on the word says "the rest" for every copy it does not send to moves, and cites the SDM's leaf table and the Optimization Reference Manual for FSRM. Comments and records only: the build the queued A/B measures is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With no bench booting, the record names the A/B on both benches the choice by FSRM still owes: the small rows on the AMD bench, no slower row on the T14, and the 256 KiB placement rows on both, judged as #1133's closing asked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8267b8b to
61b3248
Compare
|
Orchestrator: landing under the benches-down rule. Review r2 LAND. Head 61b3248 is the same libc commits rebased onto a7ca8ad (range-diff |
Problem
sdk/libc's x86-64memcpyandmemmove's forward branch were onerep movsbat every length. On the AMD bench (Zen 2, no FSRM)mem.memcpy.64read 0.61 of Linux's andmem.memcpy.2560.90 (2026-11-11 scoreboard,issues/a-small-memcpy-on-the-amd-bench-runs-at-as-little-as-six-tenths-of-linuxs.md). In the diagnosis's candidate copies on that bench,rep movsbtook 15.8 ns for 64 bytes against 5.4 by a 16-byte vector loop. On the T14 (FSRM)rep movsbwas fastest, or within half a nanosecond of it, at every size (evidence).Change
rep movsbkeeps onerep movsbfor every copy. FSRM is CPUID.(EAX=7,ECX=0):EDX[4], the vendor's statement that a shortrep movsbstarts fast.memcpy.rep movsb.usize::MAXuntil the process's first copy. That first copy asks CPUID, out of line, and then copies. Every later copy reads the word and compares its length with it; on a CPU without FSRM a short copy then also tests the word forusize::MAX.copy_forwardnow answersdest, on both architectures, so that the first copy is a tail call andmemcpy's prologue keeps nothing across it.With FSRM,
memcpyruns three instructions more beforerep movsbthan main's did: a load of the word, the compare and the branch. It runs one moremovbefore it.The kernel's copies are not changed here. The rule for the kernel side, which is integer-only, uses the same predicate, FSRM = CPUID.(EAX=7,ECX=0):EDX[4] (after checking CPUID.0:EAX >= 7), and the same 256-byte bound.
Commits
Checks
cargo test -p toyos-libc-copies --target x86_64-apple-darwin, x86-64 under Rosetta: 105 passed.cargo test -p toyos-libc-copieson aarch64: 105 passed.cargo clippy -p toyos-libc-copies --all-targets, for x86-64 and for aarch64, with the adopted lints and-D warnings.dest. The stagings below also built the guest libc for both architectures.copy_within.copy_forwardat every length to 300 and offset to 20, and now check that it answersdest.rep movsbabove, and the first copy's question.memcpyandmemmoveas each staging'sosbenchlinked them:memcpyinside either;virt_smp,std_threads_count_the_cpus_and_give_back_ended_threadsanda_user_copy_faults_in_mid_copy_or_ends_as_bad_address. Every userland program in those boots copies through the newmemcpy. The runner boots x86-64 under KVM with-cpu host, so which path it takes depends on the runner's CPU, and the host tests check both paths. It is green, at 8267b8b.git range-diffshows the three libc commits and the review commit unchanged (=). Only the owed-on-metal record commit changed: it keeps An unmap walks no futex bucket while no thread of its space is in a futex wait #1223's candidates paragraph and this PR's owed list, both in the small-memcpy record. Main's libc changes since the tested base are inposix_io.rsanderrcode.rs, outside the copies.issues/a-small-memcpy-on-the-amd-bench-runs-at-as-little-as-six-tenths-of-linuxs.md. The benches are down, so it is not a landing condition: an A/B on both benches, main from before and after, ofosbench_memand ofosbench copyat.copyatis on the measurement branches, not on main. Main after must read:mem.memcpy.64and.256faster;mem.memcpy.*row slower;mem.memcpy.262144and the eightcopyat.place.*rows, judged together by each boot's mean, no slower. An AMD spread over 5% is inconclusive and asks for more rotated blocks.memperf-copy-ab-head3bff283c againstmemperf-copy-ab-base915c248) never booted.Review r1 (0071258)
issues/a-small-memcpy-on-the-amd-bench-runs-at-as-little-as-six-tenths-of-linuxs.mdnow says how a copy chooses, and what is owed on metal.memcpy'srep movsbat the same start-up.rep movsb(CPUID.07H.01H:EAX[10]): the zero-lengthrep movsbcost is unmeasured on the T14.memmoves through the short copy: a follow-up with its own A/B.cargo test -p toyos-libc-copies --target x86_64-apple-darwin(105 passed) and its x86-64 clippy run. Each exited 0 (logs).Not run, and why
cargo run -- --ci host: under the owner's testing policy only the touched crate's tests and lints run; the merge queue runs the rest.🤖 Generated with Claude Code