-
Notifications
You must be signed in to change notification settings - Fork 4.1k
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
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
|
@DoctorKrolic, we'll probably revisit postponed PRs after wrapping up 17.11. |
@cston 17.11 P1 went to public. Does it mean that you are done with it and we can merge this PR? |
That is the just the initial preview of 17.11. We will likely be heads down until one of the much later previews. |
@cston @jaredpar |
/azp run roslyn-CI |
Azure Pipelines successfully started running 1 pipeline(s). |
src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_CollectionExpression.cs
Outdated
Show resolved
Hide resolved
Seriously? This PR is where we happen to hit this limit? 😓 |
Only in 2 CI legs though, one of which is a debug win build, while release win build and other debug builds are fine. This is either odd or suspicious |
Rerunning, we'll see if it comes up the same and go from there. It's not necessarily surprising though that debug build would include more strings than release build, due to string literals within |
This time it came up green, I'm not sure what the difference might have been, but we are probably good to go. @cston please follow up if you have any further feedback on this PR. |
Thanks @DoctorKrolic! |
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