Skip to content
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

Use a simple temp instead of InlineArray1 #73086

Merged
merged 10 commits into from
May 8, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,23 @@ private static bool ShouldUseInlineArray(BoundCollectionExpressionBase node, CSh
Debug.Assert(_compilation.Assembly.RuntimeSupportsInlineArrayTypes);

int arrayLength = elements.Length;
if (arrayLength == 1
&& _factory.WellKnownMember(asReadOnlySpan
? WellKnownMember.System_ReadOnlySpan_T__ctor_ref_readonly_T
: WellKnownMember.System_Span_T__ctor_ref_T, isOptional: true) is MethodSymbol spanRefConstructor)
{
// Special case: no need to create an InlineArray1 type. Just use a temp of the element type.
var spanType = _factory
.WellKnownType(asReadOnlySpan ? WellKnownType.System_ReadOnlySpan_T : WellKnownType.System_Span_T)
.Construct([elementType]);
var constructor = spanRefConstructor.AsMember(spanType);
var element = VisitExpression((BoundExpression)elements[0]);
var temp = _factory.StoreToTemp(element, out var assignment, isKnownToReferToTempIfReferenceType: true);
RikkiGibson marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 25, 2024

Choose a reason for hiding this comment

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

isKnownToReferToTempIfReferenceType

  1. In general, when not sure, the safest value for this parameter is false. It enables some optimizations and you don't want to apply those when they are not appropriate because that can lead to an incorrect result, i.e. bugs.
  2. This value is meaningful only for ref locals. I would avoid setting it to true for non-ref locals.
  3. The meaning of this flag - this local refers to something (it is a ref), and if the type of that something is a reference type, there is a guarantee that that something is a temporary (not a user local, not a parameter, not a field, etc.) So, it is about what we are referring to with this local. Obviously the temp we are assigning to here is a temp, but this value is not about that.

So, I recommend, cleaning up this code and any other "collection expressions" code with respect to values used for this parameter. In order to avoid spreading questionable code through copy/paste and eventually hitting a scenario that can actually break because we haven't given much thought about the value. #Closed

locals.Add(temp.LocalSymbol);
var call = _factory.New(constructor, arguments: [temp], argumentRefKinds: [asReadOnlySpan ? RefKind.In : RefKind.Ref]);
RikkiGibson marked this conversation as resolved.
Show resolved Hide resolved
return _factory.Sequence([assignment], call);
}

var inlineArrayType = _factory.ModuleBuilderOpt.EnsureInlineArrayTypeExists(syntax, _factory, arrayLength, _diagnostics.DiagnosticBag).Construct(ImmutableArray.Create(elementType));
Debug.Assert(inlineArrayType.HasInlineArrayAttribute(out int inlineArrayLength) && inlineArrayLength == arrayLength);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -789,6 +789,21 @@ public BoundObjectCreationExpression New(NamedTypeSymbol type, ImmutableArray<Bo
public BoundObjectCreationExpression New(MethodSymbol ctor, ImmutableArray<BoundExpression> args)
=> new BoundObjectCreationExpression(Syntax, ctor, args) { WasCompilerGenerated = true };

public BoundObjectCreationExpression New(MethodSymbol constructor, ImmutableArray<BoundExpression> arguments, ImmutableArray<RefKind> argumentRefKinds)
=> new BoundObjectCreationExpression(
Syntax,
constructor,
arguments,
argumentNamesOpt: default,
argumentRefKinds,
expanded: false,
argsToParamsOpt: default,
defaultArguments: default,
constantValueOpt: null,
initializerExpressionOpt: null,
constructor.ContainingType)
{ WasCompilerGenerated = true };

public BoundObjectCreationExpression New(WellKnownMember wm, ImmutableArray<BoundExpression> args)
{
var ctor = WellKnownMethod(wm);
Expand Down
1,613 changes: 788 additions & 825 deletions src/Compilers/CSharp/Test/Emit2/Semantics/CollectionExpressionTests.cs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -4558,9 +4558,7 @@ static void Test(params System.Span<int> a)

CompileAndVerify(
comp,
verify: ExecutionConditionUtil.IsMonoOrCoreClr ?
Verification.FailsILVerify with { ILVerifyMessage = "[InlineArrayAsSpan]: Return type is ByRef, TypedReference, ArgHandle, or ArgIterator. { Offset = 0xc }" }
: Verification.Skipped,
verify: Verification.Skipped,
Copy link
Member

Choose a reason for hiding this comment

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

Curious now. Lets say I have the following code:

M(1);
M(2);
void M(params Span<int> i) { } 

After this change the compiler will optimize the calling code to roughly be:

int tmp = 1;
M(new Span<int>(ref temp));

But do we create 1 or 2 temps here? Guessing we only create one today but want to check. This could be a significant optimization opportunity for us in the future.

expectedOutput: ExpectedOutput(@"
int
int")).VerifyDiagnostics();
Expand Down Expand Up @@ -4598,9 +4596,7 @@ class C3 : C2 {}

CompileAndVerify(
comp,
verify: ExecutionConditionUtil.IsMonoOrCoreClr ?
Verification.FailsILVerify with { ILVerifyMessage = "[InlineArrayAsSpan]: Return type is ByRef, TypedReference, ArgHandle, or ArgIterator. { Offset = 0xc }" }
: Verification.Skipped,
verify: Verification.Skipped,
expectedOutput: ExpectedOutput(@"
C2
C2")).VerifyDiagnostics();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1071,6 +1071,8 @@ public void AllWellKnownTypeMembers()
case WellKnownMember.System_Span_T__CopyTo_Span_T:
case WellKnownMember.System_ReadOnlySpan_T__CopyTo_Span_T:
case WellKnownMember.System_Collections_Immutable_ImmutableArray_T__AsSpan:
case WellKnownMember.System_Span_T__ctor_ref_T:
case WellKnownMember.System_ReadOnlySpan_T__ctor_ref_readonly_T:
// Not always available.
continue;
}
Expand Down
2 changes: 2 additions & 0 deletions src/Compilers/Core/Portable/WellKnownMember.cs
Original file line number Diff line number Diff line change
Expand Up @@ -479,13 +479,15 @@ internal enum WellKnownMember

System_Span_T__ctor_Pointer,
System_Span_T__ctor_Array,
System_Span_T__ctor_ref_T,
System_Span_T__get_Item,
System_Span_T__get_Length,
System_Span_T__Slice_Int_Int,

System_ReadOnlySpan_T__ctor_Pointer,
System_ReadOnlySpan_T__ctor_Array,
System_ReadOnlySpan_T__ctor_Array_Start_Length,
System_ReadOnlySpan_T__ctor_ref_readonly_T,
System_ReadOnlySpan_T__get_Item,
System_ReadOnlySpan_T__get_Length,
System_ReadOnlySpan_T__Slice_Int_Int,
Expand Down
18 changes: 18 additions & 0 deletions src/Compilers/Core/Portable/WellKnownMembers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3333,6 +3333,14 @@ static WellKnownMembers()
1, // Method Signature
(byte)SignatureTypeCode.TypeHandle, (byte)SpecialType.System_Void, // Return Type
(byte)SignatureTypeCode.SZArray, (byte)SignatureTypeCode.GenericTypeParameter, 0,

// System_Span_T__ctor_ref_T
(byte)(MemberFlags.Constructor), // Flags
(byte)WellKnownType.ExtSentinel, (byte)(WellKnownType.System_Span_T - WellKnownType.ExtSentinel), // DeclaringTypeId
0, // Arity
1, // Method Signature
(byte)SignatureTypeCode.TypeHandle, (byte)SpecialType.System_Void, // Return Type
(byte)SignatureTypeCode.ByReference, (byte)SignatureTypeCode.GenericTypeParameter, 0,

// System_Span_T__get_Item
(byte)(MemberFlags.PropertyGet), // Flags
Expand Down Expand Up @@ -3388,6 +3396,14 @@ static WellKnownMembers()
(byte)SignatureTypeCode.TypeHandle, (byte)SpecialType.System_Int32,
(byte)SignatureTypeCode.TypeHandle, (byte)SpecialType.System_Int32,

// System_ReadOnlySpan_T__ctor_ref_readonly_T
(byte)(MemberFlags.Constructor), // Flags
(byte)WellKnownType.ExtSentinel, (byte)(WellKnownType.System_ReadOnlySpan_T - WellKnownType.ExtSentinel), // DeclaringTypeId
0, // Arity
1, // Method Signature
(byte)SignatureTypeCode.TypeHandle, (byte)SpecialType.System_Void, // Return Type
(byte)SignatureTypeCode.ByReference, (byte)SignatureTypeCode.GenericTypeParameter, 0,
Copy link
Member

Choose a reason for hiding this comment

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

The signature of the ReadOnlySpan consructor changed between net7.0 and net8.0 from in to ref readonly. Will this match both versions?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In practice I expect it will. This is because the comparison here is just checking that it is a managed reference according to the runtime. It doesn't check anything about the readability or writability of the reference (C#-level concepts).

However for the particular way this optimization is implemented, I think won't even enter the path for using the 'Span referencing a temp' optimization for net7, because the containing code path is gated on InlineArray runtime feature availability, IIRC. This is shown in the codegen for the tests which use target framework net7.

Copy link
Member

Choose a reason for hiding this comment

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

Yep. I'm less interested in the optimization being applied and more interested in making sure that this well known member can see both variations of the signature. Otherwise it could create confusion for later changes.


// System_ReadOnlySpan_T__get_Item
(byte)(MemberFlags.PropertyGet), // Flags
(byte)WellKnownType.ExtSentinel, (byte)(WellKnownType.System_ReadOnlySpan_T - WellKnownType.ExtSentinel), // DeclaringTypeId
Expand Down Expand Up @@ -4722,12 +4738,14 @@ static WellKnownMembers()
".ctor", // System_Runtime_CompilerServices_ObsoleteAttribute__ctor
".ctor", // System_Span_T__ctor_Pointer
".ctor", // System_Span_T__ctor_Array
".ctor", // System_Span_T__ctor_ref_T
"get_Item", // System_Span_T__get_Item
"get_Length", // System_Span_T__get_Length
"Slice", // System_Span_T__Slice_Int_Int
".ctor", // System_ReadOnlySpan_T__ctor_Pointer
".ctor", // System_ReadOnlySpan_T__ctor_Array
".ctor", // System_ReadOnlySpan_T__ctor_Array_Start_Length
".ctor", // System_ReadOnlySpan_T__ctor_ref_readonly_T
"get_Item", // System_ReadOnlySpan_T__get_Item
"get_Length", // System_ReadOnlySpan_T__get_Length
"Slice", // System_ReadOnlySpan_T__Slice_Int_Int
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -821,7 +821,9 @@ End Namespace
WellKnownMember.System_ReadOnlySpan_T__ToArray,
WellKnownMember.System_Span_T__CopyTo_Span_T,
WellKnownMember.System_ReadOnlySpan_T__CopyTo_Span_T,
WellKnownMember.System_Collections_Immutable_ImmutableArray_T__AsSpan
WellKnownMember.System_Collections_Immutable_ImmutableArray_T__AsSpan,
WellKnownMember.System_Span_T__ctor_ref_T,
WellKnownMember.System_ReadOnlySpan_T__ctor_ref_readonly_T
' Not always available.
Continue For
End Select
Expand Down Expand Up @@ -1026,7 +1028,9 @@ End Namespace
WellKnownMember.System_ReadOnlySpan_T__ToArray,
WellKnownMember.System_Span_T__CopyTo_Span_T,
WellKnownMember.System_ReadOnlySpan_T__CopyTo_Span_T,
WellKnownMember.System_Collections_Immutable_ImmutableArray_T__AsSpan
WellKnownMember.System_Collections_Immutable_ImmutableArray_T__AsSpan,
WellKnownMember.System_Span_T__ctor_ref_T,
WellKnownMember.System_ReadOnlySpan_T__ctor_ref_readonly_T
' Not always available.
Continue For
End Select
Expand Down