libs/libc/string: Copy and compare by words when pointers agree on alignment. - #19857
Open
Fishwaldo wants to merge 1 commit into
Open
libs/libc/string: Copy and compare by words when pointers agree on alignment.#19857Fishwaldo wants to merge 1 commit into
Fishwaldo wants to merge 1 commit into
Conversation
Fishwaldo
added a commit
to Fishwaldo/nuttx
that referenced
this pull request
Aug 16, 2026
Both boards used the C string and memory routines while the architecture's hand written ones sat unused beside them. A 64 bit core with a filesystem, a network stack and a display above it spends a great deal of its time in these functions, and the assembly moves a register at a time rather than a byte. RISCV_STRING_FUNCTION selects the whole set, so one symbol covers memcpy, memset, memmove and the string routines together. The generic memset tuning options go with it, since the C memset they tune is no longer built. These routines need the alignment fixes in apache#19856 and apache#19857 to be correct on misaligned pointers. Assisted-by: Claude:claude-opus-5 Signed-off-by: Justin Hammond <justin@dynam.ac>
Fishwaldo
added a commit
to Fishwaldo/nuttx
that referenced
this pull request
Aug 16, 2026
Both boards used the C string and memory routines while the architecture's hand written ones sat unused beside them. A 64 bit core with a filesystem, a network stack and a display above it spends a great deal of its time in these functions, and the assembly moves a register at a time rather than a byte. RISCV_STRING_FUNCTION selects the whole set, so one symbol covers memcpy, memset, memmove and the string routines together. The generic memset tuning options go with it, since the C memset they tune is no longer built. These routines need the alignment fixes in apache#19856 and apache#19857 to be correct on misaligned pointers. Assisted-by: Claude:claude-opus-5 Signed-off-by: Justin Hammond <justin@dynam.ac>
Contributor
|
@Fishwaldo could you apply this change on top of #19870? The change will be more simpler. |
Fishwaldo
marked this pull request as draft
August 17, 2026 03:59
Contributor
Author
|
Hi @xiaoxiang781216 I'll rebase once #19870 lands. |
…ignment.
The BSD string functions take a word path only when both pointers are
aligned, and a byte path otherwise. A pair at the same offset from a
boundary takes the byte path even though copying or comparing a few leading
bytes aligns both at once, since aligning one aligns the other.
Add MISALIGNED(), which asks whether two pointers disagree about where a
boundary falls, and walk an agreeing pair up to the boundary before the
existing path selection. MISALIGNED4() does the same for the 4-byte path,
so a pair that is 4-byte but not 8-byte aligned reaches the wide path
instead of the middle one. No existing line changes: the walk is a new step
ahead of the current decisions. A pair at differing offsets still takes the
byte path, since no single boundary serves both.
Measured on an EIC7700 EVB (EIC7700X, RV64GC, 1.4GHz) with the BSD string
functions selected and the RISC-V assembly ones disabled, using the
benchmark in apps#3706, medians of 3 runs in MB/s at its largest size:
equal offset aligned
memcpy 414 -> 4148 10.0x 4214 -> 4208
memcmp 41 -> 361 8.8x 362 -> 360
strncmp 28 -> 202 7.4x 207 -> 207
strcmp 42 -> 273 6.5x 278 -> 276
strncpy 377 -> 1676 4.5x 1824 -> 1748
stpncpy 376 -> 1654 4.4x 1843 -> 1724
stpcpy 551 -> 1833 3.3x 1970 -> 1939
memccpy 650 -> 2012 3.1x 2478 -> 2016
strcpy 636 -> 1837 2.9x 1678 -> 1965
Cases the walk never runs for move in both directions by up to a third, the
largest being memccpy at differing offsets, 648 -> 414. Their code is
unchanged, so that is code placement rather than an effect of the change.
The change is architecture independent but has only been measured on
RV64GC. Word size, alignment cost and byte loop codegen all differ
elsewhere, so the balance wants measuring on other architectures.
Assisted-by: Claude:claude-opus-5
Signed-off-by: Justin Hammond <justin@dynam.ac>
Fishwaldo
force-pushed
the
upstream-libc-string-align
branch
from
August 17, 2026 11:21
5c9c3fe to
310581d
Compare
Fishwaldo
marked this pull request as ready for review
August 17, 2026 11:21
Fishwaldo
requested review from
Donny9,
tmedicci and
xiaoxiang781216
as code owners
August 17, 2026 11:21
Contributor
Author
|
@xiaoxiang781216 done! |
xiaoxiang781216
approved these changes
Aug 17, 2026
xiaoxiang781216
pushed a commit
that referenced
this pull request
Aug 18, 2026
Both boards used the C string and memory routines while the architecture's hand written ones sat unused beside them. A 64 bit core with a filesystem, a network stack and a display above it spends a great deal of its time in these functions, and the assembly moves a register at a time rather than a byte. RISCV_STRING_FUNCTION selects the whole set, so one symbol covers memcpy, memset, memmove and the string routines together. The generic memset tuning options go with it, since the C memset they tune is no longer built. These routines need the alignment fixes in #19856 and #19857 to be correct on misaligned pointers. Assisted-by: Claude:claude-opus-5 Signed-off-by: Justin Hammond <justin@dynam.ac>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The BSD string functions choose a word-at-a-time path only when both pointers are
aligned, and a byte-at-a-time path otherwise. That leaves a common case on the slow
path: a pair sitting at the same offset from a boundary. Copying or comparing a few
leading bytes aligns both at once, because aligning one aligns the other.
This adds
MISALIGNED(), which asks whether two pointers disagree about where aboundary falls, and walks an agreeing pair up to the boundary before the existing
path selection runs.
MISALIGNED4()does the same for the 4-byte path added by#19870, so a pair that is 4-byte but not 8-byte aligned now reaches the wide path
rather than the middle one.
Nine functions have such a gate and all nine are covered:
memcpy,memccpy,memcmp,strcpy,stpcpy,strncpy,stpncpy,strcmp,strncmp.A pair at differing offsets is deliberately untouched. No single boundary serves
both, so there is nothing to walk to.
The diff adds 187 lines and removes none. Every existing alignment test, word
loop and byte loop is preserved verbatim; the walk is a new step ahead of them. That
keeps the change easy to review and easy to revert.
Relationship to #19870
#19870 widened these functions to
long longand added a 4-byte middle path, whichcovers pairs that are both 4-byte aligned. It does not cover equal offsets that are
not multiples of four:
UNALIGNED()is the OR of both pointers, sos+1/d+1failsboth gates and still reaches the byte loop. That is the gap here.
Measurements
EIC7700 EVB (EIC7700X, 4 x RV64GC, 1.4 GHz), kernel build,
CONFIG_ALLOW_BSD_COMPONENTSand
CONFIG_LIBC_NEWLIB_OPTSPEEDset, everyCONFIG_RISCV_*string option disabled sothe C implementations are the ones under test. Benchmark from apps#3706. Medians of 3
runs, MB/s, at its largest size (32 KiB for the mem functions, 4 KiB for the str ones):
s+1/d+1s+0/d+0memcpymemcmpstrncmpstrcmpstrncpystpncpystpcpymemccpystrcpyThe same run set was taken with an independent earlier version of the benchmark and
agreed on every gain to within a few percent.
Cases the walk never runs for move in both directions by up to a third:
strcpyaligned improves 1.17x,
memccpyat differing offsets drops to 0.64x. The code onthose paths is unchanged, so that is code placement rather than an effect of the
change.
This has only been measured on RISC-V
The change itself is architecture independent, and so is the reasoning behind it, but
every number above comes from one RV64GC core. Three things that decide whether the
trade is as good elsewhere all vary by architecture:
sizeof(libc_data_t) - 1bytes, so it ischeaper relative to the transfer on a 32-bit target than on this 64-bit one.
of word speed. On a core where misaligned or byte access is comparatively cheaper,
the gain shrinks; on one where unaligned word access traps to a fixup handler, the
arithmetic changes again.
core and this compiler, not of the patch, and will land differently elsewhere.
I would welcome numbers from ARM, Xtensa or a 32-bit RISC-V target before this is
taken as generally beneficial. If it turns out to be a win only on some
architectures, the walk is small and self-contained enough to sit behind a
configuration option instead.
Alternatives considered
Two other shapes were built and measured on hardware: the same walk written with
early returns, and changing the gate conditions in place from "both aligned" to
"agree" with the walk inside the branch. All three produce the same gains and differ
only in which untouched paths land well or badly. This one was chosen because it
modifies no existing line.
Testing
tools/checkpatch.shclean.nxstyleclean on all 10 files.Correctness comes from the arch_libc test sweeps, which cover exactly what the walk
could break: source and destination offsets 0-3 x 0-3 for
memcmpand 0-7 x 0-7 forthe
strcmp/strncmp/strncpy/stpcpyfamily, at every size boundary, with thedifference or terminator placed at each position in turn.
The cases needing most care were the counted ones.
strncmp's byte tail returns*cs - *ctwhen its count is exhausted, so its walk returns 0 itself rather thanfalling through.
strncpyandstpncpymust not let a terminator found during thewalk reach the word loop, and must still pad to exactly
n; both leave theterminator for the byte loop, which pads.
stpcpyandstpncpyreturn the addressof the terminator, which the walk preserves.
Notes for reviewers
so a green run does not exercise this code at all. The measurements above are the
only evidence, and they are from a single architecture.
(
s+1/d+5and its mirrors, 6 of 64 offset-pair residues) still take the byte path.Baseline does too, so it is not a regression, just unexploited.
arch_libc_test_main.ccallperf_gettime(), which is arch-internal and absentfrom
syscall.csv. The benchmark itself is fine. Reproducing the table above needsCONFIG_TESTING_ARCH_LIBC_BENCH=ywith the per-function test options off. I willfix that separately in apps.