-
Notifications
You must be signed in to change notification settings - Fork 578
[LlvmIrGenerator] emit c"..." literals for string blobs #12280
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
34e1f61
2c0a140
2ac062e
9eb8d38
e11fcc3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -906,71 +906,67 @@ 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. | ||
|
|
||
| // `Rent()` may return a larger array than requested, so use its actual length as the capacity. | ||
| char [] chunk = ArrayPool<char>.Shared.Rent (4096); | ||
| int chunkCapacity = chunk.Length; | ||
| 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<char>.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 == chunkCapacity) { | ||
| Flush (); | ||
| } | ||
|
|
||
| counter++; | ||
| WriteByteTypeAndValue (b); | ||
| chunk [chunkUsed++] = (char) b; | ||
| return; | ||
| } | ||
|
|
||
| if (bytes.Length > 0) { | ||
| WriteCommaWithStride (counter); | ||
| } else { | ||
| context.Output.Write (context.CurrentIndent); | ||
| if (chunkUsed + 3 > chunkCapacity) { | ||
| Flush (); | ||
| } | ||
| WriteByteTypeAndValue (0); // Terminating NUL is counted for each string, but not included in its bytes | ||
|
|
||
| chunk [chunkUsed++] = '\\'; | ||
| HexUtilities.WriteHex (chunk.AsSpan (chunkUsed, 2), b, upperCase: true); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 💡 Documentation — Nice catch reusing the span-based (informational; no change required) |
||
| chunkUsed += 2; | ||
|
Copilot marked this conversation as resolved.
|
||
| } | ||
|
|
||
| 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; | ||
| } | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤖 💡 Code organization — The
+ 3here is the maximum width of a single emitted escape (\+ two hex digits), and4096on line 919 is the buffer size. Both are magic numbers whose relationship is implicit. Consider a namedconst int MaxEscapeWidth = 3;(and aconst int ChunkSize = 4096;) so the flush-threshold intent is self-documenting.(non-blocking maintainability nit)