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

Make ValueTuple members that can be made readonly readonly #90773

Closed
wants to merge 3 commits into from

Conversation

Neme12
Copy link

@Neme12 Neme12 commented Aug 17, 2023

I applied readonly to all methods except for GetHashCode and ToString, which can't be made readonly (or can be, but it would make copies of the items) because they forward to methods on the individual items that could potentially modify them.

@dotnet-issue-labeler dotnet-issue-labeler bot added the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Aug 17, 2023
@ghost ghost added the community-contribution Indicates that the PR has been added by a community member label Aug 17, 2023
@teo-tsirpanis
Copy link
Contributor

teo-tsirpanis commented Aug 17, 2023

@Neme12
Copy link
Author

Neme12 commented Aug 17, 2023

Thanks.

@neon-sunset
Copy link
Contributor

neon-sunset commented Aug 18, 2023

Doesn't this break C# tuple contract? For example

// Valid code
var tuple = (1, 2);
tuple.Item2 = 3;

@huoyaoyuan
Copy link
Member

Doesn't this break C# tuple contract? For example

Only the 0-ary ValueTuple is marked as readonly as a whole type. The members of others are still writable.

@ghost
Copy link

ghost commented Aug 18, 2023

Tagging subscribers to this area: @dotnet/area-system-runtime
See info in area-owners.md if you want to be subscribed.

Issue Details

I applied readonly to all methods except for GetHashCode and ToString, which can't be made readonly (or can be, but it would make copies of the items) because they forward to methods on the individual items that could potentially modify them.

Author: Neme12
Assignees: -
Labels:

area-System.Runtime, community-contribution, needs-area-label

Milestone: -

@jkotas jkotas removed the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Aug 18, 2023
Copy link
Member

@jozkee jozkee left a comment

Choose a reason for hiding this comment

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

Thanks for working on this.
How does GetHashCode() and ToString() create a defensive copy? sharplab.io shows identical asm.

Also, there have been other issues filed where this is being discussed. Just referencing them here.

@adamsitnik
Copy link
Member

@dotnet/fxdc does applying readonly to a type and/or method requires going through API review (removing it would cause a breaking change)?

@stephentoub @jkotas what is our current strategy for similar changes? I saw #46675 (comment)

annotate methods on non-readonly structs which do not and will never mutate the state of the instance

And I believe that most of the methods marked as readonly in this PR (Equals, indexer, GetHashCode) will never mutate. But it's not very clear to me what we would gain from it.

@stephentoub
Copy link
Member

does applying readonly to a type and/or method requires going through API review (removing it would cause a breaking change)?

We've generally preferred to do so, yes, ideally in bulk.

it's not very clear to me what we would gain from it

The main consumer gain for us marking something readonly is it can avoid a defensive copy when a value is passed via in.

Let's say you have:

(long, long, long, long) value = ...;
Foo(ref value);
...
void Foo(ref (long, long, long, long) data)
{
    data.GetHashCode();
}

The compiler doesn't need to do anything special here around preventing mutation. But if instead the code was:

(long, long, long, long) value = ...;
Foo(in value);
...
void Foo(in (long, long, long, long) data)
{
    data.GetHashCode();
}

The in is promising that the caller's value won't be mutated, which means when compiling Foo, the compiler needs to ensure that data isn't mutated, but the compiler doesn't know what GetHashCode does... it could be writing to this, in which case it would be mutating data and thus value. So if GetHashCode isn't readonly (either directly or because the struct on which it's declared is readonly), the compiler needs to make a defensive copy, and it's compiled instead as if it were:

void Foo(ref (long, long, long, long) data)
{
    (long, long, long, long) copy = data;
    copy.GetHashCode();
}

Whether that has any actual performance impact depends on a variety of factors, e.g. if the method being called is inlined, there's a good chance the JIT will eliminate the copy.

{
return Comparer<T1>.Default.Compare(Item1, other.Item1);
}

int IStructuralComparable.CompareTo(object? other, IComparer comparer)
readonly int IStructuralComparable.CompareTo(object? other, IComparer comparer)
Copy link
Member

Choose a reason for hiding this comment

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

Putting readonly on explicit interface implementations isn't going to buy you anything. These can only be accessed via interface references and those don't have defensive copy semantics.

I'm actually a bit surprised we allow this syntax.

@tannergooding
Copy link
Member

The main consumer gain for us marking something readonly is it can avoid a defensive copy when a value is passed via in

The other notable case is static readonly (int, int) s_value = ...; and then doing s_value.Method() or similar.


That being said, this is all generic code and we're just trading one copy for another copy here.

That is, today, if you have readonly (int, int) value = ... and do say value.GetHashCode() you get a copy of the full value and then GetHashCode() is called on that copy.

If you instead mark GetHashCode() as readonly, then it cannot statically determine that Item1.GetHashCode() won't mutate and Roslyn will insert independent copies of both Item1 and Item2 before calling their own respective GetHashCode() methods.

In the case where all types are unmanaged, the JIT can optimize the former copy to a block copy, while it may needs to preserve the per item copies in the latter case. Which is better is ultimately scenario dependent, but I don't think this is something we can really "fix", since we can't actually remove the copies without unsafe code.

Copy link
Member

@adamsitnik adamsitnik left a comment

Choose a reason for hiding this comment

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

does applying readonly to a type and/or method requires going through API review (removing it would cause a breaking change)?

We've generally preferred to do so, yes, ideally in bulk.

In such a case I am going to convert this PR to a draft and create a new API proposal based on the ref file changes.

@adamsitnik adamsitnik marked this pull request as draft November 3, 2023 17:15
@huoyaoyuan
Copy link
Member

Historically, #44640 and #44629 didn't go through API review.
This one should be really trivial though.

@ghost ghost closed this Dec 7, 2023
@ghost
Copy link

ghost commented Dec 7, 2023

Draft Pull Request was automatically closed for 30 days of inactivity. Please let us know if you'd like to reopen it.

@terrajobst terrajobst reopened this Dec 7, 2023
@terrajobst
Copy link
Member

Before we close this, is this intended to still be one?

From an API review standpoint, this affects public surface area and should normally go through API review but my care level is relatively low. Unless anyone on @dotnet/fxdc objects, I'm OK with letting these kind of changes be handled by PR review alone.

@ghost ghost closed this Jan 6, 2024
@ghost
Copy link

ghost commented Jan 6, 2024

Draft Pull Request was automatically closed for 30 days of inactivity. Please let us know if you'd like to reopen it.

@github-actions github-actions bot locked and limited conversation to collaborators Feb 6, 2024
This pull request was closed.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-System.Runtime community-contribution Indicates that the PR has been added by a community member
Projects
None yet
Development

Successfully merging this pull request may close these issues.