Skip to content

Make System.Guid readonly - #44629

Merged
GrabYourPitchforks merged 5 commits into
dotnet:masterfrom
GrabYourPitchforks:guid_readonly
Nov 17, 2020
Merged

Make System.Guid readonly#44629
GrabYourPitchforks merged 5 commits into
dotnet:masterfrom
GrabYourPitchforks:guid_readonly

Conversation

@GrabYourPitchforks

Copy link
Copy Markdown
Member

This is a follow-up to #1809. It contains two changes: (a) make System.Guid readonly (+ some minor code cleanup); and (b) expose Unsafe.Unbox<T> for internal use, now that Guid can use it as an implementation detail. :)

One piece of feedback given in #1809 was that we shouldn't use the internal MutableGuid type. I've changed the call sites here to use Unsafe.AsRef, but to be honest I think this is introducing noise and that MutableGuid is a cleaner solution. If we don't like MutableGuid, then perhaps we can clone all of the GUID fields into the existing mutable type GuidResult, then introduce an API GuidResult.ToGuid() which is basically a glorified reinterpret_cast.

I think I hit all the needed places to shim Unsafe.Unbox. From investigating other calls, as long as we have a rudimentary implementation in Unsafe.cs we don't need to enlighten the mono interpreter.

- Also adds Unsafe.Unbox for internal use
@jkotas

jkotas commented Nov 13, 2020

Copy link
Copy Markdown
Member

we can clone all of the GUID fields into the existing mutable type GuidResult

I like this idea. GuidResult can actually have just 4 ints (instead of the bytes and shorts from Guid). It may make the parsing tiny bit faster - we won't need to split the parsed ints into bytes.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Guid.cs Outdated
// Compare each element

if (rA != rB) { return false; }
if (Unsafe.Add(ref rA, 1) != Unsafe.Add(ref rB, 1)) { return false; }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You can also change this to do the comparison as longs using Unsafe.ReadUnaligned. We had abandoned PR for that: #35654

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Do you know why it was abandoned? I see the alignment discussion on that thread, but there doesn't seem to be any clear reason the PR was rejected.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I guess Bruce had more important things to do than to finish it.

@EgorBoEgorBoNov 13, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Something like this I guess 🙂

privatestaticboolEqualsCore(inGuidleft,inGuidright){refbyteleftBytes=refUnsafe.As<int,byte>(refUnsafe.AsRef(inleft._a));refbyterightBytes=refUnsafe.As<int,byte>(refUnsafe.AsRef(inright._a));// early out if not equalif(Unsafe.ReadUnaligned<ulong>(refleftBytes)!=Unsafe.ReadUnaligned<ulong>(refrightBytes))returnfalse;returnUnsafe.ReadUnaligned<ulong>(refUnsafe.Add(refleftBytes,8))==Unsafe.ReadUnaligned<ulong>(refUnsafe.Add(refrightBytes,8));}
G_M41817_IG01:G_M41817_IG02:movrax, qword ptr [rcx]cmprax, qword ptr [rdx]je SHORT G_M41817_IG05G_M41817_IG03:xoreax,eaxG_M41817_IG04:retG_M41817_IG05:movrax, qword ptr [rcx+8]cmprax, qword ptr [rdx+8] sete almovzxrax,alG_M41817_IG06:ret

a SIMD version of it is slightly faster for equal guids but in most cases, I guess, we can "early out"

Also, this code can be branchless even without the SIMD (https://godbolt.org/z/jjM3Gs) but JIT doesn't optimize cmp(x1,x2) && cmp(x3,x4) to cmp(x1,x2) | cmp(x3,x4) yet, but it probably makes no sense to make Guid.Equals branchless.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Using 64-bit comparisons didn't seem to help matters - see #44629 (comment).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

| Equal_32Bit | 0.9002 ns | 0.0087 ns | 0.0082 ns |
| Equal_64Bit | 1.1797 ns | 0.0109 ns | 0.0102 ns |

Interesting. #35654 had this case as an improvement.

@benaadams

Copy link
Copy Markdown
Member

Also needs the AsRef for assignments in Guid.Unix.cs?

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Here's the perf run from Guid.Equals(in Guid a, in Guid b) my machine, where the equality operation was using 32-bit comparisons, 64-bit comparisons, branchless 64-bit comparisons, and 128-bit comparisons (SIMD). This assumes that the equality operator function is not being inlined. The branch predictor has also been primed.

BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042
AMD Ryzen 9 3950X, 1 CPU, 32 logical and 16 physical cores
.NET Core SDK=5.0.100
[Host] : .NET Core 5.0.0 (CoreCLR 5.0.20.51904, CoreFX 5.0.20.51904), X64 RyuJIT
DefaultJob : .NET Core 5.0.0 (CoreCLR 5.0.20.51904, CoreFX 5.0.20.51904), X64 RyuJIT
| Method | Mean | Error | StdDev ||------------------------- |----------:|----------:|----------:|| Equal_32Bit | 0.9002 ns | 0.0087 ns | 0.0082 ns || Equal_64Bit | 1.1797 ns | 0.0109 ns | 0.0102 ns || Equal_64BitBranchless | 0.8910 ns | 0.0054 ns | 0.0050 ns || Equal_Sse2 | 0.9096 ns | 0.0080 ns | 0.0075 ns || NotEqual_32Bit | 1.1562 ns | 0.0082 ns | 0.0073 ns || NotEqual_64Bit | 1.1845 ns | 0.0091 ns | 0.0085 ns || NotEqual_64BitBranchless | 0.8972 ns | 0.0091 ns | 0.0086 ns || NotEqual_Sse2 | 0.9080 ns | 0.0069 ns | 0.0058 ns |

I'm not seeing any measurable improvement in using 64-bit comparisons over 32-bit comparisons. The branchless / SIMD versions can be a little better, but now we're operating in the picosecond range. Doesn't seem worth the complexity to me.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

On the plus side, I experimented with replacing Guid.GetHashCode() with one-round AES, since a 128-bit Guid is perfectly sized to mimic one block of plaintext. And this knocked the GetHashCode runtime down to 0.25 nanoseconds on my box. 'Twas a fun experiment, and now the Crypto Board will stare daggers at me for the rest of my days.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

With latest iteration, I tried a hybrid of the "move all the fields to GuidResult" and "is it possible to make GuidResult contain only 4 ints?" approaches. This hybrid approach uses [FieldOffset] so that both the "parse D" and "parse X" logic feel somewhat natural and don't end up polluted with width-changing Unsafe.As calls everywhere.

If this approach isn't preferred, we can investigate alternatives.

@GrabYourPitchforks
GrabYourPitchforks marked this pull request as ready for review November 14, 2020 01:37
g._d = (byte)(uintTmp >> 8);
g._e = (byte)uintTmp;
// _d, _e must be stored as a big-endian ushort
result._de = (BitConverter.IsLittleEndian) ? BinaryPrimitives.ReverseEndianness((ushort)uintTmp) : (ushort)uintTmp;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
result._de=(BitConverter.IsLittleEndian)?BinaryPrimitives.ReverseEndianness((ushort)uintTmp):(ushort)uintTmp;
result._de=BitConverter.IsLittleEndian?BinaryPrimitives.ReverseEndianness((ushort)uintTmp):(ushort)uintTmp;

Nit - multiple places. I believe the usual style for src/libraries is without the extra ( ) in this context.

(BitConverter.IsLittleEndian) ? - 27 occurences under src/libraris
BitConverter.IsLittleEndian ? - 3 occurrences under src/libraries

@GrabYourPitchforksGrabYourPitchforksNov 15, 2020

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Maybe this is an argument to add BinaryPrimitives.htonl, which would encapsulate the check and make the call sites cleaner? Right now I think this API sits on IPAddress, which is the wrong layering.

Edit: We considered this in #29222 and rejected it.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Guid.cs Outdated
@jkotas

Copy link
Copy Markdown
Member

This hybrid approach uses [FieldOffset] so that both the "parse D" and "parse X" logic feel somewhat natural and don't end up polluted with width-changing Unsafe.As calls everywhere

I like this hybrid approach.

@GrabYourPitchforks

GrabYourPitchforks commented Nov 16, 2020

Copy link
Copy Markdown
MemberAuthor

Latest iteration: removed internal Unsafe.Unbox implementation, removed Unsafe.As wherever I could by re-typing the internal APIs, code cleanup w.r.t. unnecessary parens.

Crossing my fingers and hoping CI cooperates today. :)
(Epilogue narration: Levi's optimism was indeed misplaced.)

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Thanks @jkotas for the feedback. Much appreciated! :)

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

/azp run runtime

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@GrabYourPitchforks
GrabYourPitchforks merged commit 43f63d7 into dotnet:masterNov 17, 2020
@GrabYourPitchforks
GrabYourPitchforks deleted the guid_readonly branch November 17, 2020 04:36
@ghostghost locked as resolved and limited conversation to collaborators Dec 17, 2020
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@GrabYourPitchforks@jkotas@benaadams@EgorBo
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Make System.Guid readonly by GrabYourPitchforks · Pull Request #44629 · dotnet/runtime · GitHub
Skip to content

Make System.Guid readonly - #44629

Merged
GrabYourPitchforks merged 5 commits into
dotnet:masterfrom
GrabYourPitchforks:guid_readonly
Nov 17, 2020
Merged

Make System.Guid readonly#44629
GrabYourPitchforks merged 5 commits into
dotnet:masterfrom
GrabYourPitchforks:guid_readonly

Conversation

@GrabYourPitchforks

Copy link
Copy Markdown
Member

This is a follow-up to #1809. It contains two changes: (a) make System.Guid readonly (+ some minor code cleanup); and (b) expose Unsafe.Unbox<T> for internal use, now that Guid can use it as an implementation detail. :)

One piece of feedback given in #1809 was that we shouldn't use the internal MutableGuid type. I've changed the call sites here to use Unsafe.AsRef, but to be honest I think this is introducing noise and that MutableGuid is a cleaner solution. If we don't like MutableGuid, then perhaps we can clone all of the GUID fields into the existing mutable type GuidResult, then introduce an API GuidResult.ToGuid() which is basically a glorified reinterpret_cast.

I think I hit all the needed places to shim Unsafe.Unbox. From investigating other calls, as long as we have a rudimentary implementation in Unsafe.cs we don't need to enlighten the mono interpreter.

- Also adds Unsafe.Unbox for internal use
@jkotas

jkotas commented Nov 13, 2020

Copy link
Copy Markdown
Member

we can clone all of the GUID fields into the existing mutable type GuidResult

I like this idea. GuidResult can actually have just 4 ints (instead of the bytes and shorts from Guid). It may make the parsing tiny bit faster - we won't need to split the parsed ints into bytes.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Guid.cs Outdated
// Compare each element

if (rA != rB) { return false; }
if (Unsafe.Add(ref rA, 1) != Unsafe.Add(ref rB, 1)) { return false; }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You can also change this to do the comparison as longs using Unsafe.ReadUnaligned. We had abandoned PR for that: #35654

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Do you know why it was abandoned? I see the alignment discussion on that thread, but there doesn't seem to be any clear reason the PR was rejected.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I guess Bruce had more important things to do than to finish it.

@EgorBoEgorBoNov 13, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Something like this I guess 🙂

privatestaticboolEqualsCore(inGuidleft,inGuidright){refbyteleftBytes=refUnsafe.As<int,byte>(refUnsafe.AsRef(inleft._a));refbyterightBytes=refUnsafe.As<int,byte>(refUnsafe.AsRef(inright._a));// early out if not equalif(Unsafe.ReadUnaligned<ulong>(refleftBytes)!=Unsafe.ReadUnaligned<ulong>(refrightBytes))returnfalse;returnUnsafe.ReadUnaligned<ulong>(refUnsafe.Add(refleftBytes,8))==Unsafe.ReadUnaligned<ulong>(refUnsafe.Add(refrightBytes,8));}
G_M41817_IG01:G_M41817_IG02:movrax, qword ptr [rcx]cmprax, qword ptr [rdx]je SHORT G_M41817_IG05G_M41817_IG03:xoreax,eaxG_M41817_IG04:retG_M41817_IG05:movrax, qword ptr [rcx+8]cmprax, qword ptr [rdx+8] sete almovzxrax,alG_M41817_IG06:ret

a SIMD version of it is slightly faster for equal guids but in most cases, I guess, we can "early out"

Also, this code can be branchless even without the SIMD (https://godbolt.org/z/jjM3Gs) but JIT doesn't optimize cmp(x1,x2) && cmp(x3,x4) to cmp(x1,x2) | cmp(x3,x4) yet, but it probably makes no sense to make Guid.Equals branchless.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Using 64-bit comparisons didn't seem to help matters - see #44629 (comment).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

| Equal_32Bit | 0.9002 ns | 0.0087 ns | 0.0082 ns |
| Equal_64Bit | 1.1797 ns | 0.0109 ns | 0.0102 ns |

Interesting. #35654 had this case as an improvement.

@benaadams

Copy link
Copy Markdown
Member

Also needs the AsRef for assignments in Guid.Unix.cs?

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Here's the perf run from Guid.Equals(in Guid a, in Guid b) my machine, where the equality operation was using 32-bit comparisons, 64-bit comparisons, branchless 64-bit comparisons, and 128-bit comparisons (SIMD). This assumes that the equality operator function is not being inlined. The branch predictor has also been primed.

BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042
AMD Ryzen 9 3950X, 1 CPU, 32 logical and 16 physical cores
.NET Core SDK=5.0.100
[Host] : .NET Core 5.0.0 (CoreCLR 5.0.20.51904, CoreFX 5.0.20.51904), X64 RyuJIT
DefaultJob : .NET Core 5.0.0 (CoreCLR 5.0.20.51904, CoreFX 5.0.20.51904), X64 RyuJIT
| Method | Mean | Error | StdDev ||------------------------- |----------:|----------:|----------:|| Equal_32Bit | 0.9002 ns | 0.0087 ns | 0.0082 ns || Equal_64Bit | 1.1797 ns | 0.0109 ns | 0.0102 ns || Equal_64BitBranchless | 0.8910 ns | 0.0054 ns | 0.0050 ns || Equal_Sse2 | 0.9096 ns | 0.0080 ns | 0.0075 ns || NotEqual_32Bit | 1.1562 ns | 0.0082 ns | 0.0073 ns || NotEqual_64Bit | 1.1845 ns | 0.0091 ns | 0.0085 ns || NotEqual_64BitBranchless | 0.8972 ns | 0.0091 ns | 0.0086 ns || NotEqual_Sse2 | 0.9080 ns | 0.0069 ns | 0.0058 ns |

I'm not seeing any measurable improvement in using 64-bit comparisons over 32-bit comparisons. The branchless / SIMD versions can be a little better, but now we're operating in the picosecond range. Doesn't seem worth the complexity to me.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

On the plus side, I experimented with replacing Guid.GetHashCode() with one-round AES, since a 128-bit Guid is perfectly sized to mimic one block of plaintext. And this knocked the GetHashCode runtime down to 0.25 nanoseconds on my box. 'Twas a fun experiment, and now the Crypto Board will stare daggers at me for the rest of my days.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

With latest iteration, I tried a hybrid of the "move all the fields to GuidResult" and "is it possible to make GuidResult contain only 4 ints?" approaches. This hybrid approach uses [FieldOffset] so that both the "parse D" and "parse X" logic feel somewhat natural and don't end up polluted with width-changing Unsafe.As calls everywhere.

If this approach isn't preferred, we can investigate alternatives.

@GrabYourPitchforks
GrabYourPitchforks marked this pull request as ready for review November 14, 2020 01:37
g._d = (byte)(uintTmp >> 8);
g._e = (byte)uintTmp;
// _d, _e must be stored as a big-endian ushort
result._de = (BitConverter.IsLittleEndian) ? BinaryPrimitives.ReverseEndianness((ushort)uintTmp) : (ushort)uintTmp;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
result._de=(BitConverter.IsLittleEndian)?BinaryPrimitives.ReverseEndianness((ushort)uintTmp):(ushort)uintTmp;
result._de=BitConverter.IsLittleEndian?BinaryPrimitives.ReverseEndianness((ushort)uintTmp):(ushort)uintTmp;

Nit - multiple places. I believe the usual style for src/libraries is without the extra ( ) in this context.

(BitConverter.IsLittleEndian) ? - 27 occurences under src/libraris
BitConverter.IsLittleEndian ? - 3 occurrences under src/libraries

@GrabYourPitchforksGrabYourPitchforksNov 15, 2020

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Maybe this is an argument to add BinaryPrimitives.htonl, which would encapsulate the check and make the call sites cleaner? Right now I think this API sits on IPAddress, which is the wrong layering.

Edit: We considered this in #29222 and rejected it.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Guid.cs Outdated
@jkotas

Copy link
Copy Markdown
Member

This hybrid approach uses [FieldOffset] so that both the "parse D" and "parse X" logic feel somewhat natural and don't end up polluted with width-changing Unsafe.As calls everywhere

I like this hybrid approach.

@GrabYourPitchforks

GrabYourPitchforks commented Nov 16, 2020

Copy link
Copy Markdown
MemberAuthor

Latest iteration: removed internal Unsafe.Unbox implementation, removed Unsafe.As wherever I could by re-typing the internal APIs, code cleanup w.r.t. unnecessary parens.

Crossing my fingers and hoping CI cooperates today. :)
(Epilogue narration: Levi's optimism was indeed misplaced.)

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Thanks @jkotas for the feedback. Much appreciated! :)

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

/azp run runtime

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@GrabYourPitchforks
GrabYourPitchforks merged commit 43f63d7 into dotnet:masterNov 17, 2020
@GrabYourPitchforks
GrabYourPitchforks deleted the guid_readonly branch November 17, 2020 04:36
@ghostghost locked as resolved and limited conversation to collaborators Dec 17, 2020
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@GrabYourPitchforks@jkotas@benaadams@EgorBo
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Make System.Guid readonly by GrabYourPitchforks · Pull Request #44629 · dotnet/runtime · GitHub
Skip to content

Make System.Guid readonly - #44629

Merged
GrabYourPitchforks merged 5 commits into
dotnet:masterfrom
GrabYourPitchforks:guid_readonly
Nov 17, 2020
Merged

Make System.Guid readonly#44629
GrabYourPitchforks merged 5 commits into
dotnet:masterfrom
GrabYourPitchforks:guid_readonly

Conversation

@GrabYourPitchforks

Copy link
Copy Markdown
Member

This is a follow-up to #1809. It contains two changes: (a) make System.Guid readonly (+ some minor code cleanup); and (b) expose Unsafe.Unbox<T> for internal use, now that Guid can use it as an implementation detail. :)

One piece of feedback given in #1809 was that we shouldn't use the internal MutableGuid type. I've changed the call sites here to use Unsafe.AsRef, but to be honest I think this is introducing noise and that MutableGuid is a cleaner solution. If we don't like MutableGuid, then perhaps we can clone all of the GUID fields into the existing mutable type GuidResult, then introduce an API GuidResult.ToGuid() which is basically a glorified reinterpret_cast.

I think I hit all the needed places to shim Unsafe.Unbox. From investigating other calls, as long as we have a rudimentary implementation in Unsafe.cs we don't need to enlighten the mono interpreter.

- Also adds Unsafe.Unbox for internal use
@jkotas

jkotas commented Nov 13, 2020

Copy link
Copy Markdown
Member

we can clone all of the GUID fields into the existing mutable type GuidResult

I like this idea. GuidResult can actually have just 4 ints (instead of the bytes and shorts from Guid). It may make the parsing tiny bit faster - we won't need to split the parsed ints into bytes.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Guid.cs Outdated
// Compare each element

if (rA != rB) { return false; }
if (Unsafe.Add(ref rA, 1) != Unsafe.Add(ref rB, 1)) { return false; }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You can also change this to do the comparison as longs using Unsafe.ReadUnaligned. We had abandoned PR for that: #35654

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Do you know why it was abandoned? I see the alignment discussion on that thread, but there doesn't seem to be any clear reason the PR was rejected.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I guess Bruce had more important things to do than to finish it.

@EgorBoEgorBoNov 13, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Something like this I guess 🙂

privatestaticboolEqualsCore(inGuidleft,inGuidright){refbyteleftBytes=refUnsafe.As<int,byte>(refUnsafe.AsRef(inleft._a));refbyterightBytes=refUnsafe.As<int,byte>(refUnsafe.AsRef(inright._a));// early out if not equalif(Unsafe.ReadUnaligned<ulong>(refleftBytes)!=Unsafe.ReadUnaligned<ulong>(refrightBytes))returnfalse;returnUnsafe.ReadUnaligned<ulong>(refUnsafe.Add(refleftBytes,8))==Unsafe.ReadUnaligned<ulong>(refUnsafe.Add(refrightBytes,8));}
G_M41817_IG01:G_M41817_IG02:movrax, qword ptr [rcx]cmprax, qword ptr [rdx]je SHORT G_M41817_IG05G_M41817_IG03:xoreax,eaxG_M41817_IG04:retG_M41817_IG05:movrax, qword ptr [rcx+8]cmprax, qword ptr [rdx+8] sete almovzxrax,alG_M41817_IG06:ret

a SIMD version of it is slightly faster for equal guids but in most cases, I guess, we can "early out"

Also, this code can be branchless even without the SIMD (https://godbolt.org/z/jjM3Gs) but JIT doesn't optimize cmp(x1,x2) && cmp(x3,x4) to cmp(x1,x2) | cmp(x3,x4) yet, but it probably makes no sense to make Guid.Equals branchless.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Using 64-bit comparisons didn't seem to help matters - see #44629 (comment).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

| Equal_32Bit | 0.9002 ns | 0.0087 ns | 0.0082 ns |
| Equal_64Bit | 1.1797 ns | 0.0109 ns | 0.0102 ns |

Interesting. #35654 had this case as an improvement.

@benaadams

Copy link
Copy Markdown
Member

Also needs the AsRef for assignments in Guid.Unix.cs?

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Here's the perf run from Guid.Equals(in Guid a, in Guid b) my machine, where the equality operation was using 32-bit comparisons, 64-bit comparisons, branchless 64-bit comparisons, and 128-bit comparisons (SIMD). This assumes that the equality operator function is not being inlined. The branch predictor has also been primed.

BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042
AMD Ryzen 9 3950X, 1 CPU, 32 logical and 16 physical cores
.NET Core SDK=5.0.100
[Host] : .NET Core 5.0.0 (CoreCLR 5.0.20.51904, CoreFX 5.0.20.51904), X64 RyuJIT
DefaultJob : .NET Core 5.0.0 (CoreCLR 5.0.20.51904, CoreFX 5.0.20.51904), X64 RyuJIT
| Method | Mean | Error | StdDev ||------------------------- |----------:|----------:|----------:|| Equal_32Bit | 0.9002 ns | 0.0087 ns | 0.0082 ns || Equal_64Bit | 1.1797 ns | 0.0109 ns | 0.0102 ns || Equal_64BitBranchless | 0.8910 ns | 0.0054 ns | 0.0050 ns || Equal_Sse2 | 0.9096 ns | 0.0080 ns | 0.0075 ns || NotEqual_32Bit | 1.1562 ns | 0.0082 ns | 0.0073 ns || NotEqual_64Bit | 1.1845 ns | 0.0091 ns | 0.0085 ns || NotEqual_64BitBranchless | 0.8972 ns | 0.0091 ns | 0.0086 ns || NotEqual_Sse2 | 0.9080 ns | 0.0069 ns | 0.0058 ns |

I'm not seeing any measurable improvement in using 64-bit comparisons over 32-bit comparisons. The branchless / SIMD versions can be a little better, but now we're operating in the picosecond range. Doesn't seem worth the complexity to me.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

On the plus side, I experimented with replacing Guid.GetHashCode() with one-round AES, since a 128-bit Guid is perfectly sized to mimic one block of plaintext. And this knocked the GetHashCode runtime down to 0.25 nanoseconds on my box. 'Twas a fun experiment, and now the Crypto Board will stare daggers at me for the rest of my days.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

With latest iteration, I tried a hybrid of the "move all the fields to GuidResult" and "is it possible to make GuidResult contain only 4 ints?" approaches. This hybrid approach uses [FieldOffset] so that both the "parse D" and "parse X" logic feel somewhat natural and don't end up polluted with width-changing Unsafe.As calls everywhere.

If this approach isn't preferred, we can investigate alternatives.

@GrabYourPitchforks
GrabYourPitchforks marked this pull request as ready for review November 14, 2020 01:37
g._d = (byte)(uintTmp >> 8);
g._e = (byte)uintTmp;
// _d, _e must be stored as a big-endian ushort
result._de = (BitConverter.IsLittleEndian) ? BinaryPrimitives.ReverseEndianness((ushort)uintTmp) : (ushort)uintTmp;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
result._de=(BitConverter.IsLittleEndian)?BinaryPrimitives.ReverseEndianness((ushort)uintTmp):(ushort)uintTmp;
result._de=BitConverter.IsLittleEndian?BinaryPrimitives.ReverseEndianness((ushort)uintTmp):(ushort)uintTmp;

Nit - multiple places. I believe the usual style for src/libraries is without the extra ( ) in this context.

(BitConverter.IsLittleEndian) ? - 27 occurences under src/libraris
BitConverter.IsLittleEndian ? - 3 occurrences under src/libraries

@GrabYourPitchforksGrabYourPitchforksNov 15, 2020

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Maybe this is an argument to add BinaryPrimitives.htonl, which would encapsulate the check and make the call sites cleaner? Right now I think this API sits on IPAddress, which is the wrong layering.

Edit: We considered this in #29222 and rejected it.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Guid.cs Outdated
@jkotas

Copy link
Copy Markdown
Member

This hybrid approach uses [FieldOffset] so that both the "parse D" and "parse X" logic feel somewhat natural and don't end up polluted with width-changing Unsafe.As calls everywhere

I like this hybrid approach.

@GrabYourPitchforks

GrabYourPitchforks commented Nov 16, 2020

Copy link
Copy Markdown
MemberAuthor

Latest iteration: removed internal Unsafe.Unbox implementation, removed Unsafe.As wherever I could by re-typing the internal APIs, code cleanup w.r.t. unnecessary parens.

Crossing my fingers and hoping CI cooperates today. :)
(Epilogue narration: Levi's optimism was indeed misplaced.)

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Thanks @jkotas for the feedback. Much appreciated! :)

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

/azp run runtime

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@GrabYourPitchforks
GrabYourPitchforks merged commit 43f63d7 into dotnet:masterNov 17, 2020
@GrabYourPitchforks
GrabYourPitchforks deleted the guid_readonly branch November 17, 2020 04:36
@ghostghost locked as resolved and limited conversation to collaborators Dec 17, 2020
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@GrabYourPitchforks@jkotas@benaadams@EgorBo
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Make System.Guid readonly by GrabYourPitchforks · Pull Request #44629 · dotnet/runtime · GitHub
Skip to content

Make System.Guid readonly - #44629

Merged
GrabYourPitchforks merged 5 commits into
dotnet:masterfrom
GrabYourPitchforks:guid_readonly
Nov 17, 2020
Merged

Make System.Guid readonly#44629
GrabYourPitchforks merged 5 commits into
dotnet:masterfrom
GrabYourPitchforks:guid_readonly

Conversation

@GrabYourPitchforks

Copy link
Copy Markdown
Member

This is a follow-up to #1809. It contains two changes: (a) make System.Guid readonly (+ some minor code cleanup); and (b) expose Unsafe.Unbox<T> for internal use, now that Guid can use it as an implementation detail. :)

One piece of feedback given in #1809 was that we shouldn't use the internal MutableGuid type. I've changed the call sites here to use Unsafe.AsRef, but to be honest I think this is introducing noise and that MutableGuid is a cleaner solution. If we don't like MutableGuid, then perhaps we can clone all of the GUID fields into the existing mutable type GuidResult, then introduce an API GuidResult.ToGuid() which is basically a glorified reinterpret_cast.

I think I hit all the needed places to shim Unsafe.Unbox. From investigating other calls, as long as we have a rudimentary implementation in Unsafe.cs we don't need to enlighten the mono interpreter.

- Also adds Unsafe.Unbox for internal use
@jkotas

jkotas commented Nov 13, 2020

Copy link
Copy Markdown
Member

we can clone all of the GUID fields into the existing mutable type GuidResult

I like this idea. GuidResult can actually have just 4 ints (instead of the bytes and shorts from Guid). It may make the parsing tiny bit faster - we won't need to split the parsed ints into bytes.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Guid.cs Outdated
// Compare each element

if (rA != rB) { return false; }
if (Unsafe.Add(ref rA, 1) != Unsafe.Add(ref rB, 1)) { return false; }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You can also change this to do the comparison as longs using Unsafe.ReadUnaligned. We had abandoned PR for that: #35654

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Do you know why it was abandoned? I see the alignment discussion on that thread, but there doesn't seem to be any clear reason the PR was rejected.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I guess Bruce had more important things to do than to finish it.

@EgorBoEgorBoNov 13, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Something like this I guess 🙂

privatestaticboolEqualsCore(inGuidleft,inGuidright){refbyteleftBytes=refUnsafe.As<int,byte>(refUnsafe.AsRef(inleft._a));refbyterightBytes=refUnsafe.As<int,byte>(refUnsafe.AsRef(inright._a));// early out if not equalif(Unsafe.ReadUnaligned<ulong>(refleftBytes)!=Unsafe.ReadUnaligned<ulong>(refrightBytes))returnfalse;returnUnsafe.ReadUnaligned<ulong>(refUnsafe.Add(refleftBytes,8))==Unsafe.ReadUnaligned<ulong>(refUnsafe.Add(refrightBytes,8));}
G_M41817_IG01:G_M41817_IG02:movrax, qword ptr [rcx]cmprax, qword ptr [rdx]je SHORT G_M41817_IG05G_M41817_IG03:xoreax,eaxG_M41817_IG04:retG_M41817_IG05:movrax, qword ptr [rcx+8]cmprax, qword ptr [rdx+8] sete almovzxrax,alG_M41817_IG06:ret

a SIMD version of it is slightly faster for equal guids but in most cases, I guess, we can "early out"

Also, this code can be branchless even without the SIMD (https://godbolt.org/z/jjM3Gs) but JIT doesn't optimize cmp(x1,x2) && cmp(x3,x4) to cmp(x1,x2) | cmp(x3,x4) yet, but it probably makes no sense to make Guid.Equals branchless.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Using 64-bit comparisons didn't seem to help matters - see #44629 (comment).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

| Equal_32Bit | 0.9002 ns | 0.0087 ns | 0.0082 ns |
| Equal_64Bit | 1.1797 ns | 0.0109 ns | 0.0102 ns |

Interesting. #35654 had this case as an improvement.

@benaadams

Copy link
Copy Markdown
Member

Also needs the AsRef for assignments in Guid.Unix.cs?

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Here's the perf run from Guid.Equals(in Guid a, in Guid b) my machine, where the equality operation was using 32-bit comparisons, 64-bit comparisons, branchless 64-bit comparisons, and 128-bit comparisons (SIMD). This assumes that the equality operator function is not being inlined. The branch predictor has also been primed.

BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042
AMD Ryzen 9 3950X, 1 CPU, 32 logical and 16 physical cores
.NET Core SDK=5.0.100
[Host] : .NET Core 5.0.0 (CoreCLR 5.0.20.51904, CoreFX 5.0.20.51904), X64 RyuJIT
DefaultJob : .NET Core 5.0.0 (CoreCLR 5.0.20.51904, CoreFX 5.0.20.51904), X64 RyuJIT
| Method | Mean | Error | StdDev ||------------------------- |----------:|----------:|----------:|| Equal_32Bit | 0.9002 ns | 0.0087 ns | 0.0082 ns || Equal_64Bit | 1.1797 ns | 0.0109 ns | 0.0102 ns || Equal_64BitBranchless | 0.8910 ns | 0.0054 ns | 0.0050 ns || Equal_Sse2 | 0.9096 ns | 0.0080 ns | 0.0075 ns || NotEqual_32Bit | 1.1562 ns | 0.0082 ns | 0.0073 ns || NotEqual_64Bit | 1.1845 ns | 0.0091 ns | 0.0085 ns || NotEqual_64BitBranchless | 0.8972 ns | 0.0091 ns | 0.0086 ns || NotEqual_Sse2 | 0.9080 ns | 0.0069 ns | 0.0058 ns |

I'm not seeing any measurable improvement in using 64-bit comparisons over 32-bit comparisons. The branchless / SIMD versions can be a little better, but now we're operating in the picosecond range. Doesn't seem worth the complexity to me.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

On the plus side, I experimented with replacing Guid.GetHashCode() with one-round AES, since a 128-bit Guid is perfectly sized to mimic one block of plaintext. And this knocked the GetHashCode runtime down to 0.25 nanoseconds on my box. 'Twas a fun experiment, and now the Crypto Board will stare daggers at me for the rest of my days.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

With latest iteration, I tried a hybrid of the "move all the fields to GuidResult" and "is it possible to make GuidResult contain only 4 ints?" approaches. This hybrid approach uses [FieldOffset] so that both the "parse D" and "parse X" logic feel somewhat natural and don't end up polluted with width-changing Unsafe.As calls everywhere.

If this approach isn't preferred, we can investigate alternatives.

@GrabYourPitchforks
GrabYourPitchforks marked this pull request as ready for review November 14, 2020 01:37
g._d = (byte)(uintTmp >> 8);
g._e = (byte)uintTmp;
// _d, _e must be stored as a big-endian ushort
result._de = (BitConverter.IsLittleEndian) ? BinaryPrimitives.ReverseEndianness((ushort)uintTmp) : (ushort)uintTmp;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
result._de=(BitConverter.IsLittleEndian)?BinaryPrimitives.ReverseEndianness((ushort)uintTmp):(ushort)uintTmp;
result._de=BitConverter.IsLittleEndian?BinaryPrimitives.ReverseEndianness((ushort)uintTmp):(ushort)uintTmp;

Nit - multiple places. I believe the usual style for src/libraries is without the extra ( ) in this context.

(BitConverter.IsLittleEndian) ? - 27 occurences under src/libraris
BitConverter.IsLittleEndian ? - 3 occurrences under src/libraries

@GrabYourPitchforksGrabYourPitchforksNov 15, 2020

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Maybe this is an argument to add BinaryPrimitives.htonl, which would encapsulate the check and make the call sites cleaner? Right now I think this API sits on IPAddress, which is the wrong layering.

Edit: We considered this in #29222 and rejected it.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Guid.cs Outdated
@jkotas

Copy link
Copy Markdown
Member

This hybrid approach uses [FieldOffset] so that both the "parse D" and "parse X" logic feel somewhat natural and don't end up polluted with width-changing Unsafe.As calls everywhere

I like this hybrid approach.

@GrabYourPitchforks

GrabYourPitchforks commented Nov 16, 2020

Copy link
Copy Markdown
MemberAuthor

Latest iteration: removed internal Unsafe.Unbox implementation, removed Unsafe.As wherever I could by re-typing the internal APIs, code cleanup w.r.t. unnecessary parens.

Crossing my fingers and hoping CI cooperates today. :)
(Epilogue narration: Levi's optimism was indeed misplaced.)

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Thanks @jkotas for the feedback. Much appreciated! :)

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

/azp run runtime

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@GrabYourPitchforks
GrabYourPitchforks merged commit 43f63d7 into dotnet:masterNov 17, 2020
@GrabYourPitchforks
GrabYourPitchforks deleted the guid_readonly branch November 17, 2020 04:36
@ghostghost locked as resolved and limited conversation to collaborators Dec 17, 2020
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@GrabYourPitchforks@jkotas@benaadams@EgorBo
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Make System.Guid readonly by GrabYourPitchforks · Pull Request #44629 · dotnet/runtime · GitHub
Skip to content

Make System.Guid readonly - #44629

Merged
GrabYourPitchforks merged 5 commits into
dotnet:masterfrom
GrabYourPitchforks:guid_readonly
Nov 17, 2020
Merged

Make System.Guid readonly#44629
GrabYourPitchforks merged 5 commits into
dotnet:masterfrom
GrabYourPitchforks:guid_readonly

Conversation

@GrabYourPitchforks

Copy link
Copy Markdown
Member

This is a follow-up to #1809. It contains two changes: (a) make System.Guid readonly (+ some minor code cleanup); and (b) expose Unsafe.Unbox<T> for internal use, now that Guid can use it as an implementation detail. :)

One piece of feedback given in #1809 was that we shouldn't use the internal MutableGuid type. I've changed the call sites here to use Unsafe.AsRef, but to be honest I think this is introducing noise and that MutableGuid is a cleaner solution. If we don't like MutableGuid, then perhaps we can clone all of the GUID fields into the existing mutable type GuidResult, then introduce an API GuidResult.ToGuid() which is basically a glorified reinterpret_cast.

I think I hit all the needed places to shim Unsafe.Unbox. From investigating other calls, as long as we have a rudimentary implementation in Unsafe.cs we don't need to enlighten the mono interpreter.

- Also adds Unsafe.Unbox for internal use
@jkotas

jkotas commented Nov 13, 2020

Copy link
Copy Markdown
Member

we can clone all of the GUID fields into the existing mutable type GuidResult

I like this idea. GuidResult can actually have just 4 ints (instead of the bytes and shorts from Guid). It may make the parsing tiny bit faster - we won't need to split the parsed ints into bytes.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Guid.cs Outdated
// Compare each element

if (rA != rB) { return false; }
if (Unsafe.Add(ref rA, 1) != Unsafe.Add(ref rB, 1)) { return false; }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You can also change this to do the comparison as longs using Unsafe.ReadUnaligned. We had abandoned PR for that: #35654

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Do you know why it was abandoned? I see the alignment discussion on that thread, but there doesn't seem to be any clear reason the PR was rejected.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I guess Bruce had more important things to do than to finish it.

@EgorBoEgorBoNov 13, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Something like this I guess 🙂

privatestaticboolEqualsCore(inGuidleft,inGuidright){refbyteleftBytes=refUnsafe.As<int,byte>(refUnsafe.AsRef(inleft._a));refbyterightBytes=refUnsafe.As<int,byte>(refUnsafe.AsRef(inright._a));// early out if not equalif(Unsafe.ReadUnaligned<ulong>(refleftBytes)!=Unsafe.ReadUnaligned<ulong>(refrightBytes))returnfalse;returnUnsafe.ReadUnaligned<ulong>(refUnsafe.Add(refleftBytes,8))==Unsafe.ReadUnaligned<ulong>(refUnsafe.Add(refrightBytes,8));}
G_M41817_IG01:G_M41817_IG02:movrax, qword ptr [rcx]cmprax, qword ptr [rdx]je SHORT G_M41817_IG05G_M41817_IG03:xoreax,eaxG_M41817_IG04:retG_M41817_IG05:movrax, qword ptr [rcx+8]cmprax, qword ptr [rdx+8] sete almovzxrax,alG_M41817_IG06:ret

a SIMD version of it is slightly faster for equal guids but in most cases, I guess, we can "early out"

Also, this code can be branchless even without the SIMD (https://godbolt.org/z/jjM3Gs) but JIT doesn't optimize cmp(x1,x2) && cmp(x3,x4) to cmp(x1,x2) | cmp(x3,x4) yet, but it probably makes no sense to make Guid.Equals branchless.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Using 64-bit comparisons didn't seem to help matters - see #44629 (comment).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

| Equal_32Bit | 0.9002 ns | 0.0087 ns | 0.0082 ns |
| Equal_64Bit | 1.1797 ns | 0.0109 ns | 0.0102 ns |

Interesting. #35654 had this case as an improvement.

@benaadams

Copy link
Copy Markdown
Member

Also needs the AsRef for assignments in Guid.Unix.cs?

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Here's the perf run from Guid.Equals(in Guid a, in Guid b) my machine, where the equality operation was using 32-bit comparisons, 64-bit comparisons, branchless 64-bit comparisons, and 128-bit comparisons (SIMD). This assumes that the equality operator function is not being inlined. The branch predictor has also been primed.

BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042
AMD Ryzen 9 3950X, 1 CPU, 32 logical and 16 physical cores
.NET Core SDK=5.0.100
[Host] : .NET Core 5.0.0 (CoreCLR 5.0.20.51904, CoreFX 5.0.20.51904), X64 RyuJIT
DefaultJob : .NET Core 5.0.0 (CoreCLR 5.0.20.51904, CoreFX 5.0.20.51904), X64 RyuJIT
| Method | Mean | Error | StdDev ||------------------------- |----------:|----------:|----------:|| Equal_32Bit | 0.9002 ns | 0.0087 ns | 0.0082 ns || Equal_64Bit | 1.1797 ns | 0.0109 ns | 0.0102 ns || Equal_64BitBranchless | 0.8910 ns | 0.0054 ns | 0.0050 ns || Equal_Sse2 | 0.9096 ns | 0.0080 ns | 0.0075 ns || NotEqual_32Bit | 1.1562 ns | 0.0082 ns | 0.0073 ns || NotEqual_64Bit | 1.1845 ns | 0.0091 ns | 0.0085 ns || NotEqual_64BitBranchless | 0.8972 ns | 0.0091 ns | 0.0086 ns || NotEqual_Sse2 | 0.9080 ns | 0.0069 ns | 0.0058 ns |

I'm not seeing any measurable improvement in using 64-bit comparisons over 32-bit comparisons. The branchless / SIMD versions can be a little better, but now we're operating in the picosecond range. Doesn't seem worth the complexity to me.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

On the plus side, I experimented with replacing Guid.GetHashCode() with one-round AES, since a 128-bit Guid is perfectly sized to mimic one block of plaintext. And this knocked the GetHashCode runtime down to 0.25 nanoseconds on my box. 'Twas a fun experiment, and now the Crypto Board will stare daggers at me for the rest of my days.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

With latest iteration, I tried a hybrid of the "move all the fields to GuidResult" and "is it possible to make GuidResult contain only 4 ints?" approaches. This hybrid approach uses [FieldOffset] so that both the "parse D" and "parse X" logic feel somewhat natural and don't end up polluted with width-changing Unsafe.As calls everywhere.

If this approach isn't preferred, we can investigate alternatives.

@GrabYourPitchforks
GrabYourPitchforks marked this pull request as ready for review November 14, 2020 01:37
g._d = (byte)(uintTmp >> 8);
g._e = (byte)uintTmp;
// _d, _e must be stored as a big-endian ushort
result._de = (BitConverter.IsLittleEndian) ? BinaryPrimitives.ReverseEndianness((ushort)uintTmp) : (ushort)uintTmp;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
result._de=(BitConverter.IsLittleEndian)?BinaryPrimitives.ReverseEndianness((ushort)uintTmp):(ushort)uintTmp;
result._de=BitConverter.IsLittleEndian?BinaryPrimitives.ReverseEndianness((ushort)uintTmp):(ushort)uintTmp;

Nit - multiple places. I believe the usual style for src/libraries is without the extra ( ) in this context.

(BitConverter.IsLittleEndian) ? - 27 occurences under src/libraris
BitConverter.IsLittleEndian ? - 3 occurrences under src/libraries

@GrabYourPitchforksGrabYourPitchforksNov 15, 2020

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Maybe this is an argument to add BinaryPrimitives.htonl, which would encapsulate the check and make the call sites cleaner? Right now I think this API sits on IPAddress, which is the wrong layering.

Edit: We considered this in #29222 and rejected it.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Guid.cs Outdated
@jkotas

Copy link
Copy Markdown
Member

This hybrid approach uses [FieldOffset] so that both the "parse D" and "parse X" logic feel somewhat natural and don't end up polluted with width-changing Unsafe.As calls everywhere

I like this hybrid approach.

@GrabYourPitchforks

GrabYourPitchforks commented Nov 16, 2020

Copy link
Copy Markdown
MemberAuthor

Latest iteration: removed internal Unsafe.Unbox implementation, removed Unsafe.As wherever I could by re-typing the internal APIs, code cleanup w.r.t. unnecessary parens.

Crossing my fingers and hoping CI cooperates today. :)
(Epilogue narration: Levi's optimism was indeed misplaced.)

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Thanks @jkotas for the feedback. Much appreciated! :)

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

/azp run runtime

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@GrabYourPitchforks
GrabYourPitchforks merged commit 43f63d7 into dotnet:masterNov 17, 2020
@GrabYourPitchforks
GrabYourPitchforks deleted the guid_readonly branch November 17, 2020 04:36
@ghostghost locked as resolved and limited conversation to collaborators Dec 17, 2020
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@GrabYourPitchforks@jkotas@benaadams@EgorBo
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Make System.Guid readonly by GrabYourPitchforks · Pull Request #44629 · dotnet/runtime · GitHub
Skip to content

Make System.Guid readonly - #44629

Merged
GrabYourPitchforks merged 5 commits into
dotnet:masterfrom
GrabYourPitchforks:guid_readonly
Nov 17, 2020
Merged

Make System.Guid readonly#44629
GrabYourPitchforks merged 5 commits into
dotnet:masterfrom
GrabYourPitchforks:guid_readonly

Conversation

@GrabYourPitchforks

Copy link
Copy Markdown
Member

This is a follow-up to #1809. It contains two changes: (a) make System.Guid readonly (+ some minor code cleanup); and (b) expose Unsafe.Unbox<T> for internal use, now that Guid can use it as an implementation detail. :)

One piece of feedback given in #1809 was that we shouldn't use the internal MutableGuid type. I've changed the call sites here to use Unsafe.AsRef, but to be honest I think this is introducing noise and that MutableGuid is a cleaner solution. If we don't like MutableGuid, then perhaps we can clone all of the GUID fields into the existing mutable type GuidResult, then introduce an API GuidResult.ToGuid() which is basically a glorified reinterpret_cast.

I think I hit all the needed places to shim Unsafe.Unbox. From investigating other calls, as long as we have a rudimentary implementation in Unsafe.cs we don't need to enlighten the mono interpreter.

- Also adds Unsafe.Unbox for internal use
@jkotas

jkotas commented Nov 13, 2020

Copy link
Copy Markdown
Member

we can clone all of the GUID fields into the existing mutable type GuidResult

I like this idea. GuidResult can actually have just 4 ints (instead of the bytes and shorts from Guid). It may make the parsing tiny bit faster - we won't need to split the parsed ints into bytes.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Guid.cs Outdated
// Compare each element

if (rA != rB) { return false; }
if (Unsafe.Add(ref rA, 1) != Unsafe.Add(ref rB, 1)) { return false; }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You can also change this to do the comparison as longs using Unsafe.ReadUnaligned. We had abandoned PR for that: #35654

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Do you know why it was abandoned? I see the alignment discussion on that thread, but there doesn't seem to be any clear reason the PR was rejected.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I guess Bruce had more important things to do than to finish it.

@EgorBoEgorBoNov 13, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Something like this I guess 🙂

privatestaticboolEqualsCore(inGuidleft,inGuidright){refbyteleftBytes=refUnsafe.As<int,byte>(refUnsafe.AsRef(inleft._a));refbyterightBytes=refUnsafe.As<int,byte>(refUnsafe.AsRef(inright._a));// early out if not equalif(Unsafe.ReadUnaligned<ulong>(refleftBytes)!=Unsafe.ReadUnaligned<ulong>(refrightBytes))returnfalse;returnUnsafe.ReadUnaligned<ulong>(refUnsafe.Add(refleftBytes,8))==Unsafe.ReadUnaligned<ulong>(refUnsafe.Add(refrightBytes,8));}
G_M41817_IG01:G_M41817_IG02:movrax, qword ptr [rcx]cmprax, qword ptr [rdx]je SHORT G_M41817_IG05G_M41817_IG03:xoreax,eaxG_M41817_IG04:retG_M41817_IG05:movrax, qword ptr [rcx+8]cmprax, qword ptr [rdx+8] sete almovzxrax,alG_M41817_IG06:ret

a SIMD version of it is slightly faster for equal guids but in most cases, I guess, we can "early out"

Also, this code can be branchless even without the SIMD (https://godbolt.org/z/jjM3Gs) but JIT doesn't optimize cmp(x1,x2) && cmp(x3,x4) to cmp(x1,x2) | cmp(x3,x4) yet, but it probably makes no sense to make Guid.Equals branchless.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Using 64-bit comparisons didn't seem to help matters - see #44629 (comment).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

| Equal_32Bit | 0.9002 ns | 0.0087 ns | 0.0082 ns |
| Equal_64Bit | 1.1797 ns | 0.0109 ns | 0.0102 ns |

Interesting. #35654 had this case as an improvement.

@benaadams

Copy link
Copy Markdown
Member

Also needs the AsRef for assignments in Guid.Unix.cs?

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Here's the perf run from Guid.Equals(in Guid a, in Guid b) my machine, where the equality operation was using 32-bit comparisons, 64-bit comparisons, branchless 64-bit comparisons, and 128-bit comparisons (SIMD). This assumes that the equality operator function is not being inlined. The branch predictor has also been primed.

BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042
AMD Ryzen 9 3950X, 1 CPU, 32 logical and 16 physical cores
.NET Core SDK=5.0.100
[Host] : .NET Core 5.0.0 (CoreCLR 5.0.20.51904, CoreFX 5.0.20.51904), X64 RyuJIT
DefaultJob : .NET Core 5.0.0 (CoreCLR 5.0.20.51904, CoreFX 5.0.20.51904), X64 RyuJIT
| Method | Mean | Error | StdDev ||------------------------- |----------:|----------:|----------:|| Equal_32Bit | 0.9002 ns | 0.0087 ns | 0.0082 ns || Equal_64Bit | 1.1797 ns | 0.0109 ns | 0.0102 ns || Equal_64BitBranchless | 0.8910 ns | 0.0054 ns | 0.0050 ns || Equal_Sse2 | 0.9096 ns | 0.0080 ns | 0.0075 ns || NotEqual_32Bit | 1.1562 ns | 0.0082 ns | 0.0073 ns || NotEqual_64Bit | 1.1845 ns | 0.0091 ns | 0.0085 ns || NotEqual_64BitBranchless | 0.8972 ns | 0.0091 ns | 0.0086 ns || NotEqual_Sse2 | 0.9080 ns | 0.0069 ns | 0.0058 ns |

I'm not seeing any measurable improvement in using 64-bit comparisons over 32-bit comparisons. The branchless / SIMD versions can be a little better, but now we're operating in the picosecond range. Doesn't seem worth the complexity to me.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

On the plus side, I experimented with replacing Guid.GetHashCode() with one-round AES, since a 128-bit Guid is perfectly sized to mimic one block of plaintext. And this knocked the GetHashCode runtime down to 0.25 nanoseconds on my box. 'Twas a fun experiment, and now the Crypto Board will stare daggers at me for the rest of my days.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

With latest iteration, I tried a hybrid of the "move all the fields to GuidResult" and "is it possible to make GuidResult contain only 4 ints?" approaches. This hybrid approach uses [FieldOffset] so that both the "parse D" and "parse X" logic feel somewhat natural and don't end up polluted with width-changing Unsafe.As calls everywhere.

If this approach isn't preferred, we can investigate alternatives.

@GrabYourPitchforks
GrabYourPitchforks marked this pull request as ready for review November 14, 2020 01:37
g._d = (byte)(uintTmp >> 8);
g._e = (byte)uintTmp;
// _d, _e must be stored as a big-endian ushort
result._de = (BitConverter.IsLittleEndian) ? BinaryPrimitives.ReverseEndianness((ushort)uintTmp) : (ushort)uintTmp;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
result._de=(BitConverter.IsLittleEndian)?BinaryPrimitives.ReverseEndianness((ushort)uintTmp):(ushort)uintTmp;
result._de=BitConverter.IsLittleEndian?BinaryPrimitives.ReverseEndianness((ushort)uintTmp):(ushort)uintTmp;

Nit - multiple places. I believe the usual style for src/libraries is without the extra ( ) in this context.

(BitConverter.IsLittleEndian) ? - 27 occurences under src/libraris
BitConverter.IsLittleEndian ? - 3 occurrences under src/libraries

@GrabYourPitchforksGrabYourPitchforksNov 15, 2020

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Maybe this is an argument to add BinaryPrimitives.htonl, which would encapsulate the check and make the call sites cleaner? Right now I think this API sits on IPAddress, which is the wrong layering.

Edit: We considered this in #29222 and rejected it.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Guid.cs Outdated
@jkotas

Copy link
Copy Markdown
Member

This hybrid approach uses [FieldOffset] so that both the "parse D" and "parse X" logic feel somewhat natural and don't end up polluted with width-changing Unsafe.As calls everywhere

I like this hybrid approach.

@GrabYourPitchforks

GrabYourPitchforks commented Nov 16, 2020

Copy link
Copy Markdown
MemberAuthor

Latest iteration: removed internal Unsafe.Unbox implementation, removed Unsafe.As wherever I could by re-typing the internal APIs, code cleanup w.r.t. unnecessary parens.

Crossing my fingers and hoping CI cooperates today. :)
(Epilogue narration: Levi's optimism was indeed misplaced.)

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Thanks @jkotas for the feedback. Much appreciated! :)

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

/azp run runtime

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@GrabYourPitchforks
GrabYourPitchforks merged commit 43f63d7 into dotnet:masterNov 17, 2020
@GrabYourPitchforks
GrabYourPitchforks deleted the guid_readonly branch November 17, 2020 04:36
@ghostghost locked as resolved and limited conversation to collaborators Dec 17, 2020
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@GrabYourPitchforks@jkotas@benaadams@EgorBo
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Make System.Guid readonly by GrabYourPitchforks · Pull Request #44629 · dotnet/runtime · GitHub
Skip to content

Make System.Guid readonly - #44629

Merged
GrabYourPitchforks merged 5 commits into
dotnet:masterfrom
GrabYourPitchforks:guid_readonly
Nov 17, 2020
Merged

Make System.Guid readonly#44629
GrabYourPitchforks merged 5 commits into
dotnet:masterfrom
GrabYourPitchforks:guid_readonly

Conversation

@GrabYourPitchforks

Copy link
Copy Markdown
Member

This is a follow-up to #1809. It contains two changes: (a) make System.Guid readonly (+ some minor code cleanup); and (b) expose Unsafe.Unbox<T> for internal use, now that Guid can use it as an implementation detail. :)

One piece of feedback given in #1809 was that we shouldn't use the internal MutableGuid type. I've changed the call sites here to use Unsafe.AsRef, but to be honest I think this is introducing noise and that MutableGuid is a cleaner solution. If we don't like MutableGuid, then perhaps we can clone all of the GUID fields into the existing mutable type GuidResult, then introduce an API GuidResult.ToGuid() which is basically a glorified reinterpret_cast.

I think I hit all the needed places to shim Unsafe.Unbox. From investigating other calls, as long as we have a rudimentary implementation in Unsafe.cs we don't need to enlighten the mono interpreter.

- Also adds Unsafe.Unbox for internal use
@jkotas

jkotas commented Nov 13, 2020

Copy link
Copy Markdown
Member

we can clone all of the GUID fields into the existing mutable type GuidResult

I like this idea. GuidResult can actually have just 4 ints (instead of the bytes and shorts from Guid). It may make the parsing tiny bit faster - we won't need to split the parsed ints into bytes.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Guid.cs Outdated
// Compare each element

if (rA != rB) { return false; }
if (Unsafe.Add(ref rA, 1) != Unsafe.Add(ref rB, 1)) { return false; }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You can also change this to do the comparison as longs using Unsafe.ReadUnaligned. We had abandoned PR for that: #35654

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Do you know why it was abandoned? I see the alignment discussion on that thread, but there doesn't seem to be any clear reason the PR was rejected.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I guess Bruce had more important things to do than to finish it.

@EgorBoEgorBoNov 13, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Something like this I guess 🙂

privatestaticboolEqualsCore(inGuidleft,inGuidright){refbyteleftBytes=refUnsafe.As<int,byte>(refUnsafe.AsRef(inleft._a));refbyterightBytes=refUnsafe.As<int,byte>(refUnsafe.AsRef(inright._a));// early out if not equalif(Unsafe.ReadUnaligned<ulong>(refleftBytes)!=Unsafe.ReadUnaligned<ulong>(refrightBytes))returnfalse;returnUnsafe.ReadUnaligned<ulong>(refUnsafe.Add(refleftBytes,8))==Unsafe.ReadUnaligned<ulong>(refUnsafe.Add(refrightBytes,8));}
G_M41817_IG01:G_M41817_IG02:movrax, qword ptr [rcx]cmprax, qword ptr [rdx]je SHORT G_M41817_IG05G_M41817_IG03:xoreax,eaxG_M41817_IG04:retG_M41817_IG05:movrax, qword ptr [rcx+8]cmprax, qword ptr [rdx+8] sete almovzxrax,alG_M41817_IG06:ret

a SIMD version of it is slightly faster for equal guids but in most cases, I guess, we can "early out"

Also, this code can be branchless even without the SIMD (https://godbolt.org/z/jjM3Gs) but JIT doesn't optimize cmp(x1,x2) && cmp(x3,x4) to cmp(x1,x2) | cmp(x3,x4) yet, but it probably makes no sense to make Guid.Equals branchless.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Using 64-bit comparisons didn't seem to help matters - see #44629 (comment).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

| Equal_32Bit | 0.9002 ns | 0.0087 ns | 0.0082 ns |
| Equal_64Bit | 1.1797 ns | 0.0109 ns | 0.0102 ns |

Interesting. #35654 had this case as an improvement.

@benaadams

Copy link
Copy Markdown
Member

Also needs the AsRef for assignments in Guid.Unix.cs?

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Here's the perf run from Guid.Equals(in Guid a, in Guid b) my machine, where the equality operation was using 32-bit comparisons, 64-bit comparisons, branchless 64-bit comparisons, and 128-bit comparisons (SIMD). This assumes that the equality operator function is not being inlined. The branch predictor has also been primed.

BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042
AMD Ryzen 9 3950X, 1 CPU, 32 logical and 16 physical cores
.NET Core SDK=5.0.100
[Host] : .NET Core 5.0.0 (CoreCLR 5.0.20.51904, CoreFX 5.0.20.51904), X64 RyuJIT
DefaultJob : .NET Core 5.0.0 (CoreCLR 5.0.20.51904, CoreFX 5.0.20.51904), X64 RyuJIT
| Method | Mean | Error | StdDev ||------------------------- |----------:|----------:|----------:|| Equal_32Bit | 0.9002 ns | 0.0087 ns | 0.0082 ns || Equal_64Bit | 1.1797 ns | 0.0109 ns | 0.0102 ns || Equal_64BitBranchless | 0.8910 ns | 0.0054 ns | 0.0050 ns || Equal_Sse2 | 0.9096 ns | 0.0080 ns | 0.0075 ns || NotEqual_32Bit | 1.1562 ns | 0.0082 ns | 0.0073 ns || NotEqual_64Bit | 1.1845 ns | 0.0091 ns | 0.0085 ns || NotEqual_64BitBranchless | 0.8972 ns | 0.0091 ns | 0.0086 ns || NotEqual_Sse2 | 0.9080 ns | 0.0069 ns | 0.0058 ns |

I'm not seeing any measurable improvement in using 64-bit comparisons over 32-bit comparisons. The branchless / SIMD versions can be a little better, but now we're operating in the picosecond range. Doesn't seem worth the complexity to me.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

On the plus side, I experimented with replacing Guid.GetHashCode() with one-round AES, since a 128-bit Guid is perfectly sized to mimic one block of plaintext. And this knocked the GetHashCode runtime down to 0.25 nanoseconds on my box. 'Twas a fun experiment, and now the Crypto Board will stare daggers at me for the rest of my days.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

With latest iteration, I tried a hybrid of the "move all the fields to GuidResult" and "is it possible to make GuidResult contain only 4 ints?" approaches. This hybrid approach uses [FieldOffset] so that both the "parse D" and "parse X" logic feel somewhat natural and don't end up polluted with width-changing Unsafe.As calls everywhere.

If this approach isn't preferred, we can investigate alternatives.

@GrabYourPitchforks
GrabYourPitchforks marked this pull request as ready for review November 14, 2020 01:37
g._d = (byte)(uintTmp >> 8);
g._e = (byte)uintTmp;
// _d, _e must be stored as a big-endian ushort
result._de = (BitConverter.IsLittleEndian) ? BinaryPrimitives.ReverseEndianness((ushort)uintTmp) : (ushort)uintTmp;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
result._de=(BitConverter.IsLittleEndian)?BinaryPrimitives.ReverseEndianness((ushort)uintTmp):(ushort)uintTmp;
result._de=BitConverter.IsLittleEndian?BinaryPrimitives.ReverseEndianness((ushort)uintTmp):(ushort)uintTmp;

Nit - multiple places. I believe the usual style for src/libraries is without the extra ( ) in this context.

(BitConverter.IsLittleEndian) ? - 27 occurences under src/libraris
BitConverter.IsLittleEndian ? - 3 occurrences under src/libraries

@GrabYourPitchforksGrabYourPitchforksNov 15, 2020

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Maybe this is an argument to add BinaryPrimitives.htonl, which would encapsulate the check and make the call sites cleaner? Right now I think this API sits on IPAddress, which is the wrong layering.

Edit: We considered this in #29222 and rejected it.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Guid.cs Outdated
@jkotas

Copy link
Copy Markdown
Member

This hybrid approach uses [FieldOffset] so that both the "parse D" and "parse X" logic feel somewhat natural and don't end up polluted with width-changing Unsafe.As calls everywhere

I like this hybrid approach.

@GrabYourPitchforks

GrabYourPitchforks commented Nov 16, 2020

Copy link
Copy Markdown
MemberAuthor

Latest iteration: removed internal Unsafe.Unbox implementation, removed Unsafe.As wherever I could by re-typing the internal APIs, code cleanup w.r.t. unnecessary parens.

Crossing my fingers and hoping CI cooperates today. :)
(Epilogue narration: Levi's optimism was indeed misplaced.)

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Thanks @jkotas for the feedback. Much appreciated! :)

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

/azp run runtime

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@GrabYourPitchforks
GrabYourPitchforks merged commit 43f63d7 into dotnet:masterNov 17, 2020
@GrabYourPitchforks
GrabYourPitchforks deleted the guid_readonly branch November 17, 2020 04:36
@ghostghost locked as resolved and limited conversation to collaborators Dec 17, 2020
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@GrabYourPitchforks@jkotas@benaadams@EgorBo
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Make System.Guid readonly by GrabYourPitchforks · Pull Request #44629 · dotnet/runtime · GitHub
Skip to content

Make System.Guid readonly - #44629

Merged
GrabYourPitchforks merged 5 commits into
dotnet:masterfrom
GrabYourPitchforks:guid_readonly
Nov 17, 2020
Merged

Make System.Guid readonly#44629
GrabYourPitchforks merged 5 commits into
dotnet:masterfrom
GrabYourPitchforks:guid_readonly

Conversation

@GrabYourPitchforks

Copy link
Copy Markdown
Member

This is a follow-up to #1809. It contains two changes: (a) make System.Guid readonly (+ some minor code cleanup); and (b) expose Unsafe.Unbox<T> for internal use, now that Guid can use it as an implementation detail. :)

One piece of feedback given in #1809 was that we shouldn't use the internal MutableGuid type. I've changed the call sites here to use Unsafe.AsRef, but to be honest I think this is introducing noise and that MutableGuid is a cleaner solution. If we don't like MutableGuid, then perhaps we can clone all of the GUID fields into the existing mutable type GuidResult, then introduce an API GuidResult.ToGuid() which is basically a glorified reinterpret_cast.

I think I hit all the needed places to shim Unsafe.Unbox. From investigating other calls, as long as we have a rudimentary implementation in Unsafe.cs we don't need to enlighten the mono interpreter.

- Also adds Unsafe.Unbox for internal use
@jkotas

jkotas commented Nov 13, 2020

Copy link
Copy Markdown
Member

we can clone all of the GUID fields into the existing mutable type GuidResult

I like this idea. GuidResult can actually have just 4 ints (instead of the bytes and shorts from Guid). It may make the parsing tiny bit faster - we won't need to split the parsed ints into bytes.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Guid.cs Outdated
// Compare each element

if (rA != rB) { return false; }
if (Unsafe.Add(ref rA, 1) != Unsafe.Add(ref rB, 1)) { return false; }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You can also change this to do the comparison as longs using Unsafe.ReadUnaligned. We had abandoned PR for that: #35654

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Do you know why it was abandoned? I see the alignment discussion on that thread, but there doesn't seem to be any clear reason the PR was rejected.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I guess Bruce had more important things to do than to finish it.

@EgorBoEgorBoNov 13, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Something like this I guess 🙂

privatestaticboolEqualsCore(inGuidleft,inGuidright){refbyteleftBytes=refUnsafe.As<int,byte>(refUnsafe.AsRef(inleft._a));refbyterightBytes=refUnsafe.As<int,byte>(refUnsafe.AsRef(inright._a));// early out if not equalif(Unsafe.ReadUnaligned<ulong>(refleftBytes)!=Unsafe.ReadUnaligned<ulong>(refrightBytes))returnfalse;returnUnsafe.ReadUnaligned<ulong>(refUnsafe.Add(refleftBytes,8))==Unsafe.ReadUnaligned<ulong>(refUnsafe.Add(refrightBytes,8));}
G_M41817_IG01:G_M41817_IG02:movrax, qword ptr [rcx]cmprax, qword ptr [rdx]je SHORT G_M41817_IG05G_M41817_IG03:xoreax,eaxG_M41817_IG04:retG_M41817_IG05:movrax, qword ptr [rcx+8]cmprax, qword ptr [rdx+8] sete almovzxrax,alG_M41817_IG06:ret

a SIMD version of it is slightly faster for equal guids but in most cases, I guess, we can "early out"

Also, this code can be branchless even without the SIMD (https://godbolt.org/z/jjM3Gs) but JIT doesn't optimize cmp(x1,x2) && cmp(x3,x4) to cmp(x1,x2) | cmp(x3,x4) yet, but it probably makes no sense to make Guid.Equals branchless.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Using 64-bit comparisons didn't seem to help matters - see #44629 (comment).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

| Equal_32Bit | 0.9002 ns | 0.0087 ns | 0.0082 ns |
| Equal_64Bit | 1.1797 ns | 0.0109 ns | 0.0102 ns |

Interesting. #35654 had this case as an improvement.

@benaadams

Copy link
Copy Markdown
Member

Also needs the AsRef for assignments in Guid.Unix.cs?

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Here's the perf run from Guid.Equals(in Guid a, in Guid b) my machine, where the equality operation was using 32-bit comparisons, 64-bit comparisons, branchless 64-bit comparisons, and 128-bit comparisons (SIMD). This assumes that the equality operator function is not being inlined. The branch predictor has also been primed.

BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042
AMD Ryzen 9 3950X, 1 CPU, 32 logical and 16 physical cores
.NET Core SDK=5.0.100
[Host] : .NET Core 5.0.0 (CoreCLR 5.0.20.51904, CoreFX 5.0.20.51904), X64 RyuJIT
DefaultJob : .NET Core 5.0.0 (CoreCLR 5.0.20.51904, CoreFX 5.0.20.51904), X64 RyuJIT
| Method | Mean | Error | StdDev ||------------------------- |----------:|----------:|----------:|| Equal_32Bit | 0.9002 ns | 0.0087 ns | 0.0082 ns || Equal_64Bit | 1.1797 ns | 0.0109 ns | 0.0102 ns || Equal_64BitBranchless | 0.8910 ns | 0.0054 ns | 0.0050 ns || Equal_Sse2 | 0.9096 ns | 0.0080 ns | 0.0075 ns || NotEqual_32Bit | 1.1562 ns | 0.0082 ns | 0.0073 ns || NotEqual_64Bit | 1.1845 ns | 0.0091 ns | 0.0085 ns || NotEqual_64BitBranchless | 0.8972 ns | 0.0091 ns | 0.0086 ns || NotEqual_Sse2 | 0.9080 ns | 0.0069 ns | 0.0058 ns |

I'm not seeing any measurable improvement in using 64-bit comparisons over 32-bit comparisons. The branchless / SIMD versions can be a little better, but now we're operating in the picosecond range. Doesn't seem worth the complexity to me.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

On the plus side, I experimented with replacing Guid.GetHashCode() with one-round AES, since a 128-bit Guid is perfectly sized to mimic one block of plaintext. And this knocked the GetHashCode runtime down to 0.25 nanoseconds on my box. 'Twas a fun experiment, and now the Crypto Board will stare daggers at me for the rest of my days.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

With latest iteration, I tried a hybrid of the "move all the fields to GuidResult" and "is it possible to make GuidResult contain only 4 ints?" approaches. This hybrid approach uses [FieldOffset] so that both the "parse D" and "parse X" logic feel somewhat natural and don't end up polluted with width-changing Unsafe.As calls everywhere.

If this approach isn't preferred, we can investigate alternatives.

@GrabYourPitchforks
GrabYourPitchforks marked this pull request as ready for review November 14, 2020 01:37
g._d = (byte)(uintTmp >> 8);
g._e = (byte)uintTmp;
// _d, _e must be stored as a big-endian ushort
result._de = (BitConverter.IsLittleEndian) ? BinaryPrimitives.ReverseEndianness((ushort)uintTmp) : (ushort)uintTmp;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
result._de=(BitConverter.IsLittleEndian)?BinaryPrimitives.ReverseEndianness((ushort)uintTmp):(ushort)uintTmp;
result._de=BitConverter.IsLittleEndian?BinaryPrimitives.ReverseEndianness((ushort)uintTmp):(ushort)uintTmp;

Nit - multiple places. I believe the usual style for src/libraries is without the extra ( ) in this context.

(BitConverter.IsLittleEndian) ? - 27 occurences under src/libraris
BitConverter.IsLittleEndian ? - 3 occurrences under src/libraries

@GrabYourPitchforksGrabYourPitchforksNov 15, 2020

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Maybe this is an argument to add BinaryPrimitives.htonl, which would encapsulate the check and make the call sites cleaner? Right now I think this API sits on IPAddress, which is the wrong layering.

Edit: We considered this in #29222 and rejected it.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Guid.cs Outdated
@jkotas

Copy link
Copy Markdown
Member

This hybrid approach uses [FieldOffset] so that both the "parse D" and "parse X" logic feel somewhat natural and don't end up polluted with width-changing Unsafe.As calls everywhere

I like this hybrid approach.

@GrabYourPitchforks

GrabYourPitchforks commented Nov 16, 2020

Copy link
Copy Markdown
MemberAuthor

Latest iteration: removed internal Unsafe.Unbox implementation, removed Unsafe.As wherever I could by re-typing the internal APIs, code cleanup w.r.t. unnecessary parens.

Crossing my fingers and hoping CI cooperates today. :)
(Epilogue narration: Levi's optimism was indeed misplaced.)

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Thanks @jkotas for the feedback. Much appreciated! :)

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

/azp run runtime

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@GrabYourPitchforks
GrabYourPitchforks merged commit 43f63d7 into dotnet:masterNov 17, 2020
@GrabYourPitchforks
GrabYourPitchforks deleted the guid_readonly branch November 17, 2020 04:36
@ghostghost locked as resolved and limited conversation to collaborators Dec 17, 2020
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@GrabYourPitchforks@jkotas@benaadams@EgorBo