Skip to content

[native] Reuse native compression for CRC32 hashes - #13356

Open
simonrozsival wants to merge 1 commit into
mainfrom
simonrozsival-platform-crc32-evaluation
Open

simonrozsival wants to merge 1 commit into
mainfrom
simonrozsival-platform-crc32-evaluation

Conversation

@simonrozsival

@simonrozsival simonrozsival commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

Context: #12013

CoreCLR assembly and DSO lookups maintain a hand-written IEEE CRC32 implementation even though the host already links libSystem.IO.Compression.Native for Zstandard decompression. Reuse its CompressionNative_Crc32 export rather than adding a direct zlib dependency.

Removing constexpr does not remove any compile-time hashing used by the current runtime. The only two consumers, AssemblyStore::open_assembly() and MonodroidDl::monodroid_dlopen(), hash names supplied at runtime. The internal literal-array consteval overload has no callers, and this header is not a shipped public API. The change removes the unused ability to use this helper in constant expressions; the runtime wrapper remains inline. Separately, replacing the inlined bit loop with an external compression-library call changes runtime execution, so its effect on short-name lookup performance still needs measurement.

Keep the UINT32_MAX empty-input sentinel and split large inputs into INT32_MAX-sized chunks to match the native entry point's signed length parameter. Seed with zero: the compression implementation already performs IEEE CRC32's initial/final complements; no length XOR is required. Managed generation and collision disambiguation are unchanged. The final change removes six net source lines; the maintainability benefit is eliminating the duplicate algorithm, not a large source reduction.

Validation was performed locally; $artifacts below is the out-of-tree probe/evidence directory, not checked-in tests:

  • make prepare CONFIGURATION=Release — passed. make all CONFIGURATION=Release built the native/product solution but failed local-workload setup because the extra API37.1 reference assembly was absent. make leeroy CONFIGURATION=Release then passed, building extra API levels and configuring the local workload.
  • ./dotnet-local.sh test bin/TestRelease/net11.0/Microsoft.Android.Build.Tasks.Tests.dll --filter 'FullyQualifiedName~CreateAssemblyStoreTests|FullyQualifiedName~CoreClrBootstrapBlobTests|FullyQualifiedName~GenerateNativeApplicationConfigSourcesTests' --logger 'trx;LogFileName=crc32-host-tests.trx' --results-directory "$artifacts/host-test-results" — 19 passed, zero failed.
  • "$ndk/bin/${triple}24-clang++" -std=c++23 -O2 -Wall -Wextra -Werror -fno-exceptions -fno-rtti -nostdlib++ -I src/native/clr/include -I "$artifacts" "$artifacts/crc32-correctness.cc" "$library" -o "$artifacts/crc32-correctness-android-$rid" — passed for ARM, ARM64 and x64 at API24 using NDK28.2.13676358 and each ABI's exact pinned 12.0.0-alpha.1.26523.103 compression library; ELF inspection confirmed the expected CRC export/import and no libc++ dependency. ARM32/x64 execution was not performed.
  • adb -s emulator-5554 shell 'cd /data/local/tmp/crc32-eval-767749ff && sha256sum libSystem.IO.Compression.Native.so && LD_LIBRARY_PATH=. ./crc32-correctness-android-arm64 && ./crc32-chunking-android-arm64' — passed on ARM64/API35: 54,115 original/candidate comparisons through both overloads, IEEE known vectors, empty/null/default-view sentinels, and seven recording-shim chunk boundaries through 2*INT32_MAX+17. The chunk probe checks length splitting/seed chaining, not real multi-gigabyte data processing. All 50 fixed-seed Android random inputs also matched exact managed System.IO.Hashing10.0.12 results.
  • ./dotnet-local.sh build "$artifacts/collision-app/CrcCollision.csproj" -c Release -t:Rebuild, followed by adb -s emulator-5554 install -r "$artifacts/matched/$side.apk" and adb -s emulator-5554 shell am start -n com.example.crc32eval767749ff/scrc64d32e1997ab86caac.MainActivity — both matched baseline/candidate APKs passed actual assembly-store and DSO collision lookup, repeated reverse-order lookups, and missing-DSO handling. Distinct assemblies/native libraries use the genuinely colliding names xtazkbhaltig and qzxwhiivqwxp and return distinct values. The probe explicitly pins the Microsoft.NETCore.App framework reference to 12.0.0-alpha.1.26523.103; SDK defaults otherwise select an older .NET11 runtime.
  • ./dotnet-local.sh build -t:Install -c Release -p:RuntimeIdentifier=android-arm64 -p:UseMonoRuntime=false -p:CustomAfterMicrosoftCommonTargets="$artifacts/pin-test-runtime.targets" tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj, then from that project directory ../../../dotnet-local.sh test Mono.Android.NET-Tests.csproj --no-build -c Release -p:RuntimeIdentifier=android-arm64 -p:UseMonoRuntime=false -p:CustomAfterMicrosoftCommonTargets="$artifacts/pin-test-runtime.targets" --report-trx --results-directory "$artifacts/runtime-test-results-pinned" — 991 passed, seven skipped, zero failed on ARM64/API35 with the explicit pinned runtime override. APK extraction verified the exact pinned compression library and locally built candidate host bytes.

Matched Release native rebuilds used the same configured CMake directories/toolchain/flags with only this header changed: ninja -C src/native/obj/Release/CoreCLR/android-$abi-Release net-android.release. Candidate rebuilds reproduced the original candidate binaries byte-for-byte. Product ELF sizes fell by 4/88/72 bytes for ARM/ARM64/x64 respectively; llvm-strip --strip-all copies fell by 108/88/120 bytes. In the matched ARM64 APKs, every ZIP entry's content except libmonodroid.so was identical. That runtime entry shrank by 88 raw bytes and 124 compressed bytes, but both signed APKs were 6,431,898 bytes: no final APK-size reduction after alignment.

DT_NEEDED is unchanged for all three ABIs: no new library or libc++ dependency. Compression already depends on platform libz.so. One CRC import/PLT entry is added. Both product APKs contain the exact pinned compression SHA256 2d9c71cf57e9b8ffa11a3bb8e5ef200aece4f6a385ba20d03124e8e7e60ee607; candidate libmonodroid.so SHA256 is 5a047ca45056627b011249d415609970b8be353ab0b1eddce6c1cceda4fffb93. The observed compression/libz/host file-backed mapping extents were unchanged; this is not RSS/PSS evidence.

Earlier macOS ARM64 checks against installed .NET11 RC2 also passed 50/50 random inputs, but are separate from the pinned Android execution above. Short-name lookup/startup performance remains unmeasured; no no-regression or performance improvement is claimed. No timing was performed under the correctness-only validation lease, which has been released.

Context: #12013

CoreCLR assembly and DSO lookups use a hand-written IEEE CRC32 implementation, while the host already links libSystem.IO.Compression.Native for Zstandard decompression. The pinned compression library exports CompressionNative_Crc32 on all three supported CoreCLR ABIs, so reuse that implementation without adding a library dependency.

Preserve the UINT32_MAX empty-input sentinel and process size_t lengths in INT32_MAX-sized chunks to match the compression entry point's signed length parameter. Start the CRC with zero because the compression implementation applies the initial and final complements internally. Remove the unused internal constant-evaluation overload.

A fixed-seed host probe compared the original implementation with both production wrapper overloads for 50 random byte buffers: all matched. The empty sentinel and independent IEEE CRC32 check vector also passed. This used the installed macOS .NET 11 RC2 compression library. API-24 probes compiled and linked against the pinned Android .NET 12 libraries on ARM, ARM64 and x64 without libc++. Android execution, full product validation and size/performance measurements remain pending.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings October 30, 2026 15:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Assembly and DSO lookup now cross a native runtime ABI, while hash parity, collision handling, and full SDK/device validation against the pinned Android runtime remain pending.

No open issues 🎉

What changed in this PR

This PR replaces CoreCLR’s hand-written CRC32 implementation with the existing native compression library’s CompressionNative_Crc32 export, building on the CRC32 lookup migration in #12013 without adding a direct zlib dependency.

Changes:

  • Preserves the UINT32_MAX empty-input sentinel and uses a zero seed for nonempty inputs.
  • Splits large inputs into chunks compatible with the export’s signed 32-bit length parameter.
  • Removes the unused constant-evaluation overload while leaving managed hash generation and collision handling unchanged.
File Description
src/​native/​clr/​include/​runtime-base/​crc32.hh Replaces manual CRC32 computation with native compression calls, preserving empty-input behavior and supporting large inputs.

Review effort: Balanced

@simonrozsival

Copy link
Copy Markdown
Member Author

/review

@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

✅ Android PR Reviewer completed successfully!

Generated by Android PR Reviewer for #13356

@simonrozsival simonrozsival added the ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable). label Oct 30, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review: no blocking findings

0 errors · 0 warnings · 2 inline suggestions. Reviewed the complete changed header, both hashing callers, host linkage, and managed hash producers before reconciling the PR description.

The change preserves the empty-input sentinel, starts CRC accumulation at zero, and bounds each native call to a signed 32-bit length while carrying the previous CRC forward. Both current callers hash runtime names, so removing the unused constant-evaluation overload does not remove a current compile-time use. The CoreCLR host already links System.IO.Compression.Native; no new link dependency is introduced. I found no correctness issue in the diff. The two optional suggestions are posted inline.

CI verification is limited: all 42 GitHub check runs report completed/success, including the aggregate dotnet-android check for Azure build 1656223. Direct Azure build/timeline verification was blocked by CLI authentication/tool failure, and access to the public endpoint was denied. I therefore cannot independently confirm the Azure build result or give an unconditional CI-green/merge-ready verdict. The local/device results in the description were not rerun during this review.

Generated by Android PR Reviewer for #13356 · copilot · gpt61 · 21.1 AIC · ⌖ 4.35 AIC · ⊞ 20K
Comment /review to run again

constexpr int32_t MaxChunkSize = std::numeric_limits<int32_t>::max ();
// IEEE CRC32 (0xedb88320); the compression shim applies the initial/final complements.
hash_t crc = 0;
while (len > MaxChunkSize) [[unlikely]] {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 💡 Testing — Could we retain the small correctness/recording-shim probes described in the PR as checked-in regression coverage? This introduces a native ABI dependency and seed chaining, but the only changed file is the implementation. A deterministic test covering "123456789" → 0xcbf43926, empty/default views → UINT32_MAX, embedded NUL/high-bit bytes, and recorded calls around INT32_MAX would preserve the evidence for future runtime-pack updates. The recording shim can exercise chunk lengths, pointer advancement, and seed propagation without allocating multi-gigabyte buffers; at least one vector should also exercise the real compression export.

{Rule: Test edge cases and preserve regression coverage}

consteval auto crc32_hash (const char (&value)[Size]) noexcept -> hash_t
{
return crc32_hash (value, Size - 1);
return CompressionNative_Crc32 (crc, bytes, static_cast<int32_t>(len));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 💡 Performance — Consider adding a matched ARM64 measurement for representative short assembly/DSO names before treating the runtime cost as settled. Both callers use this on lookup paths, so replacing the inlined bit loop with an external call trades per-byte work for call/PLT overhead; large-buffer CRC throughput would not establish the effect here. The PR correctly says this is unmeasured, so this is a request for evidence, not a claim that the change regresses performance. A short-name microbenchmark plus repeated app-start measurements would make the maintainability tradeoff easier to assess.

{Rule: Consider startup-time impact}

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants