Skip to content

libs/libc/string: Copy and compare by words when pointers agree on alignment. - #19857

Open
Fishwaldo wants to merge 1 commit into
apache:masterfrom
Fishwaldo:upstream-libc-string-align
Open

libs/libc/string: Copy and compare by words when pointers agree on alignment.#19857
Fishwaldo wants to merge 1 commit into
apache:masterfrom
Fishwaldo:upstream-libc-string-align

Conversation

@Fishwaldo

@Fishwaldo Fishwaldo commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

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 a
boundary 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 long and added a 4-byte middle path, which
covers 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, so s+1/d+1 fails
both 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_COMPONENTS
and CONFIG_LIBC_NEWLIB_OPTSPEED set, every CONFIG_RISCV_* string option disabled so
the 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):

equal offset s+1/d+1 aligned s+0/d+0
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

The 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: strcpy
aligned improves 1.17x, memccpy at differing offsets drops to 0.64x. The code on
those 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:

  • The cost of the walk. It is bounded by sizeof(libc_data_t) - 1 bytes, so it is
    cheaper relative to the transfer on a 32-bit target than on this 64-bit one.
  • The value of reaching the word path. Here the byte loop runs at roughly a tenth
    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.
  • Byte loop codegen. The placement variation noted above is a property of this
    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.sh clean. nxstyle clean 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 memcmp and 0-7 x 0-7 for
the strcmp/strncmp/strncpy/stpcpy family, at every size boundary, with the
difference or terminator placed at each position in turn.

== string correctness ==
  memcpy     correctness: ok (0 bad)
  memmove    correctness: ok (0 bad)
  mv-overlap correctness: ok (0 bad)
  memset     correctness: ok (0 bad)
  memcmp     correctness: ok (0 bad)
  str-scan   correctness: ok (0 bad)
  strcmp-fam correctness: ok (0 bad)
  strlcpy    correctness: ok (0 bad)
== string fails: 0 ==

The cases needing most care were the counted ones. strncmp's byte tail returns
*cs - *ct when its count is exhausted, so its walk returns 0 itself rather than
falling through. strncpy and stpncpy must not let a terminator found during the
walk reach the word loop, and must still pad to exactly n; both leave the
terminator for the byte loop, which pads. stpcpy and stpncpy return the address
of the terminator, which the walk preserves.

Notes for reviewers

  • No in-tree configuration selects the BSD string functions on a target CI builds,
    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.
  • Pairs agreeing modulo 4 but not modulo 8 and not themselves 4-byte aligned
    (s+1/d+5 and its mirrors, 6 of 64 offset-pair residues) still take the byte path.
    Baseline does too, so it is not a regression, just unexploited.
  • apps#3706 does not link in a kernel build: the per-function tests in
    arch_libc_test_main.c call perf_gettime(), which is arch-internal and absent
    from syscall.csv. The benchmark itself is fine. Reproducing the table above needs
    CONFIG_TESTING_ARCH_LIBC_BENCH=y with the per-function test options off. I will
    fix that separately in apps.

@github-actions github-actions Bot added Area: OS Components OS Components issues Size: M The size of the change in this PR is medium labels Aug 15, 2026
@github-actions

github-actions Bot commented Aug 15, 2026

Copy link
Copy Markdown

MemBrowse Memory Report

No memory changes detected for:

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>
@xiaoxiang781216

xiaoxiang781216 commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

@Fishwaldo could you apply this change on top of #19870? The change will be more simpler.

@Fishwaldo
Fishwaldo marked this pull request as draft August 17, 2026 03:59
@Fishwaldo

Copy link
Copy Markdown
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
Fishwaldo force-pushed the upstream-libc-string-align branch from 5c9c3fe to 310581d Compare August 17, 2026 11:21
@Fishwaldo Fishwaldo changed the title libs/libc/string: Optimize string routines for misaligned pointers. libs/libc/string: Copy and compare by words when pointers agree on alignment. Aug 17, 2026
@Fishwaldo
Fishwaldo marked this pull request as ready for review August 17, 2026 11:21
@Fishwaldo

Copy link
Copy Markdown
Contributor Author

@xiaoxiang781216 done!

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area: OS Components OS Components issues Size: M The size of the change in this PR is medium

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants