[native] Investigate further CoreCLR host size reductions
メンテナーはふだん 1 日以内に返信
まだ誰も着手していません。
評価
- 難易度
- 5/5
- 見積もり時間
- 1週間以上
- 初心者へのやさしさ
- 35/100
- issue の種類
- リファクタリング
- 明瞭さ
- おおむね明確
- 活発さ
- 活発
- 技術スタック
- android, cpp
調査の方向性
ARM64 net-android.release ターゲットを準備し、libmonodroid.so のリンク済みサイズとストリップ後のサイズを記録します。動作に影響しないリンカーおよびコンパイラーの変更を個別に評価し、その後 API-24 のサンプル APK 内の DSO を置き換えて、ARM64 エミュレーターでコールドスタートします。必要な JNI エクスポートとマネージド起動が引き続き機能し、サイズ、起動、リソース、リンク時間、診断の結果がサポート対象のすべての ABI について記録されていれば完了です。
索引モデルが issue の本文から書いたものです。
説明
Follow-up to #12533.
Android framework version
net11.0-android (Preview)
Affected platform version
Current dotnet/android CoreCLR native runtime stack ending at PR #12572; ARM64 Release build targeting Android API 24.
Description
After removing the CoreCLR host's libc++ dependency, investigate reducing the remaining libnet-android.release.so/libmonodroid.so footprint. A local-only proof of concept reduced the ARM64 host from 215,064 bytes to 58,408 bytes (72.8%), while still cold-starting a CoreCLR application on an ARM64 Android emulator.
The experiments fall into three groups: mechanical linker/compiler improvements, optional feature removal, and aggressive diagnostic removal. They should be evaluated independently rather than landed as one change.
Baseline
| Artifact | Bytes |
|---|---|
| Normal linked ARM64 Release DSO | 215,064 |
llvm-strip --strip-unneeded |
168,392 |
llvm-strip --strip-all |
167,928 |
Stripping preserved the JNI entry points in .dynsym. Internal P/Invoke implementations do not rely on .symtab: CoreCLR calls the live P/Invoke override callback, which returns function pointers. A stripped host successfully reached managed startup.
Measured progression
The values are cumulative and some optimizations interact, especially LTO and ICF.
| Configuration | Strip mode | Bytes |
|---|---|---|
| Baseline | --strip-unneeded |
168,392 |
Per-function/data sections + --gc-sections |
--strip-unneeded |
145,744 |
Experimental --icf=all before LTO |
--strip-unneeded |
139,856 |
--as-needed + --no-export-dynamic |
--strip-unneeded |
139,840 |
| Android packed relocations | --strip-unneeded |
130,656 |
Non-throwing string_view construction |
--strip-unneeded |
130,624 |
| Full LTO across host and linked archives | --strip-unneeded |
126,048 |
| Disable fast timing | --strip-unneeded |
101,120 |
| Disable decompressed-assembly disk cache | --strip-unneeded |
94,944 |
| Compile out Debug/Info logging | --strip-unneeded |
87,328 |
| Inline ARM64 atomics | --strip-unneeded |
85,168 |
| Disable configurable logging | --strip-all |
76,128 |
| Remove abort locations, simplify validation diagnostics, and use timing stubs | --strip-all |
58,408 |
Mechanical changes worth investigating
- Compile both the shared and static CoreCLR host targets with
-ffunction-sections -fdata-sections, then link with--gc-sections. The application-time unified native linker already enables GC, but the archive inputs need function granularity for it to be effective. - Add
--pack-dyn-relocs=android. Android packed relocations are supported by the API 24 minimum and saved about 9 KB. - Enable full LTO for the host and all static archives consumed by it. This saved about 4.6 KB, at the cost of native link time/memory and changed inlining.
- Add
--as-neededand remove the redundant--export-dynamic; this removed the unusedlibm.sodependency but saved only 16 bytes. - Consider
-mno-outline-atomicson ARM64. It removed the compiler-rt CPU-feature dispatcher and saved 2,160 bytes, but may make contended atomics slower on LSE-capable CPUs. - Replace provably in-range
string_view::substr()calls with direct{data, length}views. This avoids libc++ out-of-range abort helpers under size-oriented optimization. - Strip release runtime DSOs during pack production while retaining unstripped symbol artifacts for offline crash symbolication.
- Test
--icf=safe. It initially found no folds; after full LTO it matched the tested--icf=allresult in the minimized profile.--icf=allshould not be used without proving that function-address identity is irrelevant.
Optional feature reductions
Fast timing: approximately 25 KB
The PoC made FastTiming::enabled() a compile-time false, made initialization and managed timing P/Invokes no-ops, and removed Java_mono_android_Runtime_dumpTimingData from the export map. This removes startup timing collection, timing files/logcat output, and debug.mono.timing. A production implementation should probably retain dumpTimingData as a small no-op export for ABI compatibility.
Decompressed-assembly disk cache: approximately 6.2 KB
The PoC made cache initialization/writes no-ops and cache lookup return nullptr; LTO then removed the background writer, queue, mmap validation, hashing, and filesystem code. Normal in-memory decompression still worked. This may improve first launch slightly but makes every subsequent process launch decompress assemblies again, increasing warm-start CPU/battery use.
Aggressive diagnostic reductions
Compile out Debug/Info logging: approximately 7.6 KB
log_debugf() and log_infof() became compile-time no-ops. Warning, error, fatal logging, Android abort messages, and tombstones remained. Call arguments are no longer evaluated, so all call sites would need a side-effect audit.
Disable configurable logging: approximately 8.8 KB
The logging-category parser and reference-log initialization became no-ops. LTO removed assembly-loader diagnostics, GC spew, gref/lref file/logcat tracing, and timing-category configuration. Reference counters and GC behavior remained functional. This is likely suitable only for a separate minimal runtime flavor.
Remove abort source locations and simplify validation messages: approximately 17 KB
The PoC retained the primary fatal message, android_set_abort_message(), tombstones, and abort(), but removed file:line:column, pretty C++ function signatures, and the runtime function-name parser. This greatly reduces duplicated source paths/signatures but significantly harms field diagnostics unless unstripped symbols and stack addresses are available.
Rejected experiments
-Os/-Ozmade the stripped host larger and caused libc++ out-of-range abort helpers to survive where-O2optimized them away.--pack-dyn-relocs=android+relrsaved another 872 bytes, but RELR requires newer Android loaders and is unsuitable for the API 24 minimum.llvm-strip --strip-sectionssaved about 2 KB, but Android rejected the DSO withunsupported e_shentsize: 0x0 (expected 0x40).- Unrestricted
--icf=allsaved 5,888 bytes before LTO but can collapse distinct function addresses and is too risky as a default.
The final 58,408-byte ELF retained these dynamic JNI exports:
JNI_OnLoadJava_mono_android_Runtime_initInternalJava_mono_android_Runtime_registerJava_mono_android_Runtime_propagateUncaughtException
Its largest remaining areas were core runtime behavior: startup, GC bridge processing, P/Invoke dispatch, DSO loading, typemaps, and assembly probing/decompression.
Steps to Reproduce
- Prepare a Release
dotnet/androidnative build and build the ARM64 CoreCLRnet-android.releasetarget. - Record the linked and stripped baseline sizes.
- Apply each compiler/linker option independently and cleanly relink the ARM64 host.
- For feature experiments, compile out timing, the decompressed-assembly cache, and logging independently rather than combining them initially.
- Run
llvm-strip --strip-unneededor--strip-all, verifying the required JNI exports remain in.dynsym. - Replace
lib/arm64-v8a/libmonodroid.soin an API-24 CoreCLR sample APK, zipalign and sign it. - Cold-start it on an ARM64 emulator/device and verify managed UI startup, assembly loading/decompression, typemap setup, GC bridge initialization, and internal P/Invoke resolution.
- Benchmark cold/warm startup, memory, CPU, battery, native link time, and crash diagnosability for each candidate change across all supported ABIs.
Did you find any workaround?
A local uncommitted PoC demonstrates the possible size floor and isolates the candidate changes. The recommended first investigation is the behavior-neutral set: stripping with separate symbols, section GC, Android packed relocations, full LTO, --as-needed, and safe ICF. Timing, cache, and diagnostic removal should be separate opt-in decisions or a distinct minimal-runtime profile.
Relevant log output
# Baseline
linked-size=215064
strip-unneeded=168392
# Final aggressive PoC
strip-all=58408
pid=8696
errors=0
# Managed UI hierarchy
text="Hello, NativeAOT on Android!"
text="HelloNativeAOT"
# Invalid sectionless ELF experiment
E/linker: libmonodroid.so has unsupported e_shentsize: 0x0 (expected 0x40)
java.lang.UnsatisfiedLinkError: dlopen failed: unsupported e_shentsize
- 主要言語
- C#
- スター
- 2.1k
- フォーク
- 581
- 平均マージ
- 2日 3時間
- マージ済み PR(30日)
- 216
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートあり
- コントリビューションガイドなし
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
dotnet/android のほかの issue
-
Area: App+Library Build
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
メンテナーはふだん 1 日以内に返信
-
Area: Mono.Android
難易度 2/5 1〜3時間 初心者へのやさしさ 65/100
dotnet/android#9192 · コメント 4 件 · リアクション 1 件 ·
メンテナーはふだん 1 日以内に返信
-
[Epic] Reduce CI test cost by pruning and consolidating low-value tests対応中かも このイシューにリンクされたプルリクエストがオープン中、またはマージ済みです。 オープンneeds-triage
難易度 4/5 1週間以上 初心者へのやさしさ 45/100
メンテナーはふだん 1 日以内に返信
-
Investigate JNI reference ownership leaks and GC-bridge leak-check reliability対応中かも このイシューにリンクされたプルリクエストがオープン中、またはマージ済みです。 オープンneeds-triage
難易度 4/5 3〜5日 初心者へのやさしさ 50/100
メンテナーはふだん 1 日以内に返信
-
enhancement needs-triage
難易度 5/5 1週間以上 初心者へのやさしさ 35/100
メンテナーはふだん 1 日以内に返信
似ている issue
-
bug component/other
難易度 2/5 1〜3時間 初心者へのやさしさ 73/100
umbraco/Umbraco.AI#511 ·
メンテナーはふだん 1 日以内に返信
-
sev:L
難易度 1/5 1時間未満 初心者へのやさしさ 72/100
Systemorph/MeshWeaver#6233 ·
メンテナーはふだん 1 日以内に返信
-
[Bug]:オープンbug needs response
難易度 2/5 1〜3時間 初心者へのやさしさ 66/100
Adyen/adyen-dotnet-api-library#1874 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 66/100
CommunityToolkit/Aspire#2231 ·
メンテナーはふだん 1 日以内に返信
-
bug
難易度 1/5 1時間未満 初心者へのやさしさ 90/100
Flow-Launcher/Flow.Launcher#4697 ·
メンテナーはふだん 4 日以内に返信