diff --git a/.github/workflows/ci-main.yml b/.github/workflows/ci-main.yml index 45c633b..80b310d 100644 --- a/.github/workflows/ci-main.yml +++ b/.github/workflows/ci-main.yml @@ -131,64 +131,53 @@ jobs: if: runner.os == 'Linux' && matrix.compiler == 'gcc' continue-on-error: true run: | - rm -rf build-rel build-rel-o3 - meson setup build-rel-o3 --buildtype=release \ - -Doptimization=3 -Dcompact_release=false - meson compile -C build-rel-o3 + rm -rf build-rel meson setup build-rel --buildtype=release meson compile -C build-rel - mapfile -t control_shared < <(find build-rel-o3 -maxdepth 1 -type f \ + mapfile -t release_shared < <(find build-rel -maxdepth 1 -type f \ -name 'libchronoid.so.*' -print) - mapfile -t compact_shared < <(find build-rel -maxdepth 1 -type f \ - -name 'libchronoid.so.*' -print) - if [ "${#control_shared[@]}" -ne 1 ] \ - || [ "${#compact_shared[@]}" -ne 1 ]; then - echo "Expected one versioned shared library per build." >&2 + if [ "${#release_shared[@]}" -ne 1 ]; then + echo "Expected one versioned shared library." >&2 exit 1 fi - strip --strip-unneeded "${control_shared[0]}" - strip --strip-unneeded "${compact_shared[0]}" + strip --strip-unneeded "${release_shared[0]}" { - echo '## Footprint — ${{ matrix.os }} / GCC / x86_64' + echo '## Standard O3 footprint — ${{ matrix.os }} / GCC / ${{ runner.arch }}' echo '' echo 'Shared libraries are stripped with `strip --strip-unneeded`;' echo 'the static archive and CLI are measured as built.' echo '' - echo '| Artifact | O3 control | Compact O2 | Delta | Reduction |' - echo '|---|---:|---:|---:|---:|' - failed=0 + echo '| Artifact | Bytes |' + echo '|---|---:|' for spec in \ - "shared:${control_shared[0]}:${compact_shared[0]}" \ - 'static:build-rel-o3/libchronoid.a:build-rel/libchronoid.a' \ - 'chronoid-gen:build-rel-o3/chronoid-gen:build-rel/chronoid-gen'; do - IFS=: read -r label control_path compact_path <<< "$spec" - if [ ! -f "$control_path" ] || [ ! -f "$compact_path" ]; then + "shared:${release_shared[0]}" \ + 'static:build-rel/libchronoid.a' \ + 'chronoid-gen:build-rel/chronoid-gen'; do + IFS=: read -r label artifact_path <<< "$spec" + if [ ! -f "$artifact_path" ]; then echo "Missing footprint artifact for $label." >&2 exit 1 fi - control_bytes=$(stat -c '%s' "$control_path") - compact_bytes=$(stat -c '%s' "$compact_path") - case "$control_bytes:$compact_bytes" in - *[!0-9:]*|'':*|*:'') + artifact_bytes=$(stat -c '%s' "$artifact_path") + case "$artifact_bytes" in + *[!0-9]*|'') echo "Invalid footprint size for $label." >&2 exit 1 ;; esac - delta=$((compact_bytes - control_bytes)) - reduction=$(awk -v c="$control_bytes" -v n="$compact_bytes" \ - 'BEGIN { printf "%.1f%%", 100 * (c - n) / c }') - printf '| %s | %s | %s | %+d | %s |\n' \ - "$label" "$control_bytes" "$compact_bytes" "$delta" "$reduction" - if [ "$compact_bytes" -ge "$control_bytes" ]; then - echo "Compact release did not shrink $label." >&2 - failed=1 - fi + printf '| %s | %s |\n' "$label" "$artifact_bytes" done + echo '' + echo '
Shared-library sections' + echo '' + echo '```text' + size -A "${release_shared[0]}" + echo '```' + echo '
' } >> "$GITHUB_STEP_SUMMARY" - exit "$failed" - name: Publish results if: always() diff --git a/CHANGELOG.md b/CHANGELOG.md index b7eb7c3..66437bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,16 +11,14 @@ every binary-incompatible change, and the major version bumps with it. ### Changed (build) -- Release builds now compile production targets at optimization level 2 by - default. On GCC 16.2.1 x86_64 this reduces the stripped shared library from - 43,168 to 39,072 bytes (-9.5%), the as-built static archive from 56,142 to - 52,358 bytes (-6.7%), and the as-built CLI from 37,312 to 33,328 bytes - (-10.7%) relative to an otherwise identical O3 build. Size optimization was - smaller but roughly halved KSUID CLI throughput, while LTO enlarged the - static archive more than threefold, so O2 is the size/performance compromise. - Pass `-Dcompact_release=false` to remove the per-target override and honor - the caller's global optimization setting. Public API, ABI, wire formats, - SIMD dispatch, and runtime dependencies are unchanged. +- Release builds again honor Meson's standard optimization level 3. A private + RNG-state observation helper now uses its fixed test-buffer contract instead + of a variable-length copy, avoiding substantial compiler expansion. On GCC + 16.2.1 x86_64 this reduces the stripped shared library from 43,168 to 39,072 + bytes (-9.5%), the as-built static archive from 56,142 to 55,532 bytes + (-1.1%), and the as-built CLI from 37,312 to 37,248 bytes (-0.2%) versus the + same O3 build before the source change. Production RNG paths, public API, + ABI, wire formats, SIMD dispatch, and runtime dependencies are unchanged. ### Fixed (build) diff --git a/README.md b/README.md index fb0870f..9a580ab 100644 --- a/README.md +++ b/README.md @@ -91,24 +91,27 @@ meson compile -C build-release strip --strip-unneeded build-release/libchronoid.so.* ``` -Release builds use Meson optimization level 2 by default. This keeps the -installed artifacts compact without the substantial KSUID throughput loss -observed with size optimization. To honor Meson's or the caller's global -release optimization level instead, configure with -`-Dcompact_release=false` (Meson's standard release default is level 3). +Release builds use Meson's standard optimization level 3. Binary footprint is +reduced in the source rather than by overriding the caller's optimization +policy. ## Footprint -A GCC 16.2.1 release build on x86_64 produces the following paired result. +A GCC 16.2.1 O3 release build on x86_64 produces the following paired result. The shared library is measured after `strip --strip-unneeded`; the static -archive and CLI are measured as built. Both columns use the same compiler and -linker invocation, with only `compact_release` changed: - -| Artifact | Compact O2 | O3 control | Reduction | -| :-------------------- | ---------: | ---------: | --------: | -| libchronoid.so.1.1.0 | 39 072 | 43 168 | 9.5% | -| libchronoid.a | 52 358 | 56 142 | 6.7% | -| chronoid-gen (CLI) | 33 328 | 37 312 | 10.7% | +archive and CLI are measured as built. Both columns use the same compiler, +linker, build options, and optimization level: + +| Artifact | O3 before | O3 source-optimized | Reduction | +| :-------------------- | --------: | ------------------: | --------: | +| libchronoid.so.1.1.0 | 43 168 | 39 072 | 9.5% | +| libchronoid.a | 56 142 | 55 532 | 1.1% | +| chronoid-gen (CLI) | 37 312 | 37 248 | 0.2% | + +The source change compacts a private RNG-state observation helper used only by +the test suite. Its documented fixed-capacity contract avoids the compiler's +large variable-length `memcpy` expansion without changing production RNG hot +paths, public API, or wire formats. The bulk-encode AVX2 kernel from `chronoid/ksuid/encode_avx2.c` accounts for roughly 8 KB of the shared-library size, and the UUIDv7 hex AVX2 diff --git a/chronoid/rand.h b/chronoid/rand.h index 5c3c059..6426ade 100644 --- a/chronoid/rand.h +++ b/chronoid/rand.h @@ -88,15 +88,12 @@ extern _Atomic int chronoid_thread_exit_wipes_observed; * erase. Must be called before any draw on the same thread. */ void chronoid_random_thread_state_set_sentinel_for_testing (void); -/* Copy the calling thread's TLS RNG state bytes into |out| (which - * must be at least sizeof(chronoid_tls_rng_t) -- the test is allowed to - * over-allocate). Used to assert the wipe actually zeroed the - * region. */ -void chronoid_random_thread_state_peek_for_testing (uint8_t * out, - size_t out_len); +/* Capacity for the private TLS-state observation buffer. The helper + * returns the actual bytes copied; any unused tail stays untouched. */ +#define CHRONOID_RANDOM_THREAD_STATE_PEEK_CAPACITY 256u -/* Size in bytes that a peek buffer must accommodate. */ -size_t chronoid_random_thread_state_size_for_testing (void); +size_t chronoid_random_thread_state_peek_for_testing (uint8_t + out[CHRONOID_RANDOM_THREAD_STATE_PEEK_CAPACITY]); /* Test-only: replace the wall-clock source used by chronoid_uuidv7_* * generation. Pass a non-NULL fn to install; pass NULL to restore the diff --git a/chronoid/rand_tls.c b/chronoid/rand_tls.c index 4cb16cc..a8e90c9 100644 --- a/chronoid/rand_tls.c +++ b/chronoid/rand_tls.c @@ -78,6 +78,10 @@ typedef struct bool destructor_registered; /* thread-exit wipe registered yet? */ } chronoid_tls_rng_t; +_Static_assert (sizeof (chronoid_tls_rng_t) + <= CHRONOID_RANDOM_THREAD_STATE_PEEK_CAPACITY, + "test observation capacity must hold the complete TLS RNG state"); + static _Thread_local chronoid_tls_rng_t chronoid_tls_rng_; /* Re-entry guard for chronoid_random_thread_state_wipe. A future change @@ -120,18 +124,11 @@ chronoid_random_thread_state_set_sentinel_for_testing (void) chronoid_tls_rng_.destructor_registered = registered; } -void -chronoid_random_thread_state_peek_for_testing (uint8_t *out, size_t out_len) -{ - size_t n = sizeof chronoid_tls_rng_; - if (out_len < n) - n = out_len; - memcpy (out, &chronoid_tls_rng_, n); -} - size_t -chronoid_random_thread_state_size_for_testing (void) +chronoid_random_thread_state_peek_for_testing (uint8_t + out[CHRONOID_RANDOM_THREAD_STATE_PEEK_CAPACITY]) { + memcpy (out, &chronoid_tls_rng_, sizeof chronoid_tls_rng_); return sizeof chronoid_tls_rng_; } diff --git a/meson.build b/meson.build index 6f74423..c7c5da1 100644 --- a/meson.build +++ b/meson.build @@ -13,17 +13,6 @@ project('libchronoid', 'c', cc = meson.get_compiler('c') pkg = import('pkgconfig') -# Meson's release build type selects optimization level 3. GCC and Clang -# expand several of our fixed-trip-count codec loops aggressively at that -# level, increasing all three installed artifacts without a measurable CLI -# throughput benefit. Keep the public release recipe compact by overriding -# production targets to level 2. Setting -Dcompact_release=false removes the -# target override and honors the caller's global optimization choice. -compact_release_options = [] -if get_option('compact_release') and get_option('buildtype') == 'release' - compact_release_options = ['optimization=2'] -endif - # Hardening / portability flags applied to all our TUs. Compiler- # specific defaults split between gcc/clang and MSVC: visibility # control + POSIX feature macros only mean anything on the GCC-style @@ -302,7 +291,6 @@ if have_hex_ssse3 'chronoid/uuidv7/hex_ssse3.c', include_directories : inc, c_args : ssse3_arg != '' ? [ssse3_arg] : [], - override_options : compact_release_options, pic : true, install : false, ) @@ -317,7 +305,6 @@ if have_avx2_batch 'chronoid/ksuid/encode_avx2.c', include_directories : inc, c_args : [avx2_arg], - override_options : compact_release_options, pic : true, install : false, ) @@ -341,7 +328,6 @@ if have_hex_avx2 'chronoid/uuidv7/hex_avx2.c', include_directories : inc, c_args : [avx2_arg], - override_options : compact_release_options, pic : true, install : false, ) @@ -352,7 +338,6 @@ chronoid_lib = both_libraries('chronoid', core_sources, include_directories : inc, dependencies : extra_link_deps, link_whole : hex_ssse3_lib_dep + avx2_lib_dep + hex_avx2_lib_dep, - override_options : compact_release_options, version : meson.project_version(), # soversion tracks semver major: a major bump means a binary- # incompatible ABI break and the SONAME flips with it. 1.0 is the @@ -386,7 +371,6 @@ if get_option('cli') 'examples/chronoid-gen.c', include_directories : inc, link_with : chronoid_lib.get_static_lib(), - override_options : compact_release_options, install : true, ) endif diff --git a/meson.options b/meson.options index 82bd3d1..a83422e 100644 --- a/meson.options +++ b/meson.options @@ -2,8 +2,6 @@ option('tests', type : 'boolean', value : true, description : 'Build the unit test suite') option('cli', type : 'boolean', value : true, description : 'Build the chronoid-gen CLI tool') -option('compact_release', type : 'boolean', value : true, - description : 'Use optimization level 2 for smaller release artifacts; disable to honor the global release optimization level') option('simd', type : 'combo', choices : ['auto', 'none'], value : 'auto', description : 'Enable SIMD/NEON acceleration (auto-detected per host arch)') option('avx2_batch', type : 'feature', value : 'auto', diff --git a/tests/test_rand_tls.c b/tests/test_rand_tls.c index 5bd1487..c53a539 100644 --- a/tests/test_rand_tls.c +++ b/tests/test_rand_tls.c @@ -118,17 +118,30 @@ sentinel_thread_body (void *opaque) * registered destructor still fires at thread exit and wipes * whatever is in the slot. */ chronoid_random_thread_state_set_sentinel_for_testing (); - /* Sanity check: the sentinel landed in the slot. */ - size_t n = chronoid_random_thread_state_size_for_testing (); - uint8_t *peek = malloc (n); - if (peek == NULL) - return -1; - chronoid_random_thread_state_peek_for_testing (peek, n); + /* Sanity check: the sentinel landed in the slot. Guard bytes and + * the unused capacity tail prove that the observation helper copies + * exactly the private state size it reports. */ + enum + { guard_size = 16 }; + uint8_t guarded[guard_size + CHRONOID_RANDOM_THREAD_STATE_PEEK_CAPACITY + + guard_size]; + memset (guarded, 0x3c, sizeof guarded); + uint8_t *peek = guarded + guard_size; + size_t n = chronoid_random_thread_state_peek_for_testing (peek); + if (n == 0 || n > CHRONOID_RANDOM_THREAD_STATE_PEEK_CAPACITY) + return -3; size_t a5_count = 0; for (size_t i = 0; i < n; ++i) if (peek[i] == 0xa5) ++a5_count; - free (peek); + for (size_t i = 0; i < guard_size; ++i) { + if (guarded[i] != 0x3c + || peek[CHRONOID_RANDOM_THREAD_STATE_PEEK_CAPACITY + i] != 0x3c) + return -3; + } + for (size_t i = n; i < CHRONOID_RANDOM_THREAD_STATE_PEEK_CAPACITY; ++i) + if (peek[i] != 0x3c) + return -3; /* At least 64 bytes of state[16] plus 64 bytes of buf must be * the sentinel (the bool flags read as 1, not 0xa5, but everything * else does). Use a conservative lower bound. */