From 8b993a66da9cb823ad434370fafd60ab343e83f3 Mon Sep 17 00:00:00 2001 From: Justin Hammond Date: Sat, 15 Aug 2026 13:19:20 +0800 Subject: [PATCH 1/4] testing/libc/arch_libc: Add a throughput benchmark. The existing speed checks time one call at one size, 128 bytes, with both operands aligned. A machine implementation usually takes its wide path only when the pointers satisfy some alignment condition, so that single point reports the best case and says nothing about the rest of the input space. Measure the same functions across a size sweep and every source and destination alignment pair instead, plus strlcpy. On rv64 the difference this exposes is not marginal: strcpy 32768 B s+0/d+0 2938.0 MB/s strcpy 32768 B s+1/d+1 2942.0 MB/s strcpy 32768 B s+1/d+2 626.0 MB/s memcmp 32768 B s+0/d+0 412.4 MB/s memcmp 32768 B s+1/d+2 41.0 MB/s Two pointers misaligned by the same amount run at the aligned rate; misaligned by different amounts they fall to a tenth of it. Neither number is visible from an aligned measurement alone. A function with no machine implementation reports the same rate at every alignment, so the sweep also shows which of them a machine directory actually covers. Each result reports MB/s, which compares across machines, and cycles per byte where perf_gettime() is reachable from an application, both from one timed loop. strcat starts from an empty destination on each turn, since appending to the last result would grow it without bound, so its figure includes that store. It sits behind TESTING_ARCH_LIBC_BENCH, default n, because a measurement runs for a fixed interval and a full sweep takes about a minute. Assisted-by: Claude:claude-opus-5 Signed-off-by: Justin Hammond --- testing/libc/arch_libc/CMakeLists.txt | 7 +- testing/libc/arch_libc/Kconfig | 26 ++ testing/libc/arch_libc/Makefile | 4 + testing/libc/arch_libc/arch_libc_bench.c | 420 +++++++++++++++++++ testing/libc/arch_libc/arch_libc_test_main.c | 11 + 5 files changed, 467 insertions(+), 1 deletion(-) create mode 100644 testing/libc/arch_libc/arch_libc_bench.c diff --git a/testing/libc/arch_libc/CMakeLists.txt b/testing/libc/arch_libc/CMakeLists.txt index 89e26e78485..8c8eeff5aaa 100644 --- a/testing/libc/arch_libc/CMakeLists.txt +++ b/testing/libc/arch_libc/CMakeLists.txt @@ -21,6 +21,10 @@ # ############################################################################## if(CONFIG_TESTING_ARCH_LIBC) + if(CONFIG_TESTING_ARCH_LIBC_BENCH) + set(bench_srcs arch_libc_bench.c) + endif() + nuttx_add_application( NAME ${CONFIG_TESTING_ARCH_LIBC_PROGNAME} @@ -31,5 +35,6 @@ if(CONFIG_TESTING_ARCH_LIBC) MODULE ${CONFIG_TESTING_ARCH_LIBC} SRCS - arch_libc_test_main.c) + arch_libc_test_main.c + ${bench_srcs}) endif() diff --git a/testing/libc/arch_libc/Kconfig b/testing/libc/arch_libc/Kconfig index 05ccacdd88b..0de9563cc34 100644 --- a/testing/libc/arch_libc/Kconfig +++ b/testing/libc/arch_libc/Kconfig @@ -75,6 +75,32 @@ config TESTING_ARCH_LIBC_STRCHRNUL bool "test strchrnul" default y +config TESTING_ARCH_LIBC_BENCH + bool "throughput benchmark" + default n + ---help--- + Measure each function across a size sweep and every source and + destination alignment pair, reporting MB/s and cycles per byte. + An implementation usually takes its wide path only when the + pointers meet some alignment condition, so an aligned measurement + alone does not show what the rest of the input space costs. + +if TESTING_ARCH_LIBC_BENCH + +config TESTING_ARCH_LIBC_BENCH_BUFSIZE + int "benchmark buffer size" + default 262144 + ---help--- + Size of each of the two buffers the benchmark allocates. + +config TESTING_ARCH_LIBC_BENCH_MINMS + int "minimum milliseconds per measurement" + default 250 + ---help--- + Each measurement repeats until it has run for at least this long. + +endif + config TESTING_ARCH_LIBC_PROGNAME string "Program name" default "arch_libctest" diff --git a/testing/libc/arch_libc/Makefile b/testing/libc/arch_libc/Makefile index 610e10fa9d6..9184975804d 100644 --- a/testing/libc/arch_libc/Makefile +++ b/testing/libc/arch_libc/Makefile @@ -31,6 +31,10 @@ MODULE = $(CONFIG_TESTING_ARCH_LIBC) # arch libc test +ifneq ($(CONFIG_TESTING_ARCH_LIBC_BENCH),) +CSRCS += arch_libc_bench.c +endif + MAINSRC = arch_libc_test_main.c include $(APPDIR)/Application.mk diff --git a/testing/libc/arch_libc/arch_libc_bench.c b/testing/libc/arch_libc/arch_libc_bench.c new file mode 100644 index 00000000000..d73fd48f9cc --- /dev/null +++ b/testing/libc/arch_libc/arch_libc_bench.c @@ -0,0 +1,420 @@ +/**************************************************************************** + * apps/testing/libc/arch_libc/arch_libc_bench.c + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. The + * ASF licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + ****************************************************************************/ + +/* Throughput for the string and memory functions a machine directory may + * override, across a size sweep and every source/destination alignment + * pair. A machine implementation usually takes its wide path only when + * the pointers satisfy some alignment condition, so a single aligned size + * reports the best case and hides what the rest of the input space costs. + * + * Each measurement reports MB/s, which compares across machines, and + * cycles per byte from perf_gettime(), which does not depend on the + * timebase being calibrated. Both come from one timed loop. + * + * Two ways a benchmark of these functions measures nothing are defeated + * here: the compiler treating a pure call with unchanged arguments as loop + * invariant, countered by laundering the pointers through an asm that + * claims to change them, and comparison inputs an earlier measurement + * scribbled on, countered by preparing the buffers before each timed loop. + */ + +/**************************************************************************** + * Included Files + ****************************************************************************/ + +#include + +#include +#include +#include +#include +#include +#include + +#include + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +#define BENCH_BUF (CONFIG_TESTING_ARCH_LIBC_BENCH_BUFSIZE) +#define BENCH_MINMS (CONFIG_TESTING_ARCH_LIBC_BENCH_MINMS) +#define BENCH_GUARD 64 +#define BENCH_BATCH 16 + +/* perf_gettime() reaches an application only where the C library builds + * its own copy, or where the application and the kernel are one image. + * Elsewhere the cycle count is left out and the throughput stands alone. + */ + +#if defined(CONFIG_ARCH_HAVE_PERF_EVENTS_USER_ACCESS) || \ + defined(CONFIG_BUILD_FLAT) +# define BENCH_CYCLES 1 +#endif + +/* Operations. A writer stores through the destination pointer, so its + * destination offset is worth sweeping; a reader only takes one. + */ + +#define OP_MEMCPY 0 +#define OP_MEMMOVE 1 +#define OP_MEMSET 2 +#define OP_MEMCMP 3 +#define OP_MEMCHR 4 +#define OP_STRLEN 5 +#define OP_STRCMP 6 +#define OP_STRNCMP 7 +#define OP_STRCPY 8 +#define OP_STRLCPY 9 +#define OP_STRCHR 10 +#define OP_STRRCHR 11 +#define OP_STRCHRNUL 12 +#define OP_STRNLEN 13 +#define OP_STPCPY 14 +#define OP_STRNCPY 15 +#define OP_STRCAT 16 + +/**************************************************************************** + * Private Types + ****************************************************************************/ + +struct bench_op_s +{ + FAR const char *name; + uint8_t op; + bool dst; /* Touches the destination pointer, so its + * offset is worth sweeping too */ + bool str; /* Operand is a string, so it needs a + * terminator at the measured length */ +}; + +/**************************************************************************** + * Private Data + ****************************************************************************/ + +static FAR char *g_src; +static FAR char *g_dst; +static volatile unsigned long g_sink; + +static const struct bench_op_s g_ops[] = +{ + {"memcpy", OP_MEMCPY, true, false}, + {"memmove", OP_MEMMOVE, true, false}, + {"memset", OP_MEMSET, true, false}, + {"memcmp", OP_MEMCMP, true, false}, + {"memchr", OP_MEMCHR, false, false}, + {"strlen", OP_STRLEN, false, true}, + {"strcmp", OP_STRCMP, true, true}, + {"strncmp", OP_STRNCMP, true, true}, + {"strcpy", OP_STRCPY, true, true}, + {"strlcpy", OP_STRLCPY, true, true}, + {"strchr", OP_STRCHR, false, true}, + {"strrchr", OP_STRRCHR, false, true}, + {"strchrnul", OP_STRCHRNUL, false, true}, + {"strnlen", OP_STRNLEN, false, true}, + {"stpcpy", OP_STPCPY, true, true}, + {"strncpy", OP_STRNCPY, true, true}, + {"strcat", OP_STRCAT, true, true}, +}; + +static const size_t g_sizes[] = +{ + 64, 512, 4096, 32768 +}; + +/* Source and destination offsets within a register. Aligned, equally + * misaligned, misaligned by different amounts, and a source shift against + * an aligned destination. + */ + +static const uint8_t g_aligns[][2] = +{ + { + 0, 0 + }, + { + 1, 1 + }, + { + 1, 2 + }, + { + 3, 0 + } +}; + +#define NOPS (sizeof(g_ops) / sizeof(g_ops[0])) +#define NSIZES (sizeof(g_sizes) / sizeof(g_sizes[0])) +#define NALIGNS (sizeof(g_aligns) / sizeof(g_aligns[0])) + +/**************************************************************************** + * Private Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: bench_now + ****************************************************************************/ + +static double bench_now(void) +{ + struct timespec t; + + clock_gettime(CLOCK_MONOTONIC, &t); + return t.tv_sec + t.tv_nsec / 1e9; +} + +/**************************************************************************** + * Name: bench_seen_src + * + * Description: + * Whether an earlier alignment pair already used this source offset. + * + ****************************************************************************/ + +static bool bench_seen_src(size_t ai) +{ + size_t i; + + for (i = 0; i < ai; i++) + { + if (g_aligns[i][0] == g_aligns[ai][0]) + { + return true; + } + } + + return false; +} + +/**************************************************************************** + * Name: bench_prepare + * + * Description: + * Content for one measurement. The compares need the two operands equal + * for the whole length, or they stop early and time nothing, and the + * absent-character scans need a byte that never appears. + * + ****************************************************************************/ + +static void bench_prepare(FAR const struct bench_op_s *o, size_t n, + int so, int dofs) +{ + FAR char *s = g_src + BENCH_GUARD + so; + FAR char *d = g_dst + BENCH_GUARD + dofs; + size_t i; + + for (i = 0; i < n + BENCH_GUARD; i++) + { + s[i] = 'a' + (i % 23); + } + + memcpy(d, s, n + BENCH_GUARD); + + if (o->str) + { + s[n] = '\0'; + d[n] = '\0'; + } +} + +/**************************************************************************** + * Name: bench_one + * + * Description: + * One operation at one size and one alignment pair, timed until it has + * run for at least the configured interval. + * + ****************************************************************************/ + +static void bench_one(FAR const struct bench_op_s *o, size_t n, + int so, int dofs) +{ +#ifdef BENCH_CYCLES + clock_t c0; + clock_t c1; +#endif + double t0; + double el; + double mbs; + unsigned long reps = 0; + int i; + + bench_prepare(o, n, so, dofs); + + t0 = bench_now(); +#ifdef BENCH_CYCLES + c0 = perf_gettime(); +#endif + + do + { + for (i = 0; i < BENCH_BATCH; i++) + { + FAR char *s = g_src + BENCH_GUARD + so; + FAR char *d = g_dst + BENCH_GUARD + dofs; + + __asm__ volatile("" : "+r"(s), "+r"(d)); + + switch (o->op) + { + case OP_MEMCPY: + memcpy(d, s, n); + break; + case OP_MEMMOVE: + memmove(d + 8, d, n); + break; + case OP_MEMSET: + memset(d, i, n); + break; + case OP_MEMCMP: + g_sink += memcmp(s, d, n); + break; + case OP_MEMCHR: + g_sink += (uintptr_t)memchr(s, '~', n); + break; + case OP_STRLEN: + g_sink += strlen(s); + break; + case OP_STRCMP: + g_sink += strcmp(s, d); + break; + case OP_STRNCMP: + g_sink += strncmp(s, d, n); + break; + case OP_STRCPY: + strcpy(d, s); + break; + case OP_STRLCPY: + g_sink += strlcpy(d, s, n + 1); + break; + case OP_STRCHR: + g_sink += (uintptr_t)strchr(s, '~'); + break; + case OP_STRRCHR: + g_sink += (uintptr_t)strrchr(s, '~'); + break; + case OP_STRCHRNUL: + g_sink += (uintptr_t)strchrnul(s, '~'); + break; + case OP_STRNLEN: + g_sink += strnlen(s, n); + break; + case OP_STPCPY: + g_sink += (uintptr_t)stpcpy(d, s); + break; + case OP_STRNCPY: + strncpy(d, s, n); + break; + case OP_STRCAT: + + /* Appending to what the last turn appended would grow the + * destination without bound, so it starts empty each time. + * The store is inside the measurement. + */ + + d[0] = '\0'; + strcat(d, s); + break; + } + + g_sink += (unsigned char)d[0]; + } + + reps += BENCH_BATCH; + el = bench_now() - t0; + } + while (el * 1000.0 < BENCH_MINMS); + + mbs = (double)reps * n / el / 1048576.0; + +#ifdef BENCH_CYCLES + c1 = perf_gettime(); + printf(" %-8s %6zu B s+%d/d+%d %9.1f MB/s %8.3f cyc/B\n", + o->name, n, so, dofs, mbs, + (double)(uintmax_t)(c1 - c0) / ((double)reps * n)); +#else + printf(" %-8s %6zu B s+%d/d+%d %9.1f MB/s\n", + o->name, n, so, dofs, mbs); +#endif +} + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: arch_libc_bench + * + * Description: + * Run every operation over the size sweep and the alignment matrix. + * + ****************************************************************************/ + +int arch_libc_bench(void) +{ + size_t oi; + size_t si; + size_t ai; + + g_src = malloc(BENCH_BUF); + g_dst = malloc(BENCH_BUF); + if (g_src == NULL || g_dst == NULL) + { + printf("arch_libc bench: cannot allocate 2 x %d\n", BENCH_BUF); + free(g_src); + free(g_dst); + return 1; + } + + printf("== throughput ==\n"); + + for (oi = 0; oi < NOPS; oi++) + { + for (si = 0; si < NSIZES; si++) + { + if (g_sizes[si] + 2 * BENCH_GUARD + 8 > BENCH_BUF) + { + continue; + } + + for (ai = 0; ai < NALIGNS; ai++) + { + /* An operation that never touches the destination is decided + * by the source offset alone, so a pair repeating one already + * measured would report the same number twice. + */ + + if (!g_ops[oi].dst && bench_seen_src(ai)) + { + continue; + } + + bench_one(&g_ops[oi], g_sizes[si], + g_aligns[ai][0], g_aligns[ai][1]); + } + } + } + + free(g_src); + free(g_dst); + return 0; +} diff --git a/testing/libc/arch_libc/arch_libc_test_main.c b/testing/libc/arch_libc/arch_libc_test_main.c index 1a5e8bbf1db..9e0a222f146 100644 --- a/testing/libc/arch_libc/arch_libc_test_main.c +++ b/testing/libc/arch_libc/arch_libc_test_main.c @@ -44,6 +44,14 @@ #define TEST_REPEAT 100 #define MAX_ALIGN 16 +/**************************************************************************** + * Public Function Prototypes + ****************************************************************************/ + +#ifdef CONFIG_TESTING_ARCH_LIBC_BENCH +int arch_libc_bench(void); +#endif + /**************************************************************************** * Private Data ****************************************************************************/ @@ -1458,6 +1466,9 @@ int main(int argc, FAR char *argv[]) fail += test_strchrnul(); speed_strchrnul(); #endif +#ifdef CONFIG_TESTING_ARCH_LIBC_BENCH + fail += arch_libc_bench(); +#endif printf("arch_libc_test %s\n", fail ? "Failed" : "Passed"); return fail ? EXIT_FAILURE : EXIT_SUCCESS; From f4036a7341f70c59e904bc232e316d8da90dcdad Mon Sep 17 00:00:00 2001 From: Justin Hammond Date: Sat, 15 Aug 2026 15:13:10 +0800 Subject: [PATCH 2/4] testing/libc/arch_libc: Test strlcpy. strlcpy is the one function in this directory's reach that nothing here covers, and a machine directory may override it like any other. Sweep every source and destination alignment pair against sizes 1 to 64, and for each of those every capacity from zero to one past the length. Check the return value, which is the length of src whether or not the copy fit, the truncation point, the content, that a capacity of zero writes nothing at all, and that nothing lands past the terminator. The alignment pairs are the point. An implementation that walks one of the two pointers to a boundary and then copies a register at a time is correct whenever the two agree, so a test that only ever passes matching alignments says nothing about it. The timing half is guarded. perf_gettime() is not a system call, so an application reaches it only where the C library builds its own copy or where the application and the kernel are one image; calling it unconditionally leaves the test unbuildable on a kernel build, which is where the correctness half is still wanted. Assisted-by: Claude:claude-opus-5 Signed-off-by: Justin Hammond --- testing/libc/arch_libc/Kconfig | 4 + testing/libc/arch_libc/arch_libc_test_main.c | 117 +++++++++++++++++++ 2 files changed, 121 insertions(+) diff --git a/testing/libc/arch_libc/Kconfig b/testing/libc/arch_libc/Kconfig index 0de9563cc34..fec4ba9c131 100644 --- a/testing/libc/arch_libc/Kconfig +++ b/testing/libc/arch_libc/Kconfig @@ -43,6 +43,10 @@ config TESTING_ARCH_LIBC_STRCPY bool "test strcpy" default y +config TESTING_ARCH_LIBC_STRLCPY + bool "test strlcpy" + default y + config TESTING_ARCH_LIBC_STRLEN bool "test strlen" default y diff --git a/testing/libc/arch_libc/arch_libc_test_main.c b/testing/libc/arch_libc/arch_libc_test_main.c index 9e0a222f146..0f14f967c73 100644 --- a/testing/libc/arch_libc/arch_libc_test_main.c +++ b/testing/libc/arch_libc/arch_libc_test_main.c @@ -681,6 +681,117 @@ static void speed_strcpy(void) } #endif +/**************************************************************************** + * Name: test_strlcpy + ****************************************************************************/ + +#ifdef CONFIG_TESTING_ARCH_LIBC_STRLCPY +static int test_strlcpy(void) +{ + int size; + int fail = 0; + int ai; + size_t cap; + size_t ret; + size_t want; + + printf("Testing strlcpy...\n"); + + /* sa != da is the case that matters. An implementation that walks only + * one of the two pointers to a boundary and then copies a register at a + * time still passes every sa == da case. + */ + + for (ai = 0; ai < 64; ai++) + { + int da = ai / 8; + int sa = ai % 8; + + for (size = 1; size <= 64; size++) + { + fill_pattern(g_buf1 + sa, size); + g_buf1[sa + size] = '\0'; + + for (cap = 0; cap <= (size_t)size + 1; cap++) + { + memset(g_buf2, 0x5a, sizeof(g_buf2)); + ret = strlcpy(g_buf2 + da, g_buf1 + sa, cap); + + if (ret != (size_t)size) + { + printf(" FAIL ret: sa=%d da=%d size=%d cap=%zu got=%zu\n", + sa, da, size, cap, ret); + fail++; + continue; + } + + if (cap == 0) + { + /* Nothing may be written when there is no room */ + + if ((unsigned char)g_buf2[da] != 0x5a) + { + printf(" FAIL cap0 wrote: sa=%d da=%d\n", sa, da); + fail++; + } + + continue; + } + + want = (size_t)size < cap - 1 ? (size_t)size : cap - 1; + + if (strlen(g_buf2 + da) != want || + memcmp(g_buf2 + da, g_buf1 + sa, want) != 0) + { + printf(" FAIL content: sa=%d da=%d size=%d cap=%zu\n", + sa, da, size, cap); + fail++; + } + else if ((unsigned char)g_buf2[da + want + 1] != 0x5a) + { + printf(" FAIL overrun: sa=%d da=%d size=%d cap=%zu\n", + sa, da, size, cap); + fail++; + } + } + } + } + + printf("strlcpy: %s\n", fail ? "FAILED" : "PASSED"); + return fail; +} + +/* perf_gettime() reaches an application only where the C library builds its + * own copy, or where the application and the kernel are one image. + */ + +#if defined(CONFIG_ARCH_HAVE_PERF_EVENTS_USER_ACCESS) || \ + defined(CONFIG_BUILD_FLAT) +# define ARCH_LIBC_HAVE_PERF 1 +#endif + +#ifdef ARCH_LIBC_HAVE_PERF +static void speed_strlcpy(void) +{ + clock_t start; + clock_t end; + int i; + + fill_pattern(g_buf1, 128); + g_buf1[128] = '\0'; + start = perf_gettime(); + for (i = 0; i < TEST_REPEAT; i++) + { + g_sink = strlcpy(g_buf2, g_buf1, sizeof(g_buf2)); + } + + end = perf_gettime(); + printf("strlcpy(128) avg cycles: %ju\n", + (uintmax_t)(end - start) / TEST_REPEAT); +} +#endif +#endif + /**************************************************************************** * Name: test_strchr ****************************************************************************/ @@ -1434,6 +1545,12 @@ int main(int argc, FAR char *argv[]) fail += test_strcpy(); speed_strcpy(); #endif +#ifdef CONFIG_TESTING_ARCH_LIBC_STRLCPY + fail += test_strlcpy(); +# ifdef ARCH_LIBC_HAVE_PERF + speed_strlcpy(); +# endif +#endif #ifdef CONFIG_TESTING_ARCH_LIBC_STRCHR fail += test_strchr(); speed_strchr(); From 9360fe3ade52acd38f34ec1602264cc5f752b0d1 Mon Sep 17 00:00:00 2001 From: Justin Hammond Date: Sat, 15 Aug 2026 16:20:30 +0800 Subject: [PATCH 3/4] testing/libc/arch_libc: Test memccpy and stpncpy. Neither is covered here, and both are overridable, so a machine or libc implementation of either goes in unmeasured and unchecked. memccpy is checked with the search character present, where the copy stops just past it and the result points there, and absent, where the whole length is copied and the result is NULL. stpncpy is checked against every capacity from zero to four past the length, for the content, the zero padding beyond the terminator, and the returned pointer, which is the terminator when the string fits and one past the end when it does not. Both sweep all sixty four source and destination alignment pairs, and both are added to the benchmark, which now covers nineteen functions. Assisted-by: Claude:claude-opus-5 Signed-off-by: Justin Hammond --- testing/libc/arch_libc/Kconfig | 8 + testing/libc/arch_libc/arch_libc_bench.c | 10 + testing/libc/arch_libc/arch_libc_test_main.c | 189 +++++++++++++++++++ 3 files changed, 207 insertions(+) diff --git a/testing/libc/arch_libc/Kconfig b/testing/libc/arch_libc/Kconfig index fec4ba9c131..b90019a7fd8 100644 --- a/testing/libc/arch_libc/Kconfig +++ b/testing/libc/arch_libc/Kconfig @@ -43,6 +43,14 @@ config TESTING_ARCH_LIBC_STRCPY bool "test strcpy" default y +config TESTING_ARCH_LIBC_MEMCCPY + bool "test memccpy" + default y + +config TESTING_ARCH_LIBC_STPNCPY + bool "test stpncpy" + default y + config TESTING_ARCH_LIBC_STRLCPY bool "test strlcpy" default y diff --git a/testing/libc/arch_libc/arch_libc_bench.c b/testing/libc/arch_libc/arch_libc_bench.c index d73fd48f9cc..af5bb4dd9da 100644 --- a/testing/libc/arch_libc/arch_libc_bench.c +++ b/testing/libc/arch_libc/arch_libc_bench.c @@ -92,6 +92,8 @@ #define OP_STPCPY 14 #define OP_STRNCPY 15 #define OP_STRCAT 16 +#define OP_MEMCCPY 17 +#define OP_STPNCPY 18 /**************************************************************************** * Private Types @@ -134,6 +136,8 @@ static const struct bench_op_s g_ops[] = {"stpcpy", OP_STPCPY, true, true}, {"strncpy", OP_STRNCPY, true, true}, {"strcat", OP_STRCAT, true, true}, + {"memccpy", OP_MEMCCPY, true, false}, + {"stpncpy", OP_STPNCPY, true, true}, }; static const size_t g_sizes[] = @@ -324,6 +328,12 @@ static void bench_one(FAR const struct bench_op_s *o, size_t n, case OP_STRNCPY: strncpy(d, s, n); break; + case OP_MEMCCPY: + g_sink += (uintptr_t)memccpy(d, s, '~', n); + break; + case OP_STPNCPY: + g_sink += (uintptr_t)stpncpy(d, s, n); + break; case OP_STRCAT: /* Appending to what the last turn appended would grow the diff --git a/testing/libc/arch_libc/arch_libc_test_main.c b/testing/libc/arch_libc/arch_libc_test_main.c index 0f14f967c73..1382bd62e6f 100644 --- a/testing/libc/arch_libc/arch_libc_test_main.c +++ b/testing/libc/arch_libc/arch_libc_test_main.c @@ -792,6 +792,183 @@ static void speed_strlcpy(void) #endif #endif +/**************************************************************************** + * Name: test_memccpy + ****************************************************************************/ + +#ifdef CONFIG_TESTING_ARCH_LIBC_MEMCCPY +static int test_memccpy(void) +{ + int size; + int fail = 0; + int ai; + int i; + + printf("Testing memccpy...\n"); + + for (ai = 0; ai < 64; ai++) + { + int da = ai / 8; + int sa = ai % 8; + + for (size = 1; size <= 64; size++) + { + FAR char *p; + int at = size / 2; + + fill_pattern(g_buf1 + sa, size); + g_buf1[sa + at] = '#'; + memset(g_buf2, 0x5a, sizeof(g_buf2)); + + /* The character is present, so the copy stops just past it */ + + p = memccpy(g_buf2 + da, g_buf1 + sa, '#', size); + if (p != g_buf2 + da + at + 1 || + memcmp(g_buf2 + da, g_buf1 + sa, at + 1) != 0 || + (unsigned char)g_buf2[da + at + 1] != 0x5a) + { + printf(" FAIL found: sa=%d da=%d size=%d\n", sa, da, size); + fail++; + } + + /* The character is absent, so the whole length is copied */ + + for (i = 0; i < size; i++) + { + g_buf1[sa + i] = 'A' + (i % 26); + } + + memset(g_buf2, 0x5a, sizeof(g_buf2)); + p = memccpy(g_buf2 + da, g_buf1 + sa, '#', size); + if (p != NULL || memcmp(g_buf2 + da, g_buf1 + sa, size) != 0 || + (unsigned char)g_buf2[da + size] != 0x5a) + { + printf(" FAIL absent: sa=%d da=%d size=%d\n", sa, da, size); + fail++; + } + } + } + + printf("memccpy: %s\n", fail ? "FAILED" : "PASSED"); + return fail; +} + +#ifdef ARCH_LIBC_HAVE_PERF +static void speed_memccpy(void) +{ + clock_t start; + clock_t end; + int i; + + fill_pattern(g_buf1, 128); + start = perf_gettime(); + for (i = 0; i < TEST_REPEAT; i++) + { + g_sink = (uintptr_t)memccpy(g_buf2, g_buf1, '#', 128); + } + + end = perf_gettime(); + printf("memccpy(128) avg cycles: %ju\n", + (uintmax_t)(end - start) / TEST_REPEAT); +} +#endif +#endif + +/**************************************************************************** + * Name: test_stpncpy + ****************************************************************************/ + +#ifdef CONFIG_TESTING_ARCH_LIBC_STPNCPY +static int test_stpncpy(void) +{ + int size; + int fail = 0; + int ai; + int cap; + int i; + + printf("Testing stpncpy...\n"); + + for (ai = 0; ai < 64; ai++) + { + int da = ai / 8; + int sa = ai % 8; + + for (size = 1; size <= 48; size++) + { + fill_pattern(g_buf1 + sa, size); + g_buf1[sa + size] = '\0'; + + for (cap = 0; cap <= size + 4; cap++) + { + FAR char *p; + FAR char *want; + + memset(g_buf2, 0x5a, sizeof(g_buf2)); + p = stpncpy(g_buf2 + da, g_buf1 + sa, cap); + + /* Up to the terminator is copied, the rest is padded, and + * the result points at the terminator or one past the end. + */ + + want = size < cap ? g_buf2 + da + size : g_buf2 + da + cap; + if (p != want) + { + printf(" FAIL ret: sa=%d da=%d size=%d cap=%d\n", + sa, da, size, cap); + fail++; + continue; + } + + for (i = 0; i < cap; i++) + { + char expect = i < size ? g_buf1[sa + i] : '\0'; + + if (g_buf2[da + i] != expect) + { + printf(" FAIL content: sa=%d da=%d size=%d cap=%d\n", + sa, da, size, cap); + fail++; + break; + } + } + + if ((unsigned char)g_buf2[da + cap] != 0x5a) + { + printf(" FAIL overrun: sa=%d da=%d size=%d cap=%d\n", + sa, da, size, cap); + fail++; + } + } + } + } + + printf("stpncpy: %s\n", fail ? "FAILED" : "PASSED"); + return fail; +} + +#ifdef ARCH_LIBC_HAVE_PERF +static void speed_stpncpy(void) +{ + clock_t start; + clock_t end; + int i; + + fill_pattern(g_buf1, 128); + g_buf1[128] = '\0'; + start = perf_gettime(); + for (i = 0; i < TEST_REPEAT; i++) + { + g_sink = (uintptr_t)stpncpy(g_buf2, g_buf1, 128); + } + + end = perf_gettime(); + printf("stpncpy(128) avg cycles: %ju\n", + (uintmax_t)(end - start) / TEST_REPEAT); +} +#endif +#endif + /**************************************************************************** * Name: test_strchr ****************************************************************************/ @@ -1545,6 +1722,18 @@ int main(int argc, FAR char *argv[]) fail += test_strcpy(); speed_strcpy(); #endif +#ifdef CONFIG_TESTING_ARCH_LIBC_MEMCCPY + fail += test_memccpy(); +# ifdef ARCH_LIBC_HAVE_PERF + speed_memccpy(); +# endif +#endif +#ifdef CONFIG_TESTING_ARCH_LIBC_STPNCPY + fail += test_stpncpy(); +# ifdef ARCH_LIBC_HAVE_PERF + speed_stpncpy(); +# endif +#endif #ifdef CONFIG_TESTING_ARCH_LIBC_STRLCPY fail += test_strlcpy(); # ifdef ARCH_LIBC_HAVE_PERF From c915202a5163740c8b74be4d4282bc803eb4d381 Mon Sep 17 00:00:00 2001 From: Justin Hammond Date: Sat, 15 Aug 2026 17:31:49 +0800 Subject: [PATCH 4/4] testing/libc/arch_libc: Time against a clock that runs. Every measurement repeats until a stated interval has passed, so a clock that reads the same value twice does not slow the benchmark down, it stops it returning at all. CLOCK_MONOTONIC does not advance on every target. On qemu-intel64 it reports success and stays at zero, while CLOCK_REALTIME advances normally, and the benchmark spins in its first measurement with no output after the heading. Sample each candidate twice around a busy wait and take the first one whose reading changes. Where none does, say so and skip the timing rather than hang. Assisted-by: Claude:claude-opus-5 Signed-off-by: Justin Hammond --- testing/libc/arch_libc/arch_libc_bench.c | 56 +++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/testing/libc/arch_libc/arch_libc_bench.c b/testing/libc/arch_libc/arch_libc_bench.c index af5bb4dd9da..c24118e1a21 100644 --- a/testing/libc/arch_libc/arch_libc_bench.c +++ b/testing/libc/arch_libc/arch_libc_bench.c @@ -116,6 +116,7 @@ struct bench_op_s static FAR char *g_src; static FAR char *g_dst; static volatile unsigned long g_sink; +static clockid_t g_clock = CLOCK_MONOTONIC; static const struct bench_op_s g_ops[] = { @@ -182,10 +183,57 @@ static double bench_now(void) { struct timespec t; - clock_gettime(CLOCK_MONOTONIC, &t); + clock_gettime(g_clock, &t); return t.tv_sec + t.tv_nsec / 1e9; } +/**************************************************************************** + * Name: bench_pick_clock + * + * Description: + * Settle on a clock that runs. Every measurement below repeats until a + * stated interval has passed, so a clock that reads the same value twice + * would spin forever rather than report anything. CLOCK_MONOTONIC is + * preferred and does not advance on every target. + * + ****************************************************************************/ + +static bool bench_pick_clock(void) +{ + static const clockid_t tries[] = + { + CLOCK_MONOTONIC, CLOCK_REALTIME + }; + + struct timespec a; + struct timespec b; + volatile int i; + size_t k; + + for (k = 0; k < sizeof(tries) / sizeof(tries[0]); k++) + { + if (clock_gettime(tries[k], &a) < 0) + { + continue; + } + + for (i = 0; i < 1000000; i++); + + if (clock_gettime(tries[k], &b) < 0) + { + continue; + } + + if (b.tv_sec != a.tv_sec || b.tv_nsec != a.tv_nsec) + { + g_clock = tries[k]; + return true; + } + } + + return false; +} + /**************************************************************************** * Name: bench_seen_src * @@ -385,6 +433,12 @@ int arch_libc_bench(void) size_t si; size_t ai; + if (!bench_pick_clock()) + { + printf("arch_libc bench: no clock advances, cannot time anything\n"); + return 1; + } + g_src = malloc(BENCH_BUF); g_dst = malloc(BENCH_BUF); if (g_src == NULL || g_dst == NULL)