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 System.Guid readonly #1809

Closed
wants to merge 2 commits into from
Closed

Conversation

Gnbrkm41
Copy link
Contributor

@Gnbrkm41 Gnbrkm41 commented Jan 16, 2020

Related: #1718

This PR makes System.Guid as readonly struct. While none of the APIs mutate the instances, implementations of GUID parsing, copy and comparisons involve taking refs of those fields and assigning new values, which is not allowed by C# with readonly fields.

To work around these with minimal code changes, Unsafe.AsRef is used whenever we need to pass something as ref. These were really just for bitwise-comparison or for MemoryMarshal.TryWrite.
I'm honestly surprised by the fact that MemoryMarshal.TryWrite takes ref T instead of in T as it does not mutate the passed reference.

For the parsing implementation, rather large portions of it were mutating the guid; I ended up type-punning by creating a structurally identical struct then Unsafe.Asing it.

Not sure if API review is required for this? All the public members should not mutate the instance already anyway (and would make this a mutable struct if any of them are, which is usually a bad idea)

@tannergooding
Copy link
Member

Is the new code really better than marking just the non-mutating methods as readonly?

Could we get a list of which instance methods mutate vs which do not?

private readonly byte _h; // Do not rename (binary serialization)
private readonly byte _i; // Do not rename (binary serialization)
private readonly byte _j; // Do not rename (binary serialization)
private readonly byte _k; // Do not rename (binary serialization)
Copy link
Member

Choose a reason for hiding this comment

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

Is marking the fields as readonly going to break binary serialization?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just did a simple experiment of creating a struct with a non-readonly field, serialising it to a file, marking the field and the struct as readonly and deserialising it; It appears to work fine without exceptions. Though, I haven't really used binary serialisation so I could be wrong.

Copy link
Member

Choose a reason for hiding this comment

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

Please verify the reverse as well since that's also supported (although, if ->readonly works, surely <-readonly does too)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The opposite seems to be true as well.

@Gnbrkm41
Copy link
Contributor Author

Is the new code really better than marking just the non-mutating methods as readonly?

I'm not sure. None of the methods are documented to mutate the structure, and having every methods marked as readonly yet not having the type itself marked readonly because of its private implementation feels weird to me.

Could we get a list of which instance methods mutate vs which do not?

There actually is no instance methods that mutate. Only the ctors/static methods related with the creation of new Guid Instances actually mutate as part of the generation.

  • Guid.NewGuid (Only on Unix)

TryParse/Parse, new Guid(string) and new Guid(ReadOnlyChar<char>) use these private methods that assign new values to fields:

  • TryParseExactD
  • TryParseExactN

@jkotas
Copy link
Member

jkotas commented Jan 16, 2020

What would it take to refactor this cleanly, without introducing the MutableGuid type?

@jkotas
Copy link
Member

jkotas commented Jan 16, 2020

Hmm, I guess the performance of parsing would regress without MutableGuid or it would need Unsafe hacks in more places.

@Gnbrkm41
Copy link
Contributor Author

Does this need to be evaluated as breaking change if code like this requires compensating for this?

In this case, all the fields are private so existing user interop codes (in fact, any other code outside of the Guid type) must use unsafe methods (e.g. taking pointers, S.R.CS.Unsafe, S.R.IS.Marshal...) to access the values anyway, which wouldn't be affected by those readonly modifiers.

(Also, I agree that the delta looks pretty ugly.)
What would it take to refactor this cleanly, without introducing the MutableGuid type?

For the parsing I suppose we could Introduce a bunch of local variables, continue with the same logic & call new Guid(Int32, Int16, Int16, Byte, Byte, Byte, Byte, Byte, Byte, Byte, Byte) at the end. Not sure if the JIT would be able to turn it into something of similar efficiency though; Maybe I'm worried too much about the performance.

For the Guid.NewGuid maybe I should have turned it to something on the line of stackalloc byte[sizeof(Guid)] followed by a reinterpret-cast at the end.

@tannergooding
Copy link
Member

having every methods marked as readonly yet not having the type itself marked readonly

We have some other types, such as the System.Numerics.Vector* types, that are in the same boat. They are functionally readonly, but due to the fields being publicly exposed we can't mark the entire struct as readonly.

However, marking the instance methods as readonly achieves the desired affect as removing hidden copies.

I think it might be worth keeping the code simpler, especially since it is performance oriented, and just marking the various instance methods as readonly instead.

@Gnbrkm41
Copy link
Contributor Author

We have some other types, such as the System.Numerics.Vector* types, that are in the same boat. They are functionally readonly, but due to the fields being publicly exposed we can't mark the entire struct as readonly.

In this case though, none of the fields are exposed, so it wouldn't be a breaking change to mark the whole type as readonly (unlike the Vector types as the fields can be mutated in user code)

However, marking the instance methods as readonly achieves the desired affect as removing hidden copies.

While it is true that marking all the instance methods would achieve the same effect, Marking the whole type readonly conveys that Guid is a immutable structure (which it really is). We could have easily went with an implementation that does not require mutation of the internal structure and mark it as readonly, but we're not doing so for (possible) optimisation reasons during construction, and looking from outside they would look the same. Marking the type as non-readonly for just that does not seem right to me.

@Gnbrkm41
Copy link
Contributor Author

I'll explore a bit more to see if there's alternative ways to approach this tomorrow (of course, while not compromising on the performance aspect).

Just wondering - is there an "easy way" to obtain JIT output for those framework methods?

@Gnbrkm41
Copy link
Contributor Author

Gave another thought about it... and realised, wouldn't we need to do the same thing even if we opt to mark individual members as readonly, as C# wouldn't still allow mutation of the fields in readonly methods?

@bartonjs
Copy link
Member

wouldn't we need to do the same thing even if we opt to mark individual members as readonly

The MutableGuid type would go away, as it's only used in static methods. The ctor usages of Unsafe.AsRef would go away... GetHashCode and Equals might still need Unsafe.AsRef.

@GrabYourPitchforks
Copy link
Member

Would it also make sense to have a unit test that iterates over all the public members of Guid and verifies that they're annotated with [IsReadOnly]? That way if we add public API in the future there's less of a chance of us accidentally shipping a method that's not annotated appropriately. This might be a viable trade-off if we're not willing to annotate the entire type.

@Gnbrkm41
Copy link
Contributor Author

Gnbrkm41 commented Feb 6, 2020

It looks like it'll be worth some extra discussion perhaps. Closing for now & Will open an issue.

@Gnbrkm41 Gnbrkm41 closed this Feb 6, 2020
@Gnbrkm41 Gnbrkm41 deleted the readonlyguid branch March 13, 2020 16:37
@ghost ghost locked as resolved and limited conversation to collaborators Dec 11, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants