encodings/crc32: fix a data race in table init, enable slicing-by-8 on every target, add a PCLMULQDQ fold - #19278
Merged
Merged
Conversation
added 11 commits
July 28, 2026 09:20
The slicing-by-8 tables were built at first call into a mutable .bss array behind an unsynchronised "ready" flag. Two threads entering encoding_crc32() concurrently both run the generator; the flag is stored with a plain write after the table stores, so nothing orders the publication against the payload on a weakly ordered target. A reader can observe ready == 1 and then load a half-written entry, which yields a silently wrong checksum rather than a crash. ThreadSanitizer over eight threads released from a barrier reports nine distinct races on the old code, including reads of the 8 KiB table concurrent with its construction. Precompute the tables instead and emit them const from a generated header. They land in .rodata, cost nothing to construct, are shared between processes and cannot race. The fast path was also gated on x86 alone. Every other target -- all ARM builds that do not pass +crc, PPC, MIPS, RISC-V -- fell through to the byte-at-a-time loop. Only the assumption of little-endian word loads actually tied the loop to x86, so byte-swap under MSB_FIRST and let everyone use it. While here: load through memcpy rather than casting uint8_t* to uint32_t*, drop the const cast in the ARM path, and decrement in the body of the byte tails rather than in the loop test, which wrapped len to SIZE_MAX on the final iteration. That last one is defined behaviour but trips -fsanitize=unsigned-integer-overflow. Measured on Xeon @2.8GHz, 256 KiB buffer, gcc 13 -O2: byte-at-a-time (what non-x86 got) 0.39 GB/s slicing-by-8, .bss + ready flag 1.94 GB/s slicing-by-8, const .rodata 2.10 GB/s So ~5.4x for every non-x86 target and ~8% on x86 from dropping the per-call flag test and letting the tables be constant. Verified bit-exact against a bitwise reference for lengths 0-1200 at all 16 alignments, streaming splits, non-zero seeds and 1 MiB blocks, on x86-64, i386, aarch64 with and without +crc, and big-endian powerpc under qemu. Clean under ASan, LSan, UBSan, MSan, TSan and valgrind memcheck.
Slicing-by-8 issues one table load per input byte and accumulates all
eight lookups into a single register, so each 8-byte iteration is a
serial chain of eight dependent XORs behind an L1 load. Measured 1.47
cycles/byte, against a load-port floor of roughly 0.5. It is
latency-bound on that chain, not bound by cache misses: cachegrind
over a 256 KiB working set shows the 8 KiB table staying resident,
with D1 misses accounting for compulsory stream fill only. There are
no register spills in the loop on x86-64 or i386.
Carry-less multiply folding removes the problem rather than tuning it.
Four independent 128-bit accumulators consume 64 bytes per iteration
with no loop-carried scalar dependency, and data references drop from
1.25 per byte to 0.064 -- a factor of 19.6.
Dispatch is at runtime on the CPUID PCLMULQDQ bit, so generic x86
builds pick it up without raising the baseline ISA; pre-Westmere and
pre-Bulldozer parts keep the table path. Buffers under 64 bytes go to
the table path too, since below four accumulators the reduction tail
costs more than it saves.
The detection result is cached in a single retro_atomic_int_t holding
the whole tri-state rather than a plain flag guarding a separate
payload, so concurrent probers merely store the same value and there
is no unsynchronised write to order against. Same acquire/release
idiom as cpu_features_get().
The folding constants are all K(n) = bit_reflect_32(x^n mod P(x)) << 1
and are derived rather than copied; the derivation is in the comment
above them and reproduces the published values.
Xeon @2.8GHz, gcc 13 -O2, GB/s:
64B 4K 32K 256K 4M 64M
before 2.41 1.91 1.93 1.94 2.01 1.99
after 7.26 19.69 20.80 20.82 19.73 7.43
zlib 1.3 0.51 3.11 3.22 3.31 3.33 3.10
10.8x over the previous code and 6.5x over zlib in cache; the 64 MiB
column is DRAM-bandwidth bound on this box, not compute bound.
Callers that benefit most are the per-frame core-memory hash in
netplay_frontend.c, content scanning through interface_stream.c, the
zip/7z/zstd archive CRCs and CHD hunk verification in rchd.c.
Verified bit-exact against a bitwise reference and against zlib
crc32() for lengths 0-1200 at all 16 alignments, streaming splits,
non-zero seeds, 1 MiB blocks and a 5 MiB single shot. Clean under
ASan, LSan, UBSan, MSan, TSan and valgrind memcheck, including a
guard-page harness that hashes 901 buffers ending flush against an
unmapped page to prove the 16-byte SIMD loads never overread.
encoding_crc32() now selects between three implementations -- a PCLMULQDQ fold chosen at runtime on x86, the ARMv8 CRC32 instructions chosen at compile time, and a portable slicing-by-8 table -- and nearly every way that can go wrong is silent. A wrong checksum is still a plausible 32-bit number. Nothing pinned the value from outside the implementation, so add something that does. Seven sections. Values are checked against a bit-by-bit reference built straight from the polynomial: no tables, no word loads, and endian-agnostic, which the slicing-by-8 loop is not. Twelve frozen constants (the published CRC-32/ISO-HDLC check value plus zlib outputs) sit alongside it so that a broken reference cannot cancel out a broken implementation. The sweep hashes lengths 0-1200 at all 16 alignments with each buffer allocated at exactly the size read, so the last valid byte abuts an ASan redzone and a SIMD load that runs one iteration too far is an error rather than a silent success. The guard-page section repeats that check without a sanitizer, hashing 901 buffers that end flush against a PROT_NONE page, which is what gives the qemu lanes overread coverage. Cross-path agreement hashes the same megabyte one-shot and in chains of 1 to 4096 bytes, forcing the fold path and the table path to agree with each other rather than only with the reference. Verified by injecting eleven regressions and confirming each is caught: fold-tail overread, dispatch threshold, three separate constant bit-flips, both complements, a transposed slice index, a dropped seed on the SIMD path, the atomic feature cache reverted to a plain int, and the MSB_FIRST byteswap removed. That last one is caught only on big-endian -- on x86 the swap compiles away and every other check still passes -- which is why powerpc is in the cross-arch matrix. Two things about the concurrency section are load-bearing and are commented as such in the source. It runs first, because any section ahead of it warms the feature-detection cache on the main thread and leaves the workers reading an already published value. And its workers make exactly one dispatching call each: ThreadSanitizer keeps only a few shadow entries per address, so a one-write/many-read lazy init gets crowded out of that history by further traffic. Measured on this test, eight threads making one call each report the race in 30 runs out of 30; the same eight threads making forty calls each report it in none, and raising the thread count makes it worse rather than better. The sustained-load checks therefore live in a separate section that runs afterwards. Registered in the samples workflow's executed-target list, with a Clang/ThreadSanitizer step and a cross-arch job covering aarch64 with and without +crc, armv7, and big-endian powerpc under qemu-user. Stock aarch64 gcc does not define __ARM_FEATURE_CRC32, so those two aarch64 entries really do exercise different code: the +crc object contains three CRC32 instructions, the default object none. Runs in 0.3s natively, 0.4s under ASan. Clean under ASan, LSan, UBSan, MSan, TSan and valgrind memcheck, with GCC and Clang, at -m32 and -m64, and with HAVE_THREADS=0.
The step was inserted immediately before the first step of retro-atomic-cross rather than after the last step of samples, so it ran as that job's opening step -- before its checkout -- and failed with An error occurred trying to start process '/usr/bin/bash' with working directory '.../libretro-common/samples/encodings/crc32'. No such file or directory The directory was never missing; nothing had been cloned yet. Move it to the end of the samples job, which has already checked out and installed clang by that point.
The path was selected on architecture alone. Being on x86 says the CPU
might have PCLMULQDQ -- which is why the runtime CPUID check exists --
but it says nothing about whether the toolchain can compile
_mm_clmulepi64_si128 or honour __attribute__((target("pclmul"))). A
toolchain defining __i386__ without wmmintrin.h would fail to build.
Test __has_attribute(target) and __has_include(<wmmintrin.h>) rather
than a compiler version, with a !__clang__ guarded GCC >= 4.4 fallback
for compilers too old to have those. The negation matters: Clang
reports __GNUC__ 4, __GNUC_MINOR__ 2 permanently, so a bare ">= 4.4"
predicate rejects it, and Clang is the toolchain on every Apple
platform, the Android NDK since r18, Emscripten and PS4-ORBIS. Getting
that backwards would silently drop the fold on all of them while still
compiling and passing every test.
Verified the fold is still emitted, not just that the file builds:
pclmulqdq instruction counts of 33 (gcc), 41 (clang), 30 (gcc -m32),
47 (clang -m32) and 30 (gcc -m32 -march=i686); zero on aarch64. The
i686 baseline case matters because the target attribute has to pull
SSE2 into that one function without raising it for the translation
unit.
…for crc32 Three things that only make sense together. features_cpu.c read /proc/cpuinfo and the sysfs CPU lists through filestream_*, which pulls streams/file_stream.c, vfs/vfs_implementation.c and compat/compat_strl.c behind it. Nothing about a kernel pseudo-file wants to be interceptable by a user-supplied VFS, so use stdio. Three call sites: the ARM feature-name scan, the sysfs cpulist reader, and the Linux model-name lookup. cpulist_read_from() now reads into a fixed buffer rather than sizing from the file, because sysfs reports a page for files that contain "0-7\n". After this the translation unit's only undefined symbols are libc ones -- verified on x86-64, aarch64, armv7 and powerpc. That matters because encoding_crc32() wants to ask whether the CPU has PCLMULQDQ, and a leaf utility should not acquire a transitive dependency on file I/O to find out. With the VFS gone it costs features_cpu.c and compat_strl.c, both leaves. There was no bit to ask for. RETRO_SIMD_AES is CPUID.1:ECX[25]; PCLMULQDQ is ECX[1]. They shipped together on most parts but hypervisors mask them independently and some early Westmere SKUs had AES fused off, so one cannot stand in for the other. Add RETRO_SIMD_PCLMUL at bit 24 and probe it properly. Darwin needs its own path. x86_cpuid() is compiled under "CPU_X86 && !__MACH__", so Intel macOS never reaches the CPUID branch and relies on sysctl -- and Apple publishes no hw.optional key for PCLMULQDQ. Read machdep.cpu.features instead, which is the leaf-1 feature-name list, matching on whole space-separated tokens so a future "PCLMULQDQ2" cannot false-positive a plain strstr(). encoding_crc32() then drops its private CPUID and its own atomic cache in favour of cpu_features_get(), which already probes once behind an acquire/release pair. Note the probe needs no OSXSAVE/XCR0 dance: PCLMULQDQ is an XMM instruction, so CR4.OSFXSR is the only OS requirement and that is universal. Only AVX-width state needs xgetbv. crc32_test passes under gcc, clang, -m32, ASan+UBSan, TSan and MSan natively, and on aarch64, aarch64+crc, armv7 and big-endian powerpc under qemu-user -- the cross lanes now linking features_cpu.c with no architecture conditional, because there is nothing left to conditionalise.
UndefinedBehaviorSanitizer's implicit-conversion check fires twice in cpu_features_probe() and cpu_features_get_model_name(): features_cpu.c:940:14: implicit conversion from type 'unsigned int' of value 2147483648 to type 'int' changed the value to -2147483648 x86_cpuid() took its leaf number as int, so every extended-leaf call site -- 0x80000000, 0x80000001, 0x80000002+i -- passed a value with the top bit set through a signed parameter. Widen it to uint32_t. That in turn exposed the reverse conversion: flags[] is int32_t but holds raw register contents, and leaf 0x80000000 legitimately returns a maximum-leaf value above INT32_MAX, so assigning flags[0] to the unsigned max_flag converted implicitly. Spell those two out with a cast rather than changing the array type, which would break source compatibility for anything outside the tree calling x86_cpuid(). Neither was a live defect -- the values round-trip on every ABI we build for -- but they made the file impossible to run under -fsanitize=implicit-conversion, which matters now that the PCLMULQDQ probe lives here. Predates the crc32 work; found while sanitizing that series. Clean afterwards under gcc ASan+LeakSan+UBSan, clang MSan, clang TSan, clang UBSan+integer+implicit-conversion+unsigned-integer-overflow, and valgrind memcheck, exercising cpu_features_get(), cpu_features_get_core_amount() and cpu_features_get_model_name() from eight threads. Feature detection unchanged: same bitmask, PCLMULQDQ still reported.
…s linked encoding_crc32() now asks cpu_features_get() whether the CPU has PCLMULQDQ, so encoding_crc32.o carries an undefined reference to it. Every source list that already linked encoding_crc32.c needs features_cpu.c beside it, and features_cpu.c uses strlcpy, so those lists need compat_strl.c too. Seven sample builds and libretro-db itself were failing to link: libretro-db/samples/libretrodb libretro-db/samples/rmsgpack libretro-common/samples/streams/rzip libretro-common/samples/formats/png libretro-common/samples/formats/json libretro-common/samples/file/archive_file samples/tasks/patch_stream Done per source list rather than per file. png/Makefile has four independent lists and only two of them already carried compat_strl.c; adding a duplicate object to a link line is a multiple-definition error, so each list is checked on its own. Verified by building every Makefile under a samples/ directory repo-wide with SANITIZER=address,undefined -- 50 of 51 clean, the lone failure being deps/SPIRV-Cross/samples/cpp, which wants glm headers and is untouched by this series.
cpu_features_get() publishes the mask with a release store and reads it
with an acquire load, which orders publisher against consumer. What it
does not order is publisher against publisher: if several threads reach
the probe before any has published, they all write cpu_features_cache
with plain stores. The comment there argued that was fine because the
probe is idempotent and they store the same bits, but same value or
not, concurrent unordered writes to one object are a data race:
WARNING: ThreadSanitizer: data race
Write of size 8 ... in cpu_features_get
Previous write of size 8 ... in cpu_features_get
Location is global 'cpu_features_get.cpu_features_cache'
Elect exactly one writer with an atomic increment. Threads that lose
return the value they computed themselves, which is identical, so
nobody spins and nobody writes memory another thread is writing.
retro_atomic_fetch_add_int() returns the previous value in all seven
backends.
Latent until something called cpu_features_get() from several threads
at once during startup; crc32_test's first-touch storm is the first
thing in the tree that does. Reproduced deliberately by widening the
probe with a 30ms sleep and calling from eight barrier-released
threads: one race before this change, none after. The window is hard
to hit on a single-core machine, which is why local runs passed while
the CI runner failed.
The ARM path was gated on __ARM_FEATURE_CRC32 alone, which the compiler only defines when the target baseline already includes the instructions. In tree that is arm64 macOS, where Clang derives it from the default -mcpu, and Makefile.libnx, which passes +crc explicitly. Everything else on ARM ran slicing-by-8: Android arm64, Linux ARM, Raspberry Pi, iOS and tvOS, on hardware that has had the instructions since ARMv8.1. Compile the path in whenever the toolchain can build it into a single function without raising the ISA for the translation unit, and choose between it and the table at runtime, the way the x86 side chooses on CPUID. Measured on an Apple M1, 256 KiB buffer: 1.44 GB/s for slicing-by-8 against 3.38 for the instructions, and the target-attribute build is indistinguishable from the baseline build at 3.38 vs 3.38, so nothing is lost by not raising the baseline. Raising it instead is not an option on Apple: iOS deployment targets in pkg/apple reach back to 6.0 and Clang reports CRC32 as absent through A10, so an unconditional +crc would be an illegal instruction on those devices. Runtime detection is the only route that covers them. Detection goes through a new RETRO_SIMD_CRC32 bit rather than a second private probe: hw.optional.armv8_crc32 on Darwin with the FEAT_CRC32 spelling as a fallback, and the existing check_arm_cpu_feature() reader on Linux, where aarch64 lists it as "crc32". Both keys confirmed present and answering on an M1. aarch64 with no baseline flags now emits the same three crc32 instructions as a +crc build and takes them at runtime. Verified on aarch64 with and without +crc, armv7, big-endian powerpc and x86-64, which correctly reports the bit absent and is otherwise untouched. Every sample in the tree still builds.
…M_ARM64 CLANGARM64 on windows-11-arm fails to build: encoding_crc32.c:286:10: fatal error: 'arm64_neon.h' file not found arm64_neon.h is an MSVC header. Selecting it on _M_ARM64 alone breaks any Clang that defines that macro for Windows-on-ARM compatibility -- MSYS2's CLANGARM64 does, and it ships arm_acle.h and no arm64_neon.h. Clang targeting aarch64-pc-windows-msvc defines it too, while the mingw32 triple does not, so the macro says nothing useful about which header exists. Key on _MSC_VER && !__clang__ instead. The bad include predates this series, but nothing reached it until the runtime ARM dispatch started compiling the block on targets whose baseline lacks __ARM_FEATURE_CRC32, which is exactly this one. Reproduced by forcing -D_M_ARM64 on an aarch64 Clang: the same fatal error before, clean after. Rechecked across gcc and clang on x86-64, aarch64 gcc with and without +crc, aarch64 clang plain and with -D_M_ARM64, armv7 and big-endian powerpc.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three commits against
libretro-common/encodings/encoding_crc32.c. The first is acorrectness fix, the second is performance, the third is the regression test that
should have existed before either.
1. There is a live data race in the current code
The slicing-by-8 tables are built on first call into a mutable
.bssarray behind anunsynchronised
crc32_slice8_readyflag. Two threads entering concurrently both runthe generator. The flag is published with a plain write after the table stores, so
nothing orders the publication against the payload. A reader can observe
ready == 1and then load a half-written entry — which produces a silently wrong checksum, not
a crash.
This is reachable:
encoding_crc32()is called from the task queue (content scanningvia
interface_stream.c), fromnetplay_frontend.con the core-memory hash, from thezip/7z/zstd archive readers, and from CHD hunk verification in
rchd.c.ThreadSanitizer over eight threads released from a barrier reports nine distinct
races against current master, including reads of the 8 KiB table concurrent with its
construction. The new test in commit 3 reports the same nine when pointed at the
pre-patch file.
Fixed by precomputing the tables and emitting them
constfrom a generated header:.rodata, no construction, shared between processes, cannot race.2. Every non-x86 target has been running the byte-at-a-time loop
The fast path was gated on
__i386__ || __x86_64__. The only thing actually tying itto x86 was the assumption of little-endian word loads, so this byte-swaps under
MSB_FIRSTand lets everyone use it.The ARM path is gated on
__ARM_FEATURE_CRC32, which stockaarch64-linux-gnu-gccdoes not define — and grepping the tree, only
Makefile.libnxpasses+crc. SoAndroid, Linux ARM, Raspberry Pi and the Apple targets that don't get it from their
compiler default have all been on the byte loop, along with PPC, MIPS and RISC-V.
3. PCLMULQDQ folding on x86
Slicing-by-8 issues one table load per input byte and accumulates all eight lookups
into a single register, so each 8-byte iteration is a serial chain of eight dependent
XORs behind an L1 load — 1.44 cycles/byte against a load-port floor of roughly 0.5.
Worth being precise, because the obvious guess is wrong: this is not a cache
problem. Cachegrind over a 256 KiB working set shows the 8 KiB table staying
resident, with D1 misses accounting for compulsory stream fill only, and there are no
register spills in the loop on either x86-64 or i386. It is latency-bound on the
dependency chain.
Carry-less multiply folding removes the chain rather than tuning around it: four
independent 128-bit accumulators, 64 bytes per iteration, data references down from
1.25 per byte to 0.064.
Dispatch is at runtime on the CPUID PCLMULQDQ bit, so generic x86 builds pick it up
without raising the baseline ISA; pre-Westmere and pre-Bulldozer parts keep the table
path, as do buffers under 64 bytes where the reduction tail costs more than it saves.
Folding constants are all
K(n) = bit_reflect_32(x^n mod P(x)) << 1, derived ratherthan copied from the Intel paper; the derivation is in the comment above them and
reproduces the published values.
Detection is cached in a single
retro_atomic_int_tholding the whole tri-state ratherthan a plain flag guarding a separate payload, so concurrent probers merely store the
same value. Same acquire/release idiom as
cpu_features_get().Performance
Xeon @2.8GHz (TSC-referenced), gcc 13
-O2, median of 15 trials per cell.Throughput, GB/s
crc32()Cycles per byte
crc32()Speedup
Note the current code is slower than zlib through the middle of that range; zlib 1.3
braids multiple streams specifically to break the dependency chain described above.
Run-to-run spread was 1–8% for most cells (worst 18%, at 64 B). This is a shared vCPU;
the 64 MiB column is DRAM-bandwidth bound here rather than compute bound and should look
different on a part with a large L3.
Cross-architecture. qemu-user timings say nothing about real hardware, so instead:
instructions executed in the steady-state loop per input byte,
-O2 -fno-inline.The byte loop measures 7.00 insns/byte and 7.02 cycles/byte on x86 — IPC ≈ 1, entirely
dependency-bound, which is why it is so much worse than its instruction count suggests.
Derived call-site impact (arithmetic from the throughput above, not measured in situ):
4. Regression test
libretro-common/samples/encodings/crc32/, following thevcdiffsample conventions(
-Wall -pedantic -std=gnu99,SANITIZER=opt-in,HAVE_THREADSgate). 0.3s natively,0.4s under ASan.
Values are pinned against a bit-by-bit reference built from the polynomial — no tables,
no word loads, endian-agnostic — plus twelve frozen constants (the published
CRC-32/ISO-HDLC check value and zlib outputs) so a broken reference cannot cancel out a
broken implementation.
The length sweep allocates each buffer at exactly the size read, so the last valid byte
abuts an ASan redzone and a SIMD load that runs one iteration too far is an error rather
than a silent success. A guard-page section repeats that without a sanitizer, hashing 901
buffers that end flush against a
PROT_NONEpage, which is what gives the qemu lanesoverread coverage.
Verified by injecting eleven regressions and confirming each is caught: fold-tail
overread, dispatch threshold, three separate constant bit-flips, both complements, a
transposed slice index, a dropped seed on the SIMD path, the atomic reverted to a plain
int, and theMSB_FIRSTbyteswap removed. That last is caught only on big-endian —on x86 the swap compiles away and everything else still passes — which is why powerpc is
in the cross-arch matrix.
Two things about the concurrency section are load-bearing and commented as such. It runs
first, because any section ahead of it warms the feature-detection cache on the main
thread and leaves the workers reading an already published value. And its workers make
exactly one dispatching call each: TSan keeps only a few shadow entries per address, so a
one-write/many-read lazy init gets crowded out of that history by further traffic.
Measured on this test, eight threads making one call each report the race 30 runs out of
30; the same eight threads making forty calls each report it in none, and raising the
thread count makes it worse rather than better. Sustained-load checks live in a separate
section that runs afterwards.
CI. Registered in
Linux-libretro-common-samples.ymlthree ways: added to theexecuted-target list (already built under
SANITIZER=address,undefined), aClang/ThreadSanitizer step alongside the existing
retro_atomic_testone, and a newcrc32-crossjob covering aarch64, aarch64+crc, armv7 and big-endian powerpc underqemu-user. The two aarch64 entries are not redundant: the
+crcobject contains threecrc32*instructions and the default object none.Verification
Bit-exact against a bitwise reference and against zlib
crc32()for lengths 0–1200 atall 16 alignments, streaming splits, non-zero seeds, 1 MiB blocks and a 5 MiB single
shot. Canonical check value
CBF43926confirmed.--leak-check=full --track-origins-pedantic,-m32,HAVE_THREADS=0Each commit passes the test individually, so the series is bisect-safe.
Two notes. Helgrind and DRD each report findings on the feature cache; both are false
positives — neither models
__atomichappens-before, confirmed by running them against aprogram doing nothing but atomic acquire-loads and release-stores, where a race is
definitionally impossible. TSan is clean. Separately, clang's
unsigned-integer-overflowflaggedwhile (len--)wrappinglentoSIZE_MAX; that isdefined behaviour and predates this series, but it is free to remove and the tail loops
now decrement in the body.
Open question, not addressed here
__ARM_FEATURE_CRC32is still only enabled byMakefile.libnx. This series makes thefallback fast and proves the intrinsic path correct under qemu, but does not turn
+crcon anywhere. Doing that properly needs runtime
HWCAPdetection or a per-targetbuild-flag decision, and seemed better kept separate.
Commits
encodings/crc32: make slicing-by-8 const, universal and thread-safeencodings/crc32: add PCLMULQDQ folding path on x86samples/encodings: add a crc32 regression test