Skip to content

[LlvmIrGenerator] ~4x faster type map generation - #12279

Open
jonathanpeppers wants to merge 11 commits into
mainfrom
jonathanpeppers-silver-spork
Open

[LlvmIrGenerator] ~4x faster type map generation#12279
jonathanpeppers wants to merge 11 commits into
mainfrom
jonathanpeppers-silver-spork

Conversation

@jonathanpeppers

@jonathanpeppers jonathanpeppers commented Jul 31, 2026

Copy link
Copy Markdown
Member

Fixes a hot spot in LlvmIrGenerator that dominates <GenerateTypeMappings/>, and unifies three copies of "bytes → hex" while we're in there.

The problem

WriteStringBlobArray() emits one LLVM IR element per byte of the type-map string blobs — 3,630,285 calls for a hello-world MAUI app (96.5% of all elements in the 52.6 MB .ll). Each one went through WriteType() + WriteValue(), which do Type.IsPrimitive, TypeUtilities.IsArray(), IsSubclassOf(), plus boxing and several string allocations — all to emit the constant i8 and two hex digits.

The fix

  1. Write bytes directly in the blob loop: "i8 u0x" + two hex chars. No reflection, no allocation.
  2. Basic-type fast path in WriteType()basicTypeMap.ContainsKey() short-circuits the reflection probing. Safe because nothing in basicTypeMap can be a structure instance, array, sectioned array or string blob. This helps all callers, including the 132,549 i32 struct fields.

Results

dotnet new maui -sc, CoreCLR, GenerateTypeMappings on a clean build:

before after
3,204 ms 792 ms

~4× faster, −2.4 s. Measured by reverting only LlvmIrGenerator.cs against an otherwise identical SDK build, so nothing else varied.

Output is byte-identical: typemaps.arm64-v8a.ll SHA256 D875D690… across every run, before and after.

HexUtilities.cs

We had three private hex implementations. They're now one public static class HexUtilities in Microsoft.Android.Build.BaseTasks:

was now
Files.GetHexValue() / Files.ToHexString() HexUtilities.ToHexString()
ScannerHashingHelper.ToHexString() / GetHexValue() HexUtilities.ToHexString (hash, upperCase: false)
(new) generator hot path HexUtilities.WriteHex (TextWriter, byte, …)

API: GetHexValue(), WriteHex(Span<char>, …), WriteHex(TextWriter, …) (no stackalloc), ToHexString(). The upperCase flag exists because Files emits uppercase while ScannerHashingHelper and LLVM IR emit lowercase.

Microsoft.Android.Sdk.TrimmableTypeMap still source-links the file — a ProjectReference to BaseTasks would drag in Microsoft.Build.*, LibZipSharp (native assets), K4os.LZ4 and Mono.Unix. That linked copy stays internal via one #if, since two public Microsoft.Android.Build.Tasks.HexUtilities types make Xamarin.Android.Build.Tasks fail with CS0433 (it references both assemblies).

Measured and rejected

Short-circuiting all numeric writes saved ~33 ms of ~1,500 ms — noise. Not worth an i32 fast path; the rest is mostly I/O for a 52.6 MB file.

The real remaining win is emitting c"..." string literals instead of per-byte i8 arrays (~52.6 MB → ~5 MB), which would also cut llc time. Left for a follow-up.

Testing

  • Microsoft.Android.Build.BaseTasks-Tests: 129/129 (was 107 — 22 new HexUtilities tests)
  • LlvmIrGeneratorTests: 9/9
  • Xamarin.Android.sln builds clean

jonathanpeppers and others added 8 commits July 31, 2026 08:47
WriteStringBlobArray() emitted each byte of a string blob by calling
WriteType() and WriteValue().  Both take a Type, so every byte
performed reflection (Type.IsPrimitive, Type.IsArray,
Type.IsSubclassOf()), boxed the �yte, and allocated two strings.

For a dotnet new maui -sc app the CoreCLR Debug typemap contains
3,630,293 blob bytes across three blobs, so this ran ~3.6 million times.

dotnet-trace of an incremental build with a XAML-only change showed
WriteStringBlobArray() at 2,507ms of the 3,612ms spent in
<GenerateTypeMappings/>, with RuntimeType.IsPrimitiveImpl() alone
accounting for 2,433ms.

The rendering of a byte is one of only 256 possible strings, so cache
them in a static table.  Output is byte-for-byte identical.

<GenerateTypeMappings/>: 3,727ms -> 568ms
Total incremental build:   8.37s   -> 3.11s

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d1a93958-51c0-4df6-b848-97dc8c93f3e6
Follow up to the string blob optimization: instead of special-casing
byte arrays only, add a fast path at the top of `WriteType()` for the
scalar types in `basicTypeMap` (`bool`, `byte`, `int`, ...).

These types can never be a `StructureInstance`, an array, an
`LlvmIrSectionedArrayBase` or an `LlvmIrStringBlob`, so all the
reflection-heavy probing (`Type.IsPrimitive`, `Type.IsArray`,
`Type.IsSubclassOf`) that ran before reaching the trivial
`basicTypeMap` lookup was pure overhead.  Arrays and string blobs
write one element at a time, so this ran ~3.6 million times for a
hello world MAUI application.  This now benefits every caller, not
just string blobs.

Also replace the 256 entry lookup table added previously with a direct
write of `i8 u0x` plus the two hex digits, and factor the hex digit
conversion duplicated in `Files.cs` and `ScannerHashingHelper.cs`
into a new shared `HexUtilities` class.

Timings for an incremental, XAML only change to a
`dotnet new maui -sc` project:

  * `GenerateTypeMappings`: 3,727ms -> ~600ms
  * Total build: 8.37s -> ~3.1s

`typemaps.arm64-v8a.ll` is byte for byte identical before and after.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d1a93958-51c0-4df6-b848-97dc8c93f3e6
Drop the one line private ToHexString wrapper and have both callers use
the shared helper directly.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d1a93958-51c0-4df6-b848-97dc8c93f3e6
Covers upper/lower case digits, the default casing, empty input, all
256 byte values, and lengths either side of the 128 char stackalloc
threshold in ToHexString().

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d1a93958-51c0-4df6-b848-97dc8c93f3e6
Move the nibble splitting out of the callers and into two WriteHex()
overloads, one writing to a Span<char> and one to a TextWriter.
ToHexString() now shares the Span<char> overload, and LlvmIrGenerator
no longer does its own bit math.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d1a93958-51c0-4df6-b848-97dc8c93f3e6
Rather than source linking HexUtilities.cs into the test project, grant
it access via InternalsVisibleTo, matching Xamarin.Android.Build.Tasks
and Microsoft.Android.Sdk.TrimmableTypeMap.  The test assembly is now
signed with product.snk, as the other test projects already are.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d1a93958-51c0-4df6-b848-97dc8c93f3e6
Write the two digits straight to the TextWriter rather than staging
them through a 2 char stackalloc.  Also fixes a stray brace introduced
in the previous commit.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d1a93958-51c0-4df6-b848-97dc8c93f3e6
Removes the InternalsVisibleTo (and the product.snk signing it forced on
the test project), plus the <Compile Include/> source link in
Xamarin.Android.Build.Tasks, which references BaseTasks already.

Microsoft.Android.Sdk.TrimmableTypeMap still links the source, because a
ProjectReference to BaseTasks would drag Microsoft.Build.*, LibZipSharp,
K4os.LZ4 and Mono.Unix into it.  That copy stays \internal\ -- two public
Microsoft.Android.Build.Tasks.HexUtilities types make Xamarin.Android.Build.Tasks
fail with CS0433, since it references both assemblies.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d1a93958-51c0-4df6-b848-97dc8c93f3e6
Copilot AI review requested due to automatic review settings July 31, 2026 16:42

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.

Pull request overview

This PR reduces <GenerateTypeMappings/> time by removing per-byte reflection/boxing overhead in LlvmIrGenerator and consolidating duplicated “bytes → hex” logic into a single shared HexUtilities helper.

Changes:

  • Optimized string-blob IR emission by writing i8 u0xNN directly for each byte and adding a basic-type fast path in WriteType().
  • Introduced HexUtilities in Microsoft.Android.Build.BaseTasks and switched existing hex call sites (Files, ScannerHashingHelper) to use it.
  • Added dedicated unit tests for HexUtilities.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/Microsoft.Android.Build.BaseTasks-Tests/HexUtilitiesTests.cs Adds coverage for GetHexValue, WriteHex, and ToHexString behaviors (upper/lowercase, boundaries).
src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrGenerator.cs Removes hot-path overhead by emitting byte constants directly and short-circuiting primitive type handling.
src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs Replaces local hex formatting with shared HexUtilities (lowercase).
src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj Source-links the shared HexUtilities.cs file into the TrimmableTypeMap project.
src/Microsoft.Android.Build.BaseTasks/Microsoft.Android.Build.BaseTasks.csproj Defines a compile constant to make HexUtilities public only in BaseTasks.
src/Microsoft.Android.Build.BaseTasks/HexUtilities.cs New shared implementation for allocation-free hex rendering.
src/Microsoft.Android.Build.BaseTasks/Files.cs Replaces in-file hex conversion with HexUtilities.ToHexString().
Suppressed comments (3)

src/Microsoft.Android.Build.BaseTasks/HexUtilities.cs:67

  • 🤖 ⚠️ warning Formatting — Mono style requires a space before [ in array/stackalloc expressions. This avoids mixing standard C# formatting with the repo’s convention.
			Span<char> chars = charLength <= MaxStackCharLength
				? stackalloc char[charLength]
				: new char[charLength];

src/Microsoft.Android.Build.BaseTasks/HexUtilities.cs:41

  • 🤖 ❌ error API designWriteHex(Span<char> destination, ...) assumes destination.Length >= 2 and otherwise throws IndexOutOfRangeException. Since this helper is public in BaseTasks, validate the span length and throw an ArgumentException with a clear message.
		public static void WriteHex (Span<char> destination, byte value, bool upperCase = true)
		{
			destination [0] = GetHexValue (value >> 4, upperCase);
			destination [1] = GetHexValue (value & 0x0f, upperCase);
		}

src/Microsoft.Android.Build.BaseTasks/HexUtilities.cs:30

  • 🤖 ⚠️ warning Formatting — This repo uses Mono style spacing; casts should be written with a space (e.g. (char) (value + '0')) for consistency with the rest of BaseTasks.
			if (value < 10)
				return (char)(value + '0');
			return (char)(value - 10 + (upperCase ? 'A' : 'a'));

Comment thread src/Microsoft.Android.Build.BaseTasks/HexUtilities.cs
Addresses review feedback: WriteHex(Span<char>) now throws ArgumentException
instead of IndexOutOfRangeException when the destination is too short, and
casts/array creation use the repo's spacing convention.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d1a93958-51c0-4df6-b848-97dc8c93f3e6
@jonathanpeppers

Copy link
Copy Markdown
Member Author

Went through the review feedback -- checked each item against the actual repo rather than taking it at face value. Fixes in 195cab0.

Applied

Feedback Outcome
WriteHex(Span<char>) should validate length Fair -- it is public now, so IndexOutOfRangeException is a poor contract. Throws ArgumentException instead, + 3 tests.
Cast spacing (char) ( Repo is 49:2 in favour.
stackalloc char [n] spacing Repo is 30:16 in favour.

Both spacing issues came from copying the Files.cs code this replaces verbatim.

Tests: 126 -> 129 passing.

Declined

  • #endif annotation -- not the repo convention (17 bare vs 11 annotated in these two projects), and the extra cited locations are not preprocessor directives.
  • Redundant dictionary lookup in the WriteType() fast path -- the suggested fix would skip MapToIRType()'s native-pointer handling, and the measured ceiling for all numeric writes is ~33 ms of ~1,500 ms. Details in the thread.

@jonathanpeppers

Copy link
Copy Markdown
Member Author

/review

@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Android PR Reviewer completed successfully!

@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 — ⚠️ minor suggestions

Strong, well-scoped PR. I read the changed files in full and independently verified the key claim: the byte-write fast path and WriteType() basic-type short-circuit are behavior-preserving.

Verification

  • WriteByteTypeAndValue: original WriteType()+WriteValue() emitted i8 + u0x + {b:x02} (2 lowercase hex). New code emits "i8 u0x" + HexUtilities.WriteHex(..., upperCase: false) — byte-for-byte identical. It's only ever reached from WriteStringBlobArray(), which forces NumberFormat = Hexadecimal and elementType = byte, so the hardcoded values are safe today.
  • WriteType() fast path: nothing in basicTypeMap can be a structure/array/sectioned-array/string-blob, and the constructed LlvmTypeInfo matches the general GetIRType() path (lines 535–542) exactly. Equivalent output, less reflection.
  • HexUtilities unification: casing preserved per caller — Files.ToHexString stays uppercase (default upperCase: true); ScannerHashingHelper and the generator pass upperCase: false. The #if MICROSOFT_ANDROID_BUILD_BASETASKS public/internal split correctly avoids CS0433 in Xamarin.Android.Build.Tasks.
  • Tests: 22 new tests cover casing, the 2-char span contract, null TextWriter, all 256 byte values, empty input, and the 128-char stackalloc threshold — thorough.

CI

GitHub-surfaced Azure DevOps checks are green (Java.Interop + Android Tools tests passing); Linux/Mac/Windows build legs still in progress at review time. No failures observed.

Suggestions (non-blocking, posted inline)

  • 💡 Guard the hidden i8/hex invariant in WriteByteTypeAndValue with a Debug.Assert.
  • 💡 Now-public GetHexValue silently produces garbage for out-of-range input — consider a Debug-only assert.

Nice work, especially the measured-and-rejected notes and byte-identical .ll SHA verification. 👍

Generated by Android PR Reviewer for #12279 · 124.2 AIC · ⌖ 18.7 AIC · ⊞ 6.9K
Comment /review to run again

Comment thread src/Microsoft.Android.Build.BaseTasks/HexUtilities.cs
jonathanpeppers and others added 2 commits July 31, 2026 12:39
HexUtilities is now public, so validate the
Debug.Assert is already [Conditional ("DEBUG")], so the wrapper added nothing
-- the call and its interpolated string are elided in release either way.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d1a93958-51c0-4df6-b848-97dc8c93f3e6
@jonathanpeppers jonathanpeppers added the ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable). label Jul 31, 2026
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