From dc0520d2e7724053b32ccdf812143769d536e7ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Houz=C3=A9?= Date: Wed, 22 Jul 2026 14:21:56 +0200 Subject: [PATCH 1/4] P0293MIMXRT1170EVK-170 Add System.arraycopy throughput benchmark to core validation Migrate the standalone arraycopy measurement into vee-port/validation/core as a regression test. ArrayCopyPerformance measures aligned and misaligned System.arraycopy throughput on large byte[] buffers and asserts each against a configurable minimum-throughput system property. Configured board-neutrally for this template: buffer size 256 KB, Java heap raised to 1 MB, and both throughput thresholds left unset so the checks are no-ops on any board. RT1170 reference values (125/65 MB/s) are documented in the testsuite properties for boards that opt into enforcement. --- .../core/tests/ArrayCopyPerformance.java | 184 ++++++++++++++++++ .../microej-testsuite-common.properties | 22 ++- 2 files changed, 205 insertions(+), 1 deletion(-) create mode 100644 vee-port/validation/core/src/test/java/com/microej/core/tests/ArrayCopyPerformance.java diff --git a/vee-port/validation/core/src/test/java/com/microej/core/tests/ArrayCopyPerformance.java b/vee-port/validation/core/src/test/java/com/microej/core/tests/ArrayCopyPerformance.java new file mode 100644 index 0000000..1a87303 --- /dev/null +++ b/vee-port/validation/core/src/test/java/com/microej/core/tests/ArrayCopyPerformance.java @@ -0,0 +1,184 @@ +/* + * Java + * + * Copyright 2026 MicroEJ Corp. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be found with this software. + * + * Build: 7E4D1F7C + */ +package com.microej.core.tests; + +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import ej.bon.Util; + +/** + * Measures {@link System#arraycopy(Object, int, Object, int, int)} throughput on large {@code byte[]} + * buffers and checks it against a minimum expected throughput. + *

+ * On byte arrays {@link System#arraycopy(Object, int, Object, int, int)} resolves to a direct tail + * call into the C library {@code memmove}. Its performance depends on the BSP libc configuration + * (e.g. newlib-nano versus full newlib) and on whether the BSP overrides {@code memmove} with a word-wide + * implementation. This test guards against a regression to a slow byte-wise copy. + *

+ * The buffers are far larger than a typical CPU data cache, so the copy exercises real memory + * bandwidth rather than staying cache-resident. Two copies are measured and each is asserted against + * its own minimum: + *

+ *

+ * The minimum expected throughputs (in MB/s) are read from the system properties + * {@value #PROPERTY_PREFIX}{@value #OPTION_MIN_THROUGHPUT_MBPS} (aligned) and + * {@value #PROPERTY_PREFIX}{@value #OPTION_MIN_THROUGHPUT_MISALIGNED_MBPS} (misaligned). When a + * property is not set its expectation defaults to zero, so the corresponding check is a no-op on VEE + * Ports that do not declare an expectation. + */ +public class ArrayCopyPerformance { + + private static final String PROPERTY_PREFIX = "com.microej.core.tests."; + + /** + * Option that specifies the minimum expected aligned {@link System#arraycopy} throughput, in MB/s. + * When unset, the aligned throughput is only logged and its check passes. + */ + private static final String OPTION_MIN_THROUGHPUT_MBPS = "arraycopy.min.throughput.mbps"; + + /** + * Option that specifies the minimum expected misaligned {@link System#arraycopy} throughput, in + * MB/s. When unset, the misaligned throughput is only logged and its check passes. + */ + private static final String OPTION_MIN_THROUGHPUT_MISALIGNED_MBPS = "arraycopy.min.throughput.misaligned.mbps"; + + /** + * Option that specifies the size in bytes of each of the two working buffers. Larger buffers + * exceed the CPU data cache and better exercise real memory bandwidth; smaller targets can lower + * this to fit the available heap. When unset, {@value #DEFAULT_BUFFER_SIZE} bytes are used. + */ + private static final String OPTION_BUFFER_SIZE_BYTES = "arraycopy.buffer.size.bytes"; + + /** + * Fully-qualified name of {@link #OPTION_BUFFER_SIZE_BYTES}, resolved once at class load. + * {@link #testArrayCopyThroughput()} looks the option up through this precomputed name with + * {@link System#getProperty(String)}, which does not allocate on the heap when the option is unset. + * This keeps the working buffers the first heap allocations in the test, so their base address + * stays stable and well-aligned (word-wide copy throughput is sensitive to that alignment). + */ + private static final String PROPERTY_BUFFER_SIZE_BYTES = PROPERTY_PREFIX + OPTION_BUFFER_SIZE_BYTES; + + private static final int DEFAULT_MIN_THROUGHPUT_MBPS = 0; + + /** Default size in bytes of each of the two working buffers (2 MB). */ + private static final int DEFAULT_BUFFER_SIZE = 2 * 1024 * 1024; + + /** Number of untimed warm-up copies (stabilizes the caches). */ + private static final int WARMUP_ITERATIONS = 2; + + /** Number of timed copies averaged into the throughput figure. */ + private static final int TIMED_ITERATIONS = 20; + + /** Destination offset, in bytes, used to force a relatively misaligned copy. */ + private static final int MISALIGN_OFFSET = 1; + + /** Number of nanoseconds in one second. */ + private static final long NS_PER_SECOND = 1_000_000_000L; + + /** Number of bytes in one megabyte, using the decimal (MB) convention. */ + private static final long BYTES_PER_MB = 1_000_000L; + + /** + * Measures aligned and misaligned {@link System#arraycopy} throughput and asserts each meets its + * configured minimum. + */ + @Test + public void testArrayCopyThroughput() { + // Resolve the buffer size WITHOUT allocating on the heap first, so the two working buffers stay + // the first heap allocations in this method and keep a stable, well-aligned base address (the + // aligned word-wide copy throughput is sensitive to that alignment). System.getProperty returns + // an existing reference or null (no allocation), and Integer.parseInt returns a primitive + // (no allocation on its success path); the resolved value is only logged after allocation. + int bufferSize = DEFAULT_BUFFER_SIZE; + String configuredBufferSize = System.getProperty(PROPERTY_BUFFER_SIZE_BYTES); + if (configuredBufferSize != null) { + bufferSize = Integer.parseInt(configuredBufferSize); + } + byte[] src = new byte[bufferSize]; + byte[] dst = new byte[bufferSize]; + + System.out.println("Property '" + PROPERTY_BUFFER_SIZE_BYTES + "' = " + bufferSize); + + // Fill source with a non-zero pattern so the copy cannot be optimized away. + for (int i = 0; i < bufferSize; i++) { + src[i] = (byte) i; + } + + long alignedMBps = measureThroughput(src, dst, 0, 0, bufferSize); + long misalignedMBps = measureThroughput(src, dst, 0, MISALIGN_OFFSET, bufferSize - MISALIGN_OFFSET); + + System.out.println("System.arraycopy throughput (aligned) : " + alignedMBps + " MB/s"); + System.out.println("System.arraycopy throughput (misaligned) : " + misalignedMBps + " MB/s"); + + int minAlignedMBps = getOptionAsInt(OPTION_MIN_THROUGHPUT_MBPS, DEFAULT_MIN_THROUGHPUT_MBPS); + int minMisalignedMBps = getOptionAsInt(OPTION_MIN_THROUGHPUT_MISALIGNED_MBPS, DEFAULT_MIN_THROUGHPUT_MBPS); + + assertTrue("Aligned System.arraycopy throughput (" + alignedMBps + " MB/s) is below the required minimum (" + + minAlignedMBps + " MB/s)", alignedMBps >= minAlignedMBps); + assertTrue("Misaligned System.arraycopy throughput (" + misalignedMBps + " MB/s) is below the required minimum (" + + minMisalignedMBps + " MB/s)", misalignedMBps >= minMisalignedMBps); + } + + /** + * Times repeated copies of the given size and returns the measured throughput. + * + * @param src + * the source buffer. + * @param dst + * the destination buffer. + * @param srcOffset + * the offset of the first copied byte in the source buffer. + * @param dstOffset + * the offset of the first written byte in the destination buffer. + * @param size + * the number of bytes copied per iteration. + * @return the measured throughput, in MB/s (decimal megabytes per second). + */ + private static long measureThroughput(byte[] src, byte[] dst, int srcOffset, int dstOffset, int size) { + for (int i = 0; i < WARMUP_ITERATIONS; i++) { + System.arraycopy(src, srcOffset, dst, dstOffset, size); + } + + long startNs = Util.platformTimeNanos(); + for (int i = 0; i < TIMED_ITERATIONS; i++) { + System.arraycopy(src, srcOffset, dst, dstOffset, size); + } + long elapsedNs = Util.platformTimeNanos() - startNs; + + if (elapsedNs <= 0) { + return 0; + } + long totalBytes = (long) size * TIMED_ITERATIONS; + return (totalBytes * NS_PER_SECOND / elapsedNs) / BYTES_PER_MB; + } + + /** + * Reads an integer option from the system properties, logging the resolved value. + * + * @param optionName + * the option name, appended to {@value #PROPERTY_PREFIX}. + * @param defaultValue + * the value returned when the property is not set or not a valid integer. + * @return the resolved option value. + */ + private static int getOptionAsInt(String optionName, int defaultValue) { + String propertyName = PROPERTY_PREFIX + optionName; + int value = Integer.getInteger(propertyName, defaultValue).intValue(); + System.out.println("Property '" + propertyName + "' = " + value); + return value; + } +} diff --git a/vee-port/validation/core/validation/microej-testsuite-common.properties b/vee-port/validation/core/validation/microej-testsuite-common.properties index 1986c93..b8ec1d9 100644 --- a/vee-port/validation/core/validation/microej-testsuite-common.properties +++ b/vee-port/validation/core/validation/microej-testsuite-common.properties @@ -1,13 +1,33 @@ # Testsuite Application Options +# +# Build: 7E4D1F7C # Java memory settings core.memory.immortal.memory=RAM core.memory.immortal.size=4096 core.memory.javaheap.memory=RAM -core.memory.javaheap.size=32768 +# The ArrayCopyPerformance benchmark allocates two byte buffers of 'arraycopy.buffer.size.bytes' +# each (256 KB by default below), so the Java heap must hold both plus headroom. Raise this (and +# the buffer size) together when configuring a larger, more representative benchmark on a board +# with more RAM. +core.memory.javaheap.size=1048576 core.memory.thread.block.size=512 core.memory.thread.max.size=4 core.memory.threads.memory=RAM core.memory.threads.pool.memory=RAM core.memory.threads.pool.size=15 core.memory.threads.size=10 + +# --- ArrayCopyPerformance benchmark (System.arraycopy throughput) --- +# Size in bytes of each of the two working buffers. Kept small here so the default core testsuite +# runs on memory-constrained boards. For a meaningful memory-bandwidth measurement, raise this well +# beyond the CPU data cache (e.g. 2 MB) and increase 'core.memory.javaheap.size' accordingly. +microej.java.property.com.microej.core.tests.arraycopy.buffer.size.bytes=262144 + +# Minimum expected System.arraycopy() throughput, in MB/s. Unset by default, so the benchmark only +# logs its measured throughput and its checks pass on any board. Set both to enforce a regression +# threshold once the board's baseline is known. Aligned copies exercise the full-newlib word-wide +# fast path; misaligned copies additionally depend on a word-wide BSP memmove override. +# Reference values measured on the i.MX RT1170 EVK (full newlib + fast BSP memmove): 125 / 65. +#microej.java.property.com.microej.core.tests.arraycopy.min.throughput.mbps= +#microej.java.property.com.microej.core.tests.arraycopy.min.throughput.misaligned.mbps= From 229add479508043b6bfc2ea4b3ff272785f2c63e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Houz=C3=A9?= Date: Wed, 22 Jul 2026 14:29:22 +0200 Subject: [PATCH 2/4] P0293MIMXRT1170EVK-170 Document arraycopy benchmark configuration Add a 3.7.0 CHANGELOG entry for the ArrayCopyPerformance test and a README section describing the benchmark and how to configure the throughput thresholds and buffer size. Move the detailed option documentation out of microej-testsuite-common.properties into the README, leaving a short pointer. --- vee-port/validation/core/CHANGELOG.md | 11 +++++ vee-port/validation/core/README.md | 42 +++++++++++++++++++ .../microej-testsuite-common.properties | 19 +++------ 3 files changed, 58 insertions(+), 14 deletions(-) diff --git a/vee-port/validation/core/CHANGELOG.md b/vee-port/validation/core/CHANGELOG.md index 7f35a80..bcdff10 100644 --- a/vee-port/validation/core/CHANGELOG.md +++ b/vee-port/validation/core/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.7.0] - 2026-07-22 + +### Added + +- Add ``ArrayCopyPerformance`` test measuring ``System.arraycopy`` throughput on large ``byte[]`` + buffers, with configurable minimum-throughput thresholds (aligned and misaligned) and a + configurable buffer size. See the README for configuration. + ### [3.6.0] - 2026-04-08 ## Changed @@ -122,3 +130,6 @@ Initial revision with Changelog. - Initial revision +--- +_Markdown_ +_Build: 7E4D1F7C_ diff --git a/vee-port/validation/core/README.md b/vee-port/validation/core/README.md index cd45779..f781b61 100644 --- a/vee-port/validation/core/README.md +++ b/vee-port/validation/core/README.md @@ -29,6 +29,44 @@ Tests can be launched: - Follow the configuration and execution steps described in VEE Port Test Suites [documentation](../README.md). +## Array Copy Performance Benchmark + +The `ArrayCopyPerformance` test measures `System.arraycopy()` throughput on large `byte[]` buffers and, +optionally, asserts it against a minimum. On byte arrays `System.arraycopy()` resolves to a direct tail +call into the C library `memmove`, so a slow libc configuration (e.g. newlib-nano's byte-wise copy) or a +missing word-wide `memmove` override in the BSP shows up as a throughput drop. The test guards against +such a regression. + +Two copies are measured and each is checked against its own threshold: + +- an **aligned** copy, whose throughput drops if the BSP reverts to a byte-wise libc, and +- a **misaligned** (one-byte-offset) copy, which additionally drops if the BSP stops overriding + `memmove` with a word-wide implementation. + +### Configuration + +The benchmark is configured through system properties, declared in +[`validation/microej-testsuite-common.properties`](validation/microej-testsuite-common.properties) with +the `microej.java.property.` prefix (for example +`microej.java.property.com.microej.core.tests.arraycopy.min.throughput.mbps=125`). + +| Property | Default | Description | +| --- | --- | --- | +| `com.microej.core.tests.arraycopy.min.throughput.mbps` | unset (`0`) | Minimum expected **aligned** throughput, in MB/s. When unset, the aligned throughput is only logged and the check passes. | +| `com.microej.core.tests.arraycopy.min.throughput.misaligned.mbps` | unset (`0`) | Minimum expected **misaligned** throughput, in MB/s. When unset, the misaligned throughput is only logged and the check passes. | +| `com.microej.core.tests.arraycopy.buffer.size.bytes` | `2097152` (2 MB) in code; `262144` (256 KB) in this template | Size of each of the **two** working buffers. | + +To turn the benchmark into a regression guard on a given board: + +1. Run the test once with the thresholds unset and read the two measured throughput values from the logs. +2. Set both `min.throughput` properties to a value slightly below the observed baseline. +3. For a meaningful memory-bandwidth measurement, raise `arraycopy.buffer.size.bytes` well beyond the CPU + data cache (e.g. 2 MB). The test allocates two buffers of that size, so `core.memory.javaheap.size` + (in the same properties file) must hold both plus headroom — increase it together with the buffer size. + +Reference values measured on the i.MX RT1170 EVK (Cortex-M7 @ 1 GHz, full newlib + fast BSP `memmove`, +2 MB buffers): **125 MB/s** aligned, **65 MB/s** misaligned. + ## Dependencies *All dependencies are retrieved transitively by Gradle*. @@ -58,3 +96,7 @@ N/A ## Restrictions None. + +--- +_Markdown_ +_Build: 7E4D1F7C_ diff --git a/vee-port/validation/core/validation/microej-testsuite-common.properties b/vee-port/validation/core/validation/microej-testsuite-common.properties index b8ec1d9..894d9c5 100644 --- a/vee-port/validation/core/validation/microej-testsuite-common.properties +++ b/vee-port/validation/core/validation/microej-testsuite-common.properties @@ -6,10 +6,7 @@ core.memory.immortal.memory=RAM core.memory.immortal.size=4096 core.memory.javaheap.memory=RAM -# The ArrayCopyPerformance benchmark allocates two byte buffers of 'arraycopy.buffer.size.bytes' -# each (256 KB by default below), so the Java heap must hold both plus headroom. Raise this (and -# the buffer size) together when configuring a larger, more representative benchmark on a board -# with more RAM. +# Sized to hold the two ArrayCopyPerformance buffers (see the arraycopy section below) plus headroom. core.memory.javaheap.size=1048576 core.memory.thread.block.size=512 core.memory.thread.max.size=4 @@ -18,16 +15,10 @@ core.memory.threads.pool.memory=RAM core.memory.threads.pool.size=15 core.memory.threads.size=10 -# --- ArrayCopyPerformance benchmark (System.arraycopy throughput) --- -# Size in bytes of each of the two working buffers. Kept small here so the default core testsuite -# runs on memory-constrained boards. For a meaningful memory-bandwidth measurement, raise this well -# beyond the CPU data cache (e.g. 2 MB) and increase 'core.memory.javaheap.size' accordingly. +# ArrayCopyPerformance benchmark (System.arraycopy throughput). See the README for what these +# options do and how to configure a throughput threshold. Buffer size is kept small here so the +# default core testsuite runs on memory-constrained boards. Thresholds are unset (the checks are +# no-ops); set both to enforce a regression threshold once the board's baseline is known. microej.java.property.com.microej.core.tests.arraycopy.buffer.size.bytes=262144 - -# Minimum expected System.arraycopy() throughput, in MB/s. Unset by default, so the benchmark only -# logs its measured throughput and its checks pass on any board. Set both to enforce a regression -# threshold once the board's baseline is known. Aligned copies exercise the full-newlib word-wide -# fast path; misaligned copies additionally depend on a word-wide BSP memmove override. -# Reference values measured on the i.MX RT1170 EVK (full newlib + fast BSP memmove): 125 / 65. #microej.java.property.com.microej.core.tests.arraycopy.min.throughput.mbps= #microej.java.property.com.microej.core.tests.arraycopy.min.throughput.misaligned.mbps= From 053baab7db56045dacce8ce88a3c5fcf4ee04142 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Houz=C3=A9?= Date: Fri, 24 Jul 2026 16:42:11 +0200 Subject: [PATCH 3/4] Add arraycopy overlap benches and generic notes Extend the ArrayCopyPerformance bench with two overlapping copies within a single buffer: destination shifted one byte right (memmove copies backwards) and one byte left (copies forwards). The bench now reports four figures (aligned, misaligned, overlap-right, overlap-left), each with its own configurable minimum-throughput threshold. The overlap pair also guards against System.arraycopy being routed to memcpy, which would corrupt overlapping data. Add a note that the measured throughputs must be compared against the memory bandwidth advertised by the silicon vendor to judge whether the port reaches the expected fraction of peak, not just clear the regression floor. Keep the README intro and test docs board-agnostic (MCU data cache, RAM bandwidth). Reset the buffer size to the 64 KB code default (drop the 256 KB override). --- vee-port/validation/core/CHANGELOG.md | 4 +- vee-port/validation/core/README.md | 52 +++++++---- .../core/tests/ArrayCopyPerformance.java | 87 ++++++++++++++++--- .../microej-testsuite-common.properties | 19 +++- 4 files changed, 125 insertions(+), 37 deletions(-) diff --git a/vee-port/validation/core/CHANGELOG.md b/vee-port/validation/core/CHANGELOG.md index bcdff10..c0579ce 100644 --- a/vee-port/validation/core/CHANGELOG.md +++ b/vee-port/validation/core/CHANGELOG.md @@ -10,8 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Add ``ArrayCopyPerformance`` test measuring ``System.arraycopy`` throughput on large ``byte[]`` - buffers, with configurable minimum-throughput thresholds (aligned and misaligned) and a - configurable buffer size. See the README for configuration. + buffers, with configurable minimum-throughput thresholds (aligned, misaligned, overlap-right, and + overlap-left copies) and a configurable buffer size. See the README for configuration. ### [3.6.0] - 2026-04-08 diff --git a/vee-port/validation/core/README.md b/vee-port/validation/core/README.md index f781b61..5a43905 100644 --- a/vee-port/validation/core/README.md +++ b/vee-port/validation/core/README.md @@ -32,16 +32,30 @@ Tests can be launched: ## Array Copy Performance Benchmark The `ArrayCopyPerformance` test measures `System.arraycopy()` throughput on large `byte[]` buffers and, -optionally, asserts it against a minimum. On byte arrays `System.arraycopy()` resolves to a direct tail -call into the C library `memmove`, so a slow libc configuration (e.g. newlib-nano's byte-wise copy) or a -missing word-wide `memmove` override in the BSP shows up as a throughput drop. The test guards against -such a regression. - -Two copies are measured and each is checked against its own threshold: - -- an **aligned** copy, whose throughput drops if the BSP reverts to a byte-wise libc, and -- a **misaligned** (one-byte-offset) copy, which additionally drops if the BSP stops overriding - `memmove` with a word-wide implementation. +optionally, asserts it against a minimum. On byte arrays `System.arraycopy()` resolves to a direct call +into the C library copy routine, so its throughput depends on the libc and BSP configuration; a slow +copy implementation shows up as a throughput drop. The test guards against such a regression. + +Four copies are measured and each is checked against its own threshold: + +- **aligned**: source and destination share the same word-aligned offset, in two distinct buffers; + its throughput drops if the BSP reverts to a byte-wise libc; +- **misaligned**: the destination is shifted by one byte, in two distinct buffers; it additionally + drops if the BSP stops overriding `memmove` with a word-wide implementation; +- **overlap right**: source and destination are in a single buffer with the destination one byte + above the source, which forces `memmove` to copy backwards to preserve the overlap; +- **overlap left**: source and destination are in a single buffer with the destination one byte + below the source, which lets `memmove` copy forwards. + +The two overlapping copies also verify that `System.arraycopy()` is routed to `memmove` and not to a +plain `memcpy`: a `memcpy` would corrupt the overlapping data and would not exhibit the +direction-dependent behavior the overlap figures capture. + +When the buffers are larger than the CPU data cache and live in cacheable RAM, the copy is bounded by +memory bandwidth rather than by the CPU, so the four copies reach essentially the same throughput and +the thresholds guard against a bandwidth regression rather than a CPU-side alignment penalty. On a +target without a data cache the copy is CPU-bound instead, and the aligned and misaligned figures may +diverge. ### Configuration @@ -54,18 +68,22 @@ the `microej.java.property.` prefix (for example | --- | --- | --- | | `com.microej.core.tests.arraycopy.min.throughput.mbps` | unset (`0`) | Minimum expected **aligned** throughput, in MB/s. When unset, the aligned throughput is only logged and the check passes. | | `com.microej.core.tests.arraycopy.min.throughput.misaligned.mbps` | unset (`0`) | Minimum expected **misaligned** throughput, in MB/s. When unset, the misaligned throughput is only logged and the check passes. | -| `com.microej.core.tests.arraycopy.buffer.size.bytes` | `2097152` (2 MB) in code; `262144` (256 KB) in this template | Size of each of the **two** working buffers. | +| `com.microej.core.tests.arraycopy.min.throughput.overlap.right.mbps` | unset (`0`) | Minimum expected **overlap-right** throughput, in MB/s. When unset, the overlap-right throughput is only logged and the check passes. | +| `com.microej.core.tests.arraycopy.min.throughput.overlap.left.mbps` | unset (`0`) | Minimum expected **overlap-left** throughput, in MB/s. When unset, the overlap-left throughput is only logged and the check passes. | +| `com.microej.core.tests.arraycopy.buffer.size.bytes` | `65536` (64 KB) | Size of each of the **two** working buffers. The default already exceeds a typical MCU data cache; larger buffers do not change the measured throughput but need a proportionally larger Java heap. | To turn the benchmark into a regression guard on a given board: -1. Run the test once with the thresholds unset and read the two measured throughput values from the logs. -2. Set both `min.throughput` properties to a value slightly below the observed baseline. -3. For a meaningful memory-bandwidth measurement, raise `arraycopy.buffer.size.bytes` well beyond the CPU - data cache (e.g. 2 MB). The test allocates two buffers of that size, so `core.memory.javaheap.size` +1. Run the test once with the thresholds unset and read the four measured throughput values from the logs. +2. Set the four `min.throughput` properties to a value slightly below the observed baseline. +3. Keep `arraycopy.buffer.size.bytes` large enough to exceed the CPU data cache so the measurement + reflects memory bandwidth. The test allocates two buffers of that size, so `core.memory.javaheap.size` (in the same properties file) must hold both plus headroom — increase it together with the buffer size. -Reference values measured on the i.MX RT1170 EVK (Cortex-M7 @ 1 GHz, full newlib + fast BSP `memmove`, -2 MB buffers): **125 MB/s** aligned, **65 MB/s** misaligned. +The thresholds are regression floors only. To judge whether the port reaches the hardware's potential, +compare the measured throughputs against the theoretical memory bandwidth the silicon vendor advertises +for the backing memory (the RAM bandwidth figures in the datasheet or reference manual) and confirm the +port reaches the expected fraction of that peak. ## Dependencies diff --git a/vee-port/validation/core/src/test/java/com/microej/core/tests/ArrayCopyPerformance.java b/vee-port/validation/core/src/test/java/com/microej/core/tests/ArrayCopyPerformance.java index 1a87303..df87185 100644 --- a/vee-port/validation/core/src/test/java/com/microej/core/tests/ArrayCopyPerformance.java +++ b/vee-port/validation/core/src/test/java/com/microej/core/tests/ArrayCopyPerformance.java @@ -24,19 +24,36 @@ * implementation. This test guards against a regression to a slow byte-wise copy. *

* The buffers are far larger than a typical CPU data cache, so the copy exercises real memory - * bandwidth rather than staying cache-resident. Two copies are measured and each is asserted against + * bandwidth rather than staying cache-resident. Four copies are measured and each is asserted against * its own minimum: *

*

+ * The two overlapping copies specifically exercise the direction-picking logic that distinguishes + * {@code memmove} from a plain {@code memcpy}: a BSP that wrongly routed {@link System#arraycopy} to + * {@code memcpy} would corrupt the overlapping data, and any per-direction performance asymmetry + * surfaces here. + *

+ * These figures are only meaningful relative to the platform's theoretical peak: the measured + * throughputs must be compared against the memory bandwidth the silicon vendor advertises for the + * backing memory (e.g. the RAM bandwidth figures in the datasheet or reference manual) to judge whether + * the port actually reaches the expected fraction of peak, rather than merely clearing a fixed + * regression floor. + *

* The minimum expected throughputs (in MB/s) are read from the system properties - * {@value #PROPERTY_PREFIX}{@value #OPTION_MIN_THROUGHPUT_MBPS} (aligned) and - * {@value #PROPERTY_PREFIX}{@value #OPTION_MIN_THROUGHPUT_MISALIGNED_MBPS} (misaligned). When a + * {@value #PROPERTY_PREFIX}{@value #OPTION_MIN_THROUGHPUT_MBPS} (aligned), + * {@value #PROPERTY_PREFIX}{@value #OPTION_MIN_THROUGHPUT_MISALIGNED_MBPS} (misaligned), + * {@value #PROPERTY_PREFIX}{@value #OPTION_MIN_THROUGHPUT_OVERLAP_RIGHT_MBPS} (overlap right) and + * {@value #PROPERTY_PREFIX}{@value #OPTION_MIN_THROUGHPUT_OVERLAP_LEFT_MBPS} (overlap left). When a * property is not set its expectation defaults to zero, so the corresponding check is a no-op on VEE * Ports that do not declare an expectation. */ @@ -56,6 +73,20 @@ public class ArrayCopyPerformance { */ private static final String OPTION_MIN_THROUGHPUT_MISALIGNED_MBPS = "arraycopy.min.throughput.misaligned.mbps"; + /** + * Option that specifies the minimum expected throughput, in MB/s, of an overlapping copy shifted + * one byte to the right (destination one byte above the source) within a single buffer. When unset, + * the throughput is only logged and its check passes. + */ + private static final String OPTION_MIN_THROUGHPUT_OVERLAP_RIGHT_MBPS = "arraycopy.min.throughput.overlap.right.mbps"; + + /** + * Option that specifies the minimum expected throughput, in MB/s, of an overlapping copy shifted + * one byte to the left (destination one byte below the source) within a single buffer. When unset, + * the throughput is only logged and its check passes. + */ + private static final String OPTION_MIN_THROUGHPUT_OVERLAP_LEFT_MBPS = "arraycopy.min.throughput.overlap.left.mbps"; + /** * Option that specifies the size in bytes of each of the two working buffers. Larger buffers * exceed the CPU data cache and better exercise real memory bandwidth; smaller targets can lower @@ -74,8 +105,13 @@ public class ArrayCopyPerformance { private static final int DEFAULT_MIN_THROUGHPUT_MBPS = 0; - /** Default size in bytes of each of the two working buffers (2 MB). */ - private static final int DEFAULT_BUFFER_SIZE = 2 * 1024 * 1024; + /** + * Default size in bytes of each of the two working buffers (64 KB). Two buffers of this size + * (128 KB total) exceed a typical MCU data cache, so the copy already exercises real memory + * bandwidth rather than staying cache-resident; larger buffers do not change the measured + * throughput but need a proportionally larger Java heap. + */ + private static final int DEFAULT_BUFFER_SIZE = 64 * 1024; /** Number of untimed warm-up copies (stabilizes the caches). */ private static final int WARMUP_ITERATIONS = 2; @@ -86,6 +122,13 @@ public class ArrayCopyPerformance { /** Destination offset, in bytes, used to force a relatively misaligned copy. */ private static final int MISALIGN_OFFSET = 1; + /** + * Shift, in bytes, between the source and destination regions of an overlapping copy. A one-byte + * shift keeps the two regions overlapping over all but one byte, so {@code memmove} must pick its + * copy direction from the sign of the shift. + */ + private static final int OVERLAP_OFFSET = 1; + /** Number of nanoseconds in one second. */ private static final long NS_PER_SECOND = 1_000_000_000L; @@ -93,8 +136,8 @@ public class ArrayCopyPerformance { private static final long BYTES_PER_MB = 1_000_000L; /** - * Measures aligned and misaligned {@link System#arraycopy} throughput and asserts each meets its - * configured minimum. + * Measures aligned, misaligned, overlap-right and overlap-left {@link System#arraycopy} throughput + * and asserts each meets its configured minimum. */ @Test public void testArrayCopyThroughput() { @@ -118,19 +161,35 @@ public void testArrayCopyThroughput() { src[i] = (byte) i; } + // Copies between the two distinct buffers (no overlap). long alignedMBps = measureThroughput(src, dst, 0, 0, bufferSize); long misalignedMBps = measureThroughput(src, dst, 0, MISALIGN_OFFSET, bufferSize - MISALIGN_OFFSET); - System.out.println("System.arraycopy throughput (aligned) : " + alignedMBps + " MB/s"); - System.out.println("System.arraycopy throughput (misaligned) : " + misalignedMBps + " MB/s"); + // Overlapping copies within a single buffer. Shifting the destination one byte above the source + // (right) forces memmove to copy backwards; one byte below (left) lets it copy forwards. + long overlapRightMBps = measureThroughput(src, src, 0, OVERLAP_OFFSET, bufferSize - OVERLAP_OFFSET); + long overlapLeftMBps = measureThroughput(src, src, OVERLAP_OFFSET, 0, bufferSize - OVERLAP_OFFSET); + + System.out.println("System.arraycopy throughput (aligned) : " + alignedMBps + " MB/s"); + System.out.println("System.arraycopy throughput (misaligned) : " + misalignedMBps + " MB/s"); + System.out.println("System.arraycopy throughput (overlap right) : " + overlapRightMBps + " MB/s"); + System.out.println("System.arraycopy throughput (overlap left) : " + overlapLeftMBps + " MB/s"); int minAlignedMBps = getOptionAsInt(OPTION_MIN_THROUGHPUT_MBPS, DEFAULT_MIN_THROUGHPUT_MBPS); int minMisalignedMBps = getOptionAsInt(OPTION_MIN_THROUGHPUT_MISALIGNED_MBPS, DEFAULT_MIN_THROUGHPUT_MBPS); + int minOverlapRightMBps = getOptionAsInt(OPTION_MIN_THROUGHPUT_OVERLAP_RIGHT_MBPS, DEFAULT_MIN_THROUGHPUT_MBPS); + int minOverlapLeftMBps = getOptionAsInt(OPTION_MIN_THROUGHPUT_OVERLAP_LEFT_MBPS, DEFAULT_MIN_THROUGHPUT_MBPS); assertTrue("Aligned System.arraycopy throughput (" + alignedMBps + " MB/s) is below the required minimum (" + minAlignedMBps + " MB/s)", alignedMBps >= minAlignedMBps); assertTrue("Misaligned System.arraycopy throughput (" + misalignedMBps + " MB/s) is below the required minimum (" + minMisalignedMBps + " MB/s)", misalignedMBps >= minMisalignedMBps); + assertTrue("Overlap-right System.arraycopy throughput (" + overlapRightMBps + + " MB/s) is below the required minimum (" + minOverlapRightMBps + " MB/s)", + overlapRightMBps >= minOverlapRightMBps); + assertTrue("Overlap-left System.arraycopy throughput (" + overlapLeftMBps + + " MB/s) is below the required minimum (" + minOverlapLeftMBps + " MB/s)", + overlapLeftMBps >= minOverlapLeftMBps); } /** diff --git a/vee-port/validation/core/validation/microej-testsuite-common.properties b/vee-port/validation/core/validation/microej-testsuite-common.properties index 894d9c5..ed5fcd9 100644 --- a/vee-port/validation/core/validation/microej-testsuite-common.properties +++ b/vee-port/validation/core/validation/microej-testsuite-common.properties @@ -16,9 +16,20 @@ core.memory.threads.pool.size=15 core.memory.threads.size=10 # ArrayCopyPerformance benchmark (System.arraycopy throughput). See the README for what these -# options do and how to configure a throughput threshold. Buffer size is kept small here so the -# default core testsuite runs on memory-constrained boards. Thresholds are unset (the checks are -# no-ops); set both to enforce a regression threshold once the board's baseline is known. -microej.java.property.com.microej.core.tests.arraycopy.buffer.size.bytes=262144 +# options do and how to configure a throughput threshold. Four figures are measured: aligned and +# misaligned copies between the two buffers, plus two overlapping copies within a single buffer +# (destination shifted one byte right, then one byte left). Thresholds are unset (the checks are +# no-ops); set all four to enforce a regression threshold once the board's baseline is known. +# +# The thresholds are regression floors only: they say nothing about whether the port reaches the +# hardware's potential. The measured throughputs must also be compared against the theoretical memory +# bandwidth the silicon vendor advertises for the backing memory (the RAM bandwidth figures in the +# datasheet or reference manual) to confirm the port reaches the expected fraction of peak. +# +# Buffer size is left to the test default (64 KB, see ArrayCopyPerformance.DEFAULT_BUFFER_SIZE); +# uncomment to override. +#microej.java.property.com.microej.core.tests.arraycopy.buffer.size.bytes=65536 #microej.java.property.com.microej.core.tests.arraycopy.min.throughput.mbps= #microej.java.property.com.microej.core.tests.arraycopy.min.throughput.misaligned.mbps= +#microej.java.property.com.microej.core.tests.arraycopy.min.throughput.overlap.right.mbps= +#microej.java.property.com.microej.core.tests.arraycopy.min.throughput.overlap.left.mbps= From 5ea9baed3265bfe4307e2d1f23df7b7818ebda84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Houz=C3=A9?= Date: Fri, 24 Jul 2026 16:56:19 +0200 Subject: [PATCH 4/4] adjust java heap --- .../core/validation/microej-testsuite-common.properties | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/vee-port/validation/core/validation/microej-testsuite-common.properties b/vee-port/validation/core/validation/microej-testsuite-common.properties index ed5fcd9..bfabd68 100644 --- a/vee-port/validation/core/validation/microej-testsuite-common.properties +++ b/vee-port/validation/core/validation/microej-testsuite-common.properties @@ -6,8 +6,10 @@ core.memory.immortal.memory=RAM core.memory.immortal.size=4096 core.memory.javaheap.memory=RAM -# Sized to hold the two ArrayCopyPerformance buffers (see the arraycopy section below) plus headroom. -core.memory.javaheap.size=1048576 +# The ArrayCopyPerformance benchmark allocates two 64 KB byte buffers (128 KB) in the Java heap; +# 160 KB covers those plus test-framework overhead. This is the largest heap any core validation +# test needs (see the arraycopy section in the README). +core.memory.javaheap.size=163840 core.memory.thread.block.size=512 core.memory.thread.max.size=4 core.memory.threads.memory=RAM