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
Optimize codegen for collections expression of single spread of ReadOnlySpan
for collection builder emit strategy
#73102
base: main
Are you sure you want to change the base?
Conversation
…OnlySpan` in case of collection builder emit strategy
ReadOnlySpan
in case of collection builder emit strategyReadOnlySpan
Can you state explicitly under what circumstances this optimization will apply? For example I want to make sure it does not apply to:
Thanks! |
It does apply to this case, making it effectively a reassignment. Why it is bad? The ROS is an immutable type and the previous codegen didn't perform deep copy of elements anyway, so this in unobservable to the user (equalify change doesn't count since it isn't guaranteed for collection expressions). Am I worng? |
Ros is not an immutable type. It's an immutable view over potentially mutable data. Trivial example, wrap a normal array with a ros. Then assign to two other ROSs (one as a normal assignment, one as a spread copy). Now change the original array. What do you see? |
Afaict, this is only safe if passed to a collection builder Create method that is scoped. That way we know the new collection must be making a copy itself. Right @RikkiGibson? |
It's incorrect to optimize
I'm not following the scenario you have in mind. If a Create method is being used to create the resulting collection, then it's not the I can't think of any case where we can safely "skip" copying when user code includes a spread. It would have to involve proving that the spread operand and its referents is not referenced by anything else, and treating it like a "move" of the storage instead of a copy. It's not a type of optimization we really do in Roslyn. Especially for a case where, if user wants those semantics, they could just replace |
Looking at the code a bit more. Yeah, I see that In the case of And yes, to avoid a behavioral breaking change you need to take care to still do the copy for |
Here's teh scenario: ReadOnlySpan<int> x = ... backing mutable store ...;
ImmutableArray<int> y = [.. x]; This should be safe (IMO, but correct me if i'm wrong), but it should be possible to compile this to: ImmutableArray<int> y = ImmutableArray.Create<int>(x); Instead of: ReadOnlySpan<int> __compilerCopy = ... copy x ...;
ImmutableArray<int> y = ImmutableArray.Create<int>(__compilerCopy); Basically, if the ROS arg is scoped, we know that the final collection can't keep a reference to it. So it will make a copy itself, so we don't need to. (I could def be wrong on this). |
I agree with your assessment. |
Interestingly enough, ImmutableArray.Create doesn't seem to have a scoped argument: I'm curious why that is. @stephentoub any insights here? |
You only ever need |
In the case of IA, we'd either need to special case. Or see if the signature of it could be updated. And if there's a good reason the sig is as it is, and the non-copy is not-safe, we likely should not special case this type. :) |
@DoctorKrolic as far as how to adjust the implementation. I would recommend to do a check in
In which case, don't call into |
Sorry for the spam. It can probably be generalized a little further than this, e.g. if |
@RikkiGibson Can you clarify this part please:
I am generally not that much familiar with |
PErfect. That makes sense. Thanks! |
@stephentoub Ignore the ping :) |
I would like to focus on the original case here with ROS only to keep the size of the PR small. My expirience says that making several "simple" optimizations might result in exponential explosion of test cases, which need to be checked) |
Sure. Say you have the following: [CollectionBuilder(typeof(MyRefType), "Create")]
ref struct MyRefType<T>
{
// ...
}
static class MyRefType
{
public static MyRefType<T> Create<T>(ReadOnlySpan<T> values); // versus
public static MyRefType<T> Create<T>(scoped ReadOnlySpan<T> values);
} In the case of: ReadOnlySpan<int> s = ... create ...;
MyRefType<int> t = [.. s]; Because the first MyRefType.Create overload returns a ref-struct, and is not scoped, it might capture the ROS being passed in, and thus would see the underlying values of it change if it wrapped something like a mutable array. So, to be safe, we must make a copy. However, the arg to the second 'Create' overload is 'scoped'. This informs the compiler taht it could not be captured by the return value of create. And as such, we don't need to make a copy. As rikki points out, this is only needed when returning a ref-struct, as a non-ref-struct could not ever capture the ROS coming in, so it would have to be making a copy to function at all. |
AS an fuller example of that: [CollectionBuilder(typeof(MyRefType), "Create")]
ref struct HalfSpan<T>
{
private readonly ReadOnlySpan<T> _ros;
public int Length => _ros.Length / 2;
public T this[int index] => _ros[index * 2];
}
static class MyRefType
{
public static MyRefType<T> Create<T>(ReadOnlySpan<T> values) => new HalfSpan<T>(values);
}
// later
string[] values = ["a", "b", "c", "d";
HalfSpan<string> s = [.. values];
// This change must not be visible in 's'. `.. values` promises you get your own 'copy' of hte values.
values[0] = "A"; values[1] = "B"; values[2] = "C"; values[3] = "D"; |
As far as how to correctly implement the ref safety criteria. I think the following will do (in a code path where we are already narrowed down to the
|
ReadOnlySpan
ReadOnlySpan
for collection builder emit strategy
@CyrusNajmabadi @RikkiGibson I think this is conceptually ready, now would like a review of implementation code please |
I would welcome a follow-up PR which also handles cases where the spread operand is implicitly convertible to |
@dotnet/roslyn-compiler for a second review of a smallish community PR |
src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_CollectionExpression.cs
Outdated
Show resolved
Hide resolved
src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_CollectionExpression.cs
Outdated
Show resolved
Hide resolved
src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_CollectionExpression.cs
Outdated
Show resolved
Hide resolved
@cston PTAL |
src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_CollectionExpression.cs
Outdated
Show resolved
Hide resolved
Thanks @DoctorKrolic, the change looks good, but we'll hold off merging while C#13 feature work is in progress. The reason is we've spent significant time recently addressing unintended breaking changes which has taken time away from C#13. To reduce overhead and risk of regressions while we focus on C#13, we’ll limit non-essential changes for now. We'll revisit postponed PRs as we finish up feature work. Since this change is an optimization, and not driven by a reported issue, we’ll hold off merging and revisit later. I've marked the issue as Blocked to prevent merging in the meantime. Thanks for your patience. |
@cston When the feature in question is expected to be done? I have a few optimizing PR ideas, but I don't want to introduce even more overhead to the team while you are working on a feature |
@DoctorKrolic, we'll probably revisit postponed PRs after wrapping up 17.11. |
Implements suggestion from #71296 (comment). Now when collection expression if of form
[.. readOnlySpan]
we can just take thatreadOnlySpan
instead of copying it since we know that it is an immutable type