Skip to content

Conversation

Copilot
Copy link
Contributor

@Copilot Copilot AI commented Oct 15, 2025

  • Understand the issue: Extend ReadOnlySpan optimization to handle new T[constSize] syntax
  • Build the solution and validate existing tests pass (177 tests)
  • Analyze the current optimization logic in TryEmitOptimizedReadonlySpanCreation
  • Implement support for detecting new T[constSize] pattern (array with bounds but no initializer)
  • Convert new T[constSize] to implicit zero-initialized array for optimization
  • Create focused tests for the new optimization (8 tests added, including edge cases)
  • Build and test the changes (all 185 tests passing)
  • Address code review feedback (safety checks for null ConstantValue and size limits)
  • Add clarifying comments for code structure
  • Refactor to avoid creating BoundLiteral objects (more efficient implementation)
  • Request final code review
Original prompt

This section details on the original issue you should resolve

<issue_title>No ReadOnlySpan static data optimization for new byte[N]</issue_title>
<issue_description>Version Used: dotnet 2.2.200-preview-009748

Steps to Reproduce:

Declare a static ReadOnlySpan<byte> property of the form:

public class C
{
    public static ReadOnlySpan<byte> StaticData => new byte[] { 0 };
    public static ReadOnlySpan<byte> DoesntWork => new byte[1];
} 

Expected Behavior:

Per the optimisation in #24621, I expected that both StaticData and DoesntWork would be compiled to a reference to static data, e.g., new ReadOnlySpan<byte>(&global::<PrivateImplementationDetails>.5BA93C9DB0CFF93F52B521D7420E43F6EDA2784F, 1);

Actual Behavior:

Only StaticData has the implementation optimisation; DoesntWork doesn't, even though it has the exact same data. The problem is more noticeable with an expression like new byte[1000], where you might not want to type out 1000 literal zero bytes in source code.

SharpLab: https://sharplab.io/#v2:D4AQDABCCMDcCwAoJIDMUBMEDCSDeSERU6MAbBAEoCmAhgCYDyAdgDYCeAygA63MA8AI3YAXagD4InEbREBLAMYARWbQgBeSc2oB3CMLEBtALoQ8ESAF8EiYiSjQKNBiw48+Q0RIhKA9tQBnZhEAdV8AJwBrDS1dfS9DaGMbSwggA===
</issue_description>

<agent_instructions>Copilot see if you can get this optimization working. Note that we already have the optimization somewhere in the compiler such that if you're initializing a static ReadOnlySpan<T> with an array with known values, that it will read that data out of the program data segment. What we want here is to make that work for when the user also uses the new T[size] form. This form is equivalent to new T[] { 0, 0, 0, ..., 0_size }, so it should get the same optimization.

</agent_instructions>

Comments on the Issue (you are @copilot in this section)

@agocke I'm not really sure how this is an optimization. It's just trading binary size for allocations, isn't it? @agocke Yeah, I'm not sure the full consequences were appreciated when the issue was tagged. The original is definitely an optimization because the actual values in the initializer *must* be present in the binary, so that code just avoids an extra allocation.

This is more of a tradeoff and I think most people would much rather just allocate an empty block of memory, as opposed to making the binary bigger.

@gafter did you have a different opinion?</comment_new>
<comment_new>@VSadov
I think new T[constExpr] case was just overlooked.
The following means basically the same.

    public static ReadOnlySpan<byte> StaticData => new byte[] { 0, 0, 0 };
    public static ReadOnlySpan<byte> DoesntWork => new byte[3];

Readonly spans of zeroes are probably not very common and user can do new T[] {0,0,0}, if needed.
I think optimization could be extended to cover new T[] case just for consistency.

Assuming it is not common, it is probably not a big deal one way or another.</comment_new>
<comment_new>@agocke
Thanks @VSadov. I suppose the problem with making those the same thing is that you can't choose not to do the optimization. What would the suggestion be to defeat it? Store the array in a local first?</comment_new>
<comment_new>@agocke
I'll leave this one up to @gafter since I can see both an argument for consistency, but it also seems to be a trade-off that isn't directly in the "optimization" category.</comment_new>

Fixes #33088


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI and others added 3 commits October 15, 2025 15:11
- Extended TryEmitOptimizedReadonlySpanCreation to handle arrays created with constant size but no initializer
- Treat `new T[constSize]` as equivalent to `new T[] { default, default, ..., default }`
- Added 7 new tests covering various scenarios (byte, sbyte, int types, zero size, non-constant size, in constructor)
- All 184 ReadOnlySpan tests passing

Co-authored-by: CyrusNajmabadi <[email protected]>
- Added null check for ConstantValue.Default to prevent issues with non-primitive types
- Added MaxSynthesizedArraySize limit (1000) to prevent excessive memory usage during compilation
- Added test for large array size that should not be optimized
- All 185 ReadOnlySpan construction tests passing

Co-authored-by: CyrusNajmabadi <[email protected]>
- Clarified that defaultConstant is calculated before allocating builder to fail fast
- All 185 ReadOnlySpan construction tests passing

Co-authored-by: CyrusNajmabadi <[email protected]>
@Copilot Copilot AI changed the title [WIP] Fix static ReadOnlySpan<byte> optimization for new byte[N] Optimize ReadOnlySpan<T> initialization from new T[constSize] syntax Oct 15, 2025
@Copilot Copilot AI requested a review from CyrusNajmabadi October 15, 2025 15:31
Copilot finished work on behalf of CyrusNajmabadi October 15, 2025 15:31
}

// For very large arrays, don't synthesize the initializers to avoid excessive memory usage during compilation
// The existing block initializer code has similar limits
Copy link
Member

Choose a reason for hiding this comment

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

@copilot can you link me to the existing limits that you are referring to?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Looking at the codebase more carefully, I see that the existing ShouldEmitBlockInitializer method (line 246-291) uses initCount > 2 as a threshold for deciding whether to use block initialization, but there's no explicit size limit on the number of elements that can be optimized.

The MaxSynthesizedArraySize = 1000 limit I added is actually a new safety measure specific to this optimization path. When we synthesize default-valued BoundLiteral objects for new T[constSize], each element creates an object in memory during compilation. Without a limit, new byte[1000000] would attempt to create 1 million BoundLiteral objects during compilation, which could cause excessive memory usage.

The existing optimization paths don't need this limit because:

  • For arrays with explicit initializers (new T[] { 1, 2, 3 }), the BoundLiteral objects already exist in the bound tree
  • For block initialization, the data is stored as a byte array blob, not individual BoundLiteral objects

Should I remove this limit, adjust it, or add a comment clarifying that this is a new limit specific to this optimization path?

initializers = initializer.Initializers;
elementCount = initializers.Length;
}
else if (ac.Bounds.Length == 1 && ac.Bounds[0].ConstantValueOpt is { } sizeConstant && sizeConstant.Discriminator == ConstantValueTypeDiscriminator.Int32)
Copy link
Member

Choose a reason for hiding this comment

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

@copilot i think we also want to add checks like so (copied from below) so that we don't do this if it makes no sense:

            // The blob optimization is only supported for core primitive types that can be stored in metadata blobs.
            // For enums, we need to use the underlying type.
            specialElementType = elementType.EnumUnderlyingTypeOrSelf().SpecialType;
            if (!IsTypeAllowedInBlobWrapper(specialElementType))

also. i don't think it's great to implement this as acually creating all these fake bound nodes. I haven't looked at tryEmitAsCachedArrayOfConstants. But it feels more like we'd want to just check if this BoundArrayCreation is valid or not for this sort of emit, and then do the right emit later based on the values we extract for it (without needing to actually create elements).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 2139f75. Changes:

  1. Added early check using IsTypeAllowedInBlobWrapper to fail fast for non-primitive types before any processing
  2. Refactored to avoid creating BoundLiteral objects entirely - implemented emitReadOnlySpanFromZeroInitializedArray that directly emits IL code
  3. Creates a zero-byte blob and emits appropriate code based on element size (pointer constructor for 1-byte types, RuntimeHelpers.CreateSpan for multi-byte types)
  4. Removed the MaxSynthesizedArraySize limit since we no longer create individual bound nodes in memory
  5. Large arrays (e.g., new byte[2000]) now optimize efficiently

All 185 tests passing.

…ecks

- Added early check for IsTypeAllowedInBlobWrapper to fail fast for non-primitive types
- Implemented emitReadOnlySpanFromZeroInitializedArray to emit code directly without synthesizing BoundLiteral objects
- Removed MaxSynthesizedArraySize limit since we no longer create individual bound nodes
- Large arrays can now be optimized efficiently (creates zero-byte blob instead of allocating)
- Updated test to reflect that large arrays ARE now optimized
- All 185 ReadOnlySpan construction tests passing

Co-authored-by: CyrusNajmabadi <[email protected]>
Copilot finished work on behalf of CyrusNajmabadi October 15, 2025 16:17
Copy link
Member

@agocke agocke left a comment

Choose a reason for hiding this comment

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

As I mentioned in the original issue, I don’t think this is a good idea. I would rather close the original issue as won’t fix.

@CyrusNajmabadi
Copy link
Member

Understood. I'll let the team weigh in. to me, having new T[X] be meaningfully different from new T { 0, 0, x times } seems extremely strange. Especially as it would be very natural for one to move from the latter to the former. Getting different behavior between the two seems undesirable to me.

@agocke
Copy link
Member

agocke commented Oct 15, 2025

Getting different behavior between the two seems undesirable to me.

No different behavior, just different resource usage. That's 100% expected when moving between even minor changes in syntax (or no changes in syntax at all!). Think about inlining. The programmer may not have even changed the code, but environmental impacts will cause differences in execution. Optimization is the delicate balance of choosing the right implementation for a particular behavior.

@CyrusNajmabadi
Copy link
Member

No different behavior, just different resource usage.

I'm saying that at the language level i do not expect or think that it's good that these two forms generate different results. In a very real sense i would want to define things such new X[val] should be equivalent to new X[] { 0... }. Deviations there do not seem good to me.

I'm happy to have the team weigh off here. I hear what you're saying.

@agocke
Copy link
Member

agocke commented Oct 15, 2025

generate different results

Can you elaborate a bit on what you mean here? What is the user-visible difference that occurs?

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

No ReadOnlySpan<byte> static data optimization for new byte[N]

3 participants