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

Open
wants to merge 10 commits into
base: main
Choose a base branch
from

Conversation

RikkiGibson
Copy link
Contributor

Fixes #72566

@dotnet-issue-labeler dotnet-issue-labeler bot added Area-Compilers untriaged Issues and PRs which have not yet been triaged by a lead labels Apr 18, 2024
@RikkiGibson RikkiGibson marked this pull request as ready for review April 18, 2024 21:23
@RikkiGibson RikkiGibson requested a review from a team as a code owner April 18, 2024 21:23
.Construct([elementType]);
var constructor = spanRefConstructor.AsMember(spanType);
var element = VisitExpression((BoundExpression)elements[0]);
var temp = _factory.StoreToTemp(element, out var assignment);
Copy link
Member

Choose a reason for hiding this comment

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

What is the re-use policies on these temps? Basically is there any chance that the temp slot allocated here will be re-used or is it considered a temp that lives for the lifetime of the current method?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think the local slot can be reused outside the containing block, but that's fine, since the span value that references the temp can't escape outside the containing block.

Copy link
Member

Choose a reason for hiding this comment

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

Hmm. Some of our temp are statement level. I agree block level temp is fine but we should be sure which this is. Had other bugs with statement temps being reused in span before

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added a test. It doesn't look like the temp is reused.

Copy link
Member

Choose a reason for hiding this comment

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

Not sure the test is sufficient here. Looking at the code it seems that it's a re-usable temp. The default kind of the temp is SynthesizedLocalKind.LoweringTemp and that is not a long lived temp. The doc mentions these cannot live across a statement boundary

    /// 1) Short-lived (temporary)
    ///    The lifespan of a temporary variable shall not cross a statement boundary (a PDB sequence point).
    ///    These variables are not tracked by EnC and don't have names. Only values less than 0 are considered
    ///    short-lived: new short-lived kinds should have a negative value.

Copy link
Contributor

Choose a reason for hiding this comment

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

What is the re-use policies on these temps?

Slots can be reused for locals that go out of scope. The scope is defined by blocks and sequences that list locals they own. If I remember correctly, sometimes scope of locals is extended by codegen, when, for example, a sequence returns a ref to a local that it owns. One might say the bound tree violates scoping rules in such cases, but for whatever reason a decision was made to handle the case instead of enforcing correctness of the tree.

That said, symbols for temps are never reused by default. One might, of course, intentionally create a bound tree that shares the same temp for different purposes.

I hope that helps with concerns that prompted the original question.

Copy link
Contributor

Choose a reason for hiding this comment

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

Regarding the short-lived/long-lived story. According to my understanding, these are mostly about PDB/ENC, and the statement boundary the comment is talking about is in terms of syntax (perhaps talking in terms of sequence point boundaries would be more accurate), not about bound statement nodes. Reuse of slots in IL is not based on that. It is based on what I said in the previous message on this thread. So as long as the local is added to the right BoundBlock/BoundSequence, it will not be reused while code for that node is emitted.

There is, however, something interesting going on with where we put inline array locals. The line locals.Add(inlineArrayLocal.LocalSymbol); below. According to comments on _additionalLocals field, the local is going to end up on the enclosing method outermost block. Hopefully that is not going to mess with EnC too much because effectively the local crosses a sequence point boundary. "Collection expressions" devs might want to take a close look at this.

Copy link
Member

Choose a reason for hiding this comment

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

Great. Thansk for the explanation!

Copy link
Member

Choose a reason for hiding this comment

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

There is, however, something interesting going on with where we put inline array locals. ...

Logged #73246 based on this comment and offline discussion.

@cston

This comment was marked as resolved.

@cston

This comment was marked as resolved.

@RikkiGibson RikkiGibson requested a review from a team April 22, 2024 19:28
@RikkiGibson
Copy link
Contributor Author

@dotnet/roslyn-compiler for a second review

Console.Write(y[0]);
}
{
Span<int> y = [x];
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we have a test where we create single-element collection twice in the same block (not different blocks like in this test)?

Copy link
Member

Choose a reason for hiding this comment

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

Indeed this is the type of test that I was most worried about. Re-using the temp between two different blocks is just fine. Re-using it within the same block would be a significant codegen error. Want to make sure that we're not doing that.

@cston

This comment was marked as resolved.

.Construct([elementType]);
var constructor = spanRefConstructor.AsMember(spanType);
var element = VisitExpression((BoundExpression)elements[0]);
var temp = _factory.StoreToTemp(element, out var assignment, isKnownToReferToTempIfReferenceType: true);
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

@AlekseyTs

This comment was marked as resolved.

Console.Write(y[0]);
}
{
Span<int> y = [x];
Copy link
Member

Choose a reason for hiding this comment

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

Indeed this is the type of test that I was most worried about. Re-using the temp between two different blocks is just fine. Re-using it within the same block would be a significant codegen error. Want to make sure that we're not doing that.

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.

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.

@RikkiGibson
Copy link
Contributor Author

@dotnet/roslyn-compiler for second review

@@ -359,6 +359,77 @@ void assertAttributeData(string name)
}
}

[Fact]
public void Span_SingleElement_TempsAreNotReused()
Copy link
Member

Choose a reason for hiding this comment

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

Did we file a bug to follow up on this? This is a place where future optimizations could very much benefit.

In this particular case the win is pretty minor. It's just an extra int or two on the stack. Consider though when we're passing instead 3 parameters and storing lots of redundant inline array temporaries. That can start to make the stack frame size increase be noticable and may deserve an optimization pass to do proper reuse.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@RikkiGibson
Copy link
Contributor Author

@dotnet/roslyn-compiler for second review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-Compilers untriaged Issues and PRs which have not yet been triaged by a lead
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Collection expression IL could be simplified for single span element
5 participants