From 3a9aff5e565393a4bd469662a82f3adabf9f0ddd Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Fri, 31 Jul 2026 12:04:46 -0500 Subject: [PATCH 1/6] [LlvmIrGenerator] emit `c"..."` literals for string blobs `WriteStringBlobArray()` emitted one LLVM IR element per byte of the type map string blobs: @type_map_managed_type_names = ... constant [3035518 x i8] [ ; 'Android.OS.AsyncTask' @ 0 i8 u0x41, i8 u0x6e, ... ] For a hello world MAUI app that is ~3.6 million elements, producing a 52.6MB `typemaps.arm64-v8a.ll`, which is slow both to write and for `llc` to consume. Emit a single constant byte string instead, which is the same form the generator already uses for constant string literals: @type_map_managed_type_names = ... constant [3035518 x i8] c"Android.OS.AsyncTask\00..." Bytes are streamed through a pooled `char[]` chunk buffer to avoid millions of single-character `TextWriter` writes. A constant string literal has to fit on a single line, and `;` comments extend to the end of the line, so the per-string `; 'Foo' @ 123` comments are gone. Results for a hello world MAUI app (`dotnet new maui -sc`, Debug, median of 3): * `typemaps.arm64-v8a.ll`: 52.6MB -> 15.2MB * `GenerateTypeMappings`: 1489ms -> 1187ms * `CompileNativeAssembly`: 2239ms -> 203ms The resulting `typemaps.arm64-v8a.o` is byte-for-byte identical to the one produced before this change. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: f6a038cc-e09a-455d-8f08-1ea17ecfd849 --- .../LlvmIrGenerator/LlvmIrGenerator.cs | 94 +++++++++---------- 1 file changed, 44 insertions(+), 50 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrGenerator.cs index 2ea987a2ef6..0ca0673fa9b 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrGenerator.cs @@ -906,71 +906,65 @@ void WriteSectionedArrayValue (GeneratorWriteContext context, LlvmIrSectionedArr void WriteStringBlobArray (GeneratorWriteContext context, LlvmIrStringBlob blob) { - // The stride determines how many elements are written on a single line before a newline is added. - const uint stride = 16; - Type elementType = typeof(byte); + // String blobs are emitted as a single LLVM IR constant byte string (`c"..."`) instead of one + // `i8 u0xNN` element per byte. A hello world MAUI app has ~3.6 million bytes of type map + // strings, so the array form produces a >50MB `.ll` file which is slow both to write here and + // to consume in `llc`. The `c"..."` form is roughly ten times smaller. + // + // A consequence of this is that we can no longer annotate individual strings with comments, + // because a constant string literal must fit on a single line and `;` comments extend to the + // end of the line. + const int chunkSize = 4096; + char[] chunk = ArrayPool.Shared.Rent (chunkSize); + int chunkUsed = 0; - LlvmIrVariableNumberFormat oldNumberFormat = context.NumberFormat; - context.NumberFormat = LlvmIrVariableNumberFormat.Hexadecimal; - WriteArrayValueStart (context); - foreach (LlvmIrStringBlob.StringInfo si in blob.GetSegments ()) { - if (si.Offset > 0) { - context.Output.Write (','); - context.Output.WriteLine (); - context.Output.WriteLine (); + try { + context.Output.Write ('c'); + context.Output.Write ('"'); + + foreach (LlvmIrStringBlob.StringInfo si in blob.GetSegments ()) { + foreach (byte b in si.Bytes) { + WriteByte (b); + } + + // Terminating NUL is counted for each string, but not included in its bytes + WriteByte (0); } - context.Output.Write (context.CurrentIndent); - WriteCommentLine (context, $" '{si.Value}' @ {si.Offset}"); - WriteBytes (si.Bytes); + Flush (); + context.Output.Write ('"'); + } finally { + ArrayPool.Shared.Return (chunk); } - context.Output.WriteLine (); - WriteArrayValueEnd (context); - context.NumberFormat = oldNumberFormat; - void WriteBytes (byte[] bytes) + void WriteByte (byte b) { - ulong counter = 0; - bool first = true; - foreach (byte b in bytes) { - if (!first) { - WriteCommaWithStride (counter); - } else { - context.Output.Write (context.CurrentIndent); - first = false; + // `"` and `\` must always be escaped, as must anything outside of the printable ASCII range. + if (b != (byte)'"' && b != (byte)'\\' && b >= 32 && b < 127) { + if (chunkUsed == chunkSize) { + Flush (); } - - counter++; - WriteByteTypeAndValue (b); + chunk[chunkUsed++] = (char)b; + return; } - if (bytes.Length > 0) { - WriteCommaWithStride (counter); - } else { - context.Output.Write (context.CurrentIndent); + if (chunkUsed + 3 > chunkSize) { + Flush (); } - WriteByteTypeAndValue (0); // Terminating NUL is counted for each string, but not included in its bytes + + chunk[chunkUsed++] = '\\'; + HexUtilities.WriteHex (chunk.AsSpan (chunkUsed), b, upperCase: true); + chunkUsed += 2; } - void WriteCommaWithStride (ulong counter) + void Flush () { - context.Output.Write (','); - if (stride == 1 || counter % stride == 0) { - context.Output.WriteLine (); - context.Output.Write (context.CurrentIndent); - } else { - context.Output.Write (' '); + if (chunkUsed == 0) { + return; } - } - void WriteByteTypeAndValue (byte v) - { - // This is by far the hottest path in the generator: a hello world MAUI app writes - // ~3.6 million bytes here. WriteType()/WriteValue() would box the byte and allocate a - // handful of strings per element, so write the (always identical) type and the two hex - // digits directly. - context.Output.Write ("i8 u0x"); - HexUtilities.WriteHex (context.Output, v, upperCase: false); + context.Output.Write (chunk, 0, chunkUsed); + chunkUsed = 0; } } From d99938fd5ea1cc9b3890f399cc3be6eda5a522a4 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Fri, 31 Jul 2026 12:15:40 -0500 Subject: [PATCH 2/6] [LlvmIrGenerator] use the rented array's actual length ArrayPool.Rent() may hand back an array larger than requested. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: f6a038cc-e09a-455d-8f08-1ea17ecfd849 --- .../Utilities/LlvmIrGenerator/LlvmIrGenerator.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrGenerator.cs index 0ca0673fa9b..051eb59bac2 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrGenerator.cs @@ -914,8 +914,9 @@ void WriteStringBlobArray (GeneratorWriteContext context, LlvmIrStringBlob blob) // A consequence of this is that we can no longer annotate individual strings with comments, // because a constant string literal must fit on a single line and `;` comments extend to the // end of the line. - const int chunkSize = 4096; - char[] chunk = ArrayPool.Shared.Rent (chunkSize); + // `Rent()` may return a larger array than requested, so use its actual length as the capacity. + char[] chunk = ArrayPool.Shared.Rent (4096); + int chunkCapacity = chunk.Length; int chunkUsed = 0; try { @@ -941,14 +942,14 @@ void WriteByte (byte b) { // `"` and `\` must always be escaped, as must anything outside of the printable ASCII range. if (b != (byte)'"' && b != (byte)'\\' && b >= 32 && b < 127) { - if (chunkUsed == chunkSize) { + if (chunkUsed == chunkCapacity) { Flush (); } chunk[chunkUsed++] = (char)b; return; } - if (chunkUsed + 3 > chunkSize) { + if (chunkUsed + 3 > chunkCapacity) { Flush (); } From 0de339acaff396c9ff6fadbd6579bac1f3afa620 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Fri, 31 Jul 2026 12:22:21 -0500 Subject: [PATCH 3/6] [LlvmIrGenerator] comment formatting Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: f6a038cc-e09a-455d-8f08-1ea17ecfd849 --- .../Utilities/LlvmIrGenerator/LlvmIrGenerator.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrGenerator.cs index 051eb59bac2..49b19a5d701 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrGenerator.cs @@ -914,6 +914,7 @@ void WriteStringBlobArray (GeneratorWriteContext context, LlvmIrStringBlob blob) // A consequence of this is that we can no longer annotate individual strings with comments, // because a constant string literal must fit on a single line and `;` comments extend to the // end of the line. + // `Rent()` may return a larger array than requested, so use its actual length as the capacity. char[] chunk = ArrayPool.Shared.Rent (4096); int chunkCapacity = chunk.Length; From 81367aa03a2e45a7533cad45e5ac6c9c5aeb01b6 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Fri, 31 Jul 2026 12:28:00 -0500 Subject: [PATCH 4/6] [LlvmIrGenerator] Mono style spacing Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: f6a038cc-e09a-455d-8f08-1ea17ecfd849 --- .../Utilities/LlvmIrGenerator/LlvmIrGenerator.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrGenerator.cs index 49b19a5d701..477353913a5 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrGenerator.cs @@ -916,7 +916,7 @@ void WriteStringBlobArray (GeneratorWriteContext context, LlvmIrStringBlob blob) // end of the line. // `Rent()` may return a larger array than requested, so use its actual length as the capacity. - char[] chunk = ArrayPool.Shared.Rent (4096); + char [] chunk = ArrayPool.Shared.Rent (4096); int chunkCapacity = chunk.Length; int chunkUsed = 0; @@ -942,11 +942,11 @@ void WriteStringBlobArray (GeneratorWriteContext context, LlvmIrStringBlob blob) void WriteByte (byte b) { // `"` and `\` must always be escaped, as must anything outside of the printable ASCII range. - if (b != (byte)'"' && b != (byte)'\\' && b >= 32 && b < 127) { + if (b != (byte) '"' && b != (byte) '\\' && b >= 32 && b < 127) { if (chunkUsed == chunkCapacity) { Flush (); } - chunk[chunkUsed++] = (char)b; + chunk [chunkUsed++] = (char) b; return; } @@ -954,7 +954,7 @@ void WriteByte (byte b) Flush (); } - chunk[chunkUsed++] = '\\'; + chunk [chunkUsed++] = '\\'; HexUtilities.WriteHex (chunk.AsSpan (chunkUsed), b, upperCase: true); chunkUsed += 2; } From 6a1bce2bd5d33be5fec5574dac5e7cdb54eba6aa Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Fri, 31 Jul 2026 12:46:51 -0500 Subject: [PATCH 5/6] [LlvmIrGenerator] pass an explicit 2-char span to WriteHex Makes the `exactly two hex digits` contract obvious at the call site. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: f6a038cc-e09a-455d-8f08-1ea17ecfd849 --- .../Utilities/LlvmIrGenerator/LlvmIrGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrGenerator.cs index 477353913a5..893f457745f 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrGenerator.cs @@ -955,7 +955,7 @@ void WriteByte (byte b) } chunk [chunkUsed++] = '\\'; - HexUtilities.WriteHex (chunk.AsSpan (chunkUsed), b, upperCase: true); + HexUtilities.WriteHex (chunk.AsSpan (chunkUsed, 2), b, upperCase: true); chunkUsed += 2; } From af5d3a846369580d30feb383aecace6ed094b627 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Mon, 3 Aug 2026 08:14:57 -0500 Subject: [PATCH 6/6] [LlvmIrGenerator] name the chunk/escape sizes, trim comments Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: f6a038cc-e09a-455d-8f08-1ea17ecfd849 --- .../LlvmIrGenerator/LlvmIrGenerator.cs | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrGenerator.cs index 893f457745f..e6b8c6a51fd 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrGenerator.cs @@ -906,17 +906,13 @@ void WriteSectionedArrayValue (GeneratorWriteContext context, LlvmIrSectionedArr void WriteStringBlobArray (GeneratorWriteContext context, LlvmIrStringBlob blob) { - // String blobs are emitted as a single LLVM IR constant byte string (`c"..."`) instead of one - // `i8 u0xNN` element per byte. A hello world MAUI app has ~3.6 million bytes of type map - // strings, so the array form produces a >50MB `.ll` file which is slow both to write here and - // to consume in `llc`. The `c"..."` form is roughly ten times smaller. - // - // A consequence of this is that we can no longer annotate individual strings with comments, - // because a constant string literal must fit on a single line and `;` comments extend to the - // end of the line. - - // `Rent()` may return a larger array than requested, so use its actual length as the capacity. - char [] chunk = ArrayPool.Shared.Rent (4096); + // Emitted as a single `c"..."` literal rather than one `i8` element per byte, which shrinks + // the `.ll` ~10x. No per-string comments are possible, as the literal must be on one line. + const int HexDigits = 2; + const int MaxEscapeWidth = 1 + HexDigits; + const int ChunkSize = 4096; + + char [] chunk = ArrayPool.Shared.Rent (ChunkSize); int chunkCapacity = chunk.Length; int chunkUsed = 0; @@ -941,7 +937,6 @@ void WriteStringBlobArray (GeneratorWriteContext context, LlvmIrStringBlob blob) void WriteByte (byte b) { - // `"` and `\` must always be escaped, as must anything outside of the printable ASCII range. if (b != (byte) '"' && b != (byte) '\\' && b >= 32 && b < 127) { if (chunkUsed == chunkCapacity) { Flush (); @@ -950,13 +945,13 @@ void WriteByte (byte b) return; } - if (chunkUsed + 3 > chunkCapacity) { + if (chunkUsed + MaxEscapeWidth > chunkCapacity) { Flush (); } chunk [chunkUsed++] = '\\'; - HexUtilities.WriteHex (chunk.AsSpan (chunkUsed, 2), b, upperCase: true); - chunkUsed += 2; + HexUtilities.WriteHex (chunk.AsSpan (chunkUsed, HexDigits), b, upperCase: true); + chunkUsed += HexDigits; } void Flush ()