* [PATCH v7] kunit: memcpy: Add benchmark
@ 2026-11-10 2:16 Matteo Croce
2026-11-10 2:20 ` Matteo Croce
2026-11-10 3:39 ` Andrew Morton
0 siblings, 2 replies; 5+ messages in thread
From: Matteo Croce @ 2026-11-10 2:16 UTC (permalink / raw)
To: Brendan Higgins, David Gow, Andrew Morton, David Laight
Cc: linux-kselftest, linux-kernel
Add optional benchmarks for memcpy() and memmove(), enabled with
CONFIG_MEMCPY_KUNIT_BENCHMARK.
Each function is run over several buffer sizes, up to 4 MiB, with both
aligned and unaligned buffers, to spot platforms where unaligned
accesses have a noticeable performance impact.
The copy length is hidden from the compiler, so the architecture's
out-of-line implementation is always measured rather than an inlined
constant-size copy.
Sample output on a RISC-V machine:
# modprobe memcpy_kunit
KTAP version 1
1..1
KTAP version 1
# Subtest: memcpy
# module: memcpy_kunit
1..8
[...]
# memcpy_bench_test: memcpy: aligned copy of len 2: 3 MB/s
# memcpy_bench_test: memcpy: unaligned copy of len 2: 4 MB/s
# memcpy_bench_test: memcpy: aligned copy of len 64: 127 MB/s
# memcpy_bench_test: memcpy: unaligned copy of len 64: 117 MB/s
# memcpy_bench_test: memcpy: aligned copy of len 256: 508 MB/s
# memcpy_bench_test: memcpy: unaligned copy of len 256: 542 MB/s
# memcpy_bench_test: memcpy: aligned copy of len 4194304: 848 MB/s
# memcpy_bench_test: memcpy: unaligned copy of len 4194304: 699 MB/s
# memcpy_bench_test.speed: slow
ok 7 memcpy_bench_test
# memmove_bench_test: memmove: aligned move of len 64: 121 MB/s
# memmove_bench_test: memmove: unaligned move of len 64: 104 MB/s
# memmove_bench_test: memmove: aligned move of len 256: 445 MB/s
# memmove_bench_test: memmove: unaligned move of len 256: 410 MB/s
# memmove_bench_test: memmove: aligned move of len 4194304: 1419 MB/s
# memmove_bench_test: memmove: unaligned move of len 4194304: 1360 MB/s
# memmove_bench_test.speed: slow
ok 8 memmove_bench_test
# memcpy: pass:8 fail:0 skip:0 total:8
# Totals: pass:8 fail:0 skip:0 total:8
ok 1 memcpy
Signed-off-by: Matteo Croce <technoboy85@gmail.com>
---
v6 -> v7:
- subject: "KUnit: memcpy: add benchmark" -> "kunit: memcpy: Add benchmark"
- memmove: make the aligned case actually aligned
- fix int overflow in throughput calculation, cap max size to 4 MiB
- hide copy length from the compiler (OPTIMIZER_HIDE_VAR)
- always register the bench cases, kunit_skip() when disabled
- Kconfig: move under MEMCPY_KUNIT_TEST, reword prompt and help
- update sample output
v6: https://lore.kernel.org/all/20260302023052.73943-1-teknoraver@meta.com/
lib/Kconfig.debug | 12 ++++
lib/tests/memcpy_kunit.c | 120 +++++++++++++++++++++++++++++++++++++++
2 files changed, 132 insertions(+)
diff --git a/lib/Kconfig.debug b/lib/Kconfig.debug
index 6b24a2c89b84..7884a9a0f78f 100644
--- a/lib/Kconfig.debug
+++ b/lib/Kconfig.debug
@@ -3006,6 +3006,18 @@ config MEMCPY_KUNIT_TEST
If unsure, say N.
+config MEMCPY_KUNIT_BENCHMARK
+ bool "Benchmark memcpy() and memmove() functions at runtime"
+ depends on MEMCPY_KUNIT_TEST
+ help
+ Adds benchmark cases to the memcpy KUnit test suite, measuring
+ the throughput of memcpy() and memmove() over several buffer
+ sizes, with both aligned and unaligned buffers. This helps to
+ spot architectures where unaligned accesses have a noticeable
+ performance impact. Results are printed in the KUnit log.
+
+ If unsure, say N.
+
config MIN_HEAP_KUNIT_TEST
tristate "Min heap test" if !KUNIT_ALL_TESTS
depends on KUNIT
diff --git a/lib/tests/memcpy_kunit.c b/lib/tests/memcpy_kunit.c
index d36933554e46..7b7103a72ada 100644
--- a/lib/tests/memcpy_kunit.c
+++ b/lib/tests/memcpy_kunit.c
@@ -11,6 +11,7 @@
#include <linux/mm.h>
#include <linux/module.h>
#include <linux/overflow.h>
+#include <linux/sizes.h>
#include <linux/slab.h>
#include <linux/types.h>
#include <linux/vmalloc.h>
@@ -493,6 +494,123 @@ static void memmove_overlap_test(struct kunit *test)
}
}
+#define COPIES_NUM 100
+#define BENCH_MAX_SIZE min_t(size_t, SZ_4M, KMALLOC_MAX_SIZE)
+
+static void memcpy_bench_size_align(struct kunit *test, int size, bool unalign)
+{
+ u64 start, end, total_ns = 0;
+ char *dst, *src;
+ int ret = 0;
+
+ /* Prevent constant-size inlining: always call the arch memcpy() */
+ OPTIMIZER_HIDE_VAR(size);
+
+ dst = kzalloc(size, GFP_KERNEL);
+ if (!dst) {
+ ret = -ENOMEM;
+ goto out;
+ }
+
+ src = kzalloc(size, GFP_KERNEL);
+ if (!src) {
+ ret = -ENOMEM;
+ goto out_free;
+ }
+
+ for (int i = 0; i < COPIES_NUM; i++) {
+ local_irq_disable();
+ start = ktime_get_ns();
+ memcpy(dst + unalign, src, size - unalign);
+ end = ktime_get_ns();
+ local_irq_enable();
+ total_ns += end - start;
+ }
+
+ /* Avoid division by zero */
+ if (!total_ns)
+ total_ns = 1;
+
+ kunit_info(test, "memcpy: %saligned copy of len %d: %llu MB/s",
+ unalign ? "un" : "", size,
+ div64_u64((u64)COPIES_NUM * size * NSEC_PER_SEC,
+ total_ns * 1024 * 1024));
+
+ kfree(src);
+
+out_free:
+ kfree(dst);
+
+out:
+ KUNIT_ASSERT_EQ(test, ret, 0);
+}
+
+static void memcpy_bench_size(struct kunit *test, int size)
+{
+ memcpy_bench_size_align(test, size, false);
+ memcpy_bench_size_align(test, size, true);
+}
+
+static void memcpy_bench_test(struct kunit *test)
+{
+ if (!IS_ENABLED(CONFIG_MEMCPY_KUNIT_BENCHMARK))
+ kunit_skip(test, "not enabled");
+
+ memcpy_bench_size(test, 2);
+ memcpy_bench_size(test, 64);
+ memcpy_bench_size(test, 256);
+ memcpy_bench_size(test, BENCH_MAX_SIZE);
+}
+
+static void memmove_bench_size_align(struct kunit *test, int size, bool unalign)
+{
+ u64 start, end, total_ns = 0;
+ char *buf;
+ /* Keep the destination word aligned in the aligned case */
+ const int shift = ALIGN(size / 10, sizeof(long));
+
+ /* Prevent constant-size inlining: always call the arch memmove() */
+ OPTIMIZER_HIDE_VAR(size);
+
+ buf = kzalloc(size, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, buf);
+
+ for (int i = 0; i < COPIES_NUM; i++) {
+ local_irq_disable();
+ start = ktime_get_ns();
+ memmove(buf + shift + unalign, buf, size - shift - unalign);
+ end = ktime_get_ns();
+ local_irq_enable();
+ total_ns += end - start;
+ }
+
+ if (!total_ns)
+ total_ns = 1;
+
+ kunit_info(test, "memmove: %saligned move of len %d: %llu MB/s",
+ unalign ? "un" : "", size,
+ div64_u64((u64)COPIES_NUM * (size - shift) * NSEC_PER_SEC,
+ total_ns * 1024 * 1024));
+
+ kfree(buf);
+}
+
+static void memmove_bench_size(struct kunit *test, int size)
+{
+ memmove_bench_size_align(test, size, false);
+ memmove_bench_size_align(test, size, true);
+}
+
+static void memmove_bench_test(struct kunit *test)
+{
+ if (!IS_ENABLED(CONFIG_MEMCPY_KUNIT_BENCHMARK))
+ kunit_skip(test, "not enabled");
+
+ memmove_bench_size(test, 64);
+ memmove_bench_size(test, 256);
+ memmove_bench_size(test, BENCH_MAX_SIZE);
+}
+
static struct kunit_case memcpy_test_cases[] = {
KUNIT_CASE(memset_test),
KUNIT_CASE(memcpy_test),
@@ -500,6 +618,8 @@ static struct kunit_case memcpy_test_cases[] = {
KUNIT_CASE_SLOW(memmove_test),
KUNIT_CASE_SLOW(memmove_large_test),
KUNIT_CASE_SLOW(memmove_overlap_test),
+ KUNIT_CASE_SLOW(memcpy_bench_test),
+ KUNIT_CASE_SLOW(memmove_bench_test),
{}
};
--
2.56.0
^ permalink raw reply related [flat|nested] 5+ messages in thread
* Re: [PATCH v7] kunit: memcpy: Add benchmark
2026-11-10 2:16 [PATCH v7] kunit: memcpy: Add benchmark Matteo Croce
@ 2026-11-10 2:20 ` Matteo Croce
2026-11-10 3:39 ` Andrew Morton
1 sibling, 0 replies; 5+ messages in thread
From: Matteo Croce @ 2026-11-10 2:20 UTC (permalink / raw)
To: Brendan Higgins, David Gow, Andrew Morton, David Laight,
Kees Cook, linux-hardening
Cc: linux-kselftest, linux-kernel
+Cc Kees and linux-hardening, as per get_maintainer.pl
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [PATCH v7] kunit: memcpy: Add benchmark
2026-11-10 2:16 [PATCH v7] kunit: memcpy: Add benchmark Matteo Croce
2026-11-10 2:20 ` Matteo Croce
@ 2026-11-10 3:39 ` Andrew Morton
2026-11-10 10:01 ` David Laight
1 sibling, 1 reply; 5+ messages in thread
From: Andrew Morton @ 2026-11-10 3:39 UTC (permalink / raw)
To: Matteo Croce
Cc: Brendan Higgins, David Gow, David Laight, linux-kselftest,
linux-kernel, Kees Cook, linux-hardening
On Tue, 10 Nov 2026 03:16:22 +0100 Matteo Croce <technoboy85@gmail.com> wrote:
> Add optional benchmarks for memcpy() and memmove(), enabled with
> CONFIG_MEMCPY_KUNIT_BENCHMARK.
hm, sorry, I must ask "why". Nowadays optimizing memcpy() is a
super-specialized thing undertaken by the bravest architecture
specialists who are unlikely to rely upon a generic kunit test for the
purpose!
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [PATCH v7] kunit: memcpy: Add benchmark
2026-11-10 3:39 ` Andrew Morton
@ 2026-11-10 10:01 ` David Laight
2026-11-11 17:19 ` Matteo Croce
0 siblings, 1 reply; 5+ messages in thread
From: David Laight @ 2026-11-10 10:01 UTC (permalink / raw)
To: Andrew Morton
Cc: Matteo Croce, Brendan Higgins, David Gow, linux-kselftest,
linux-kernel, Kees Cook, linux-hardening
On Mon, 9 Nov 2026 19:39:25 -0800
Andrew Morton <akpm@linux-foundation.org> wrote:
> On Tue, 10 Nov 2026 03:16:22 +0100 Matteo Croce <technoboy85@gmail.com> wrote:
>
> > Add optional benchmarks for memcpy() and memmove(), enabled with
> > CONFIG_MEMCPY_KUNIT_BENCHMARK.
>
> hm, sorry, I must ask "why". Nowadays optimizing memcpy() is a
> super-specialized thing undertaken by the bravest architecture
> specialists who are unlikely to rely upon a generic kunit test for the
> purpose!
You also want a comparison against the 'obvious loops' (which it can
take some effort to generate because the compiler tends to convert them
to memcpy()). Otherwise the numbers are pretty meaningless.
But kernel benchmarks are really the wrong place.
The 'bravest architecture specialists' are going to test the code
in userspace where multiple algorithms can be tested and quickly
tweaked/dismissed.
You also need a bank of systems with different cpu variants and run the
code on all on them - much easier if you only need to copy a program across.
I also (sometimes being one of the 'bravest architecture specialists')
try to work out how many bytes/clock the a copy (or ipcsum) is doing
and what the fixed costs are.
That actually tells you something useful.
(But you always need to check that the compiler generated something like
the expected copy - otherwise it is pretty pointless.)
David
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [PATCH v7] kunit: memcpy: Add benchmark
2026-11-10 10:01 ` David Laight
@ 2026-11-11 17:19 ` Matteo Croce
0 siblings, 0 replies; 5+ messages in thread
From: Matteo Croce @ 2026-11-11 17:19 UTC (permalink / raw)
To: David Laight
Cc: Andrew Morton, Brendan Higgins, linux-kselftest, linux-kernel,
Kees Cook, linux-hardening
On Tue, 10 Nov 2026 10:01:16 +0000
David Laight <david.laight.linux@gmail.com> wrote:
> On Mon, 9 Nov 2026 19:39:25 -0800
> Andrew Morton <akpm@linux-foundation.org> wrote:
>
> > hm, sorry, I must ask "why". Nowadays optimizing memcpy() is a
> > super-specialized thing undertaken by the bravest architecture
> > specialists who are unlikely to rely upon a generic kunit test for the
> > purpose!
>
> But kernel benchmarks are really the wrong place.
> The 'bravest architecture specialists' are going to test the code
> in userspace where multiple algorithms can be tested and quickly
> tweaked/dismissed.
Hi Andrew, David,
Fair question. I'm not one of the bravest, but I used this test to
develop and measure a fix for a real problem: the RISC-V memcpy()
falls back to a byte-by-byte copy whenever source and destination
have different alignment, and on a Milk-V Megrez that made unaligned
copies run at about a third of the aligned speed. The fix[1] does a
shifted word copy for that case:
Before:
memcpy: aligned copy of 400 MBytes in 429 msecs (931 MB/s)
memcpy: unaligned copy of 400 MBytes in 1202 msecs (332 MB/s)
After:
memcpy: aligned copy of 400 MBytes in 428 msecs (933 MB/s)
memcpy: unaligned copy of 400 MBytes in 519 msecs (770 MB/s)
and it's visible outside the benchmark too: UDP RX on the board's
stmmac NIC went from 672 to 827 Mbps with 512 byte frames.
That slow path had been in the kernel for years, and I think that's
the point: the specialists tune the hot path on the hardware they
care about, while a slow path on some other SoC can go unseen.
I don't see this as an alternative to userspace testing, I do both.
I develop the assembly in a standalone harness[2], which checks every
length and alignment combination, and times ns per call over a range
of sizes against glibc's memcpy() and a few other implementations
(a vector one, C-SKY's, a glibc shifted-copy patch). That's where
the variants get tried and dismissed.
But the harness runs a hand-copied memcpy.S as userspace code, which
can use things the kernel can't, like the vector unit. The KUnit test
checks the real thing: the kernel's own memcpy(), as built for the
running kernel and, on architectures that do so, patched at boot for
the running CPU. It also lets anyone else check their hardware with
a modprobe, and can run unattended in CI.
There is precedent in lib/: CONFIG_CRC_BENCHMARK and
CONFIG_CRYPTO_LIB_BENCHMARK add opt-in benchmarks to the CRC and
crypto library KUnit suites in the same way.
[1] https://lore.kernel.org/linux-riscv/20260311173138.1820-1-teknoraver@meta.com/
[2] https://github.com/teknoraver/riscv-memcpy
> You also want a comparison against the 'obvious loops' (which it can
> take some effort to generate because the compiler tends to convert them
> to memcpy()). Otherwise the numbers are pretty meaningless.
>
> I also (sometimes being one of the 'bravest architecture specialists')
> try to work out how many bytes/clock the a copy (or ipcsum) is doing
> and what the fixed costs are.
Agreed, and the sample output shows the problem: timing every call
with ktime_get_ns() means the small sizes mostly measure the clock
read. For v8 I'd:
- time the whole batch of copies instead of each call, as the
harness already does
- print the per-call cost in ns next to the throughput, so the fixed
cost is visible (and say MiB/s, which is what it actually computes)
- add a byte-at-a-time baseline, with OPTIMIZER_HIDE_VAR() on the
pointers inside the loop so it can't be turned back into memcpy()
The baseline would have made the RISC-V case obvious at a glance:
the unaligned memcpy() would have matched it, since it was itself a
byte loop.
For bytes/clock I'd rather not use get_cycles(), as on several
architectures it doesn't tick at the core clock; ns per call at a known
size should let you derive it on a given machine.
Would that address your concerns?
Regards,
--
per aspera ad upstream
^ permalink raw reply [flat|nested] 5+ messages in thread
end of thread, other threads:[~2026-11-11 17:19 UTC | newest]
Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-10 2:16 [PATCH v7] kunit: memcpy: Add benchmark Matteo Croce
2026-11-10 2:20 ` Matteo Croce
2026-11-10 3:39 ` Andrew Morton
2026-11-10 10:01 ` David Laight
2026-11-11 17:19 ` Matteo Croce
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.