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

Convert.Try{From/To}HexString #86556

Merged
merged 28 commits into from
Oct 31, 2023
Merged

Convert.Try{From/To}HexString #86556

merged 28 commits into from
Oct 31, 2023

Conversation

hrrrrustic
Copy link
Contributor

Close #78472

@dotnet-issue-labeler dotnet-issue-labeler bot added the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label May 21, 2023
@ghost ghost added the community-contribution Indicates that the PR has been added by a community member label May 21, 2023
/// <returns>true if the conversion was successful; otherwise, false.</returns>
public static bool TryFromHexString(ReadOnlySpan<char> source, Span<byte> destination, out int bytesWritten)
{
var length = source.Length;
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
var length = source.Length;
int length = source.Length;

Use concrete types here. Same on other places.

Perf-wise this could be nuint, so all the division and modulo operations are treated as (fast) bit-operations.

Copy link
Member

Choose a reason for hiding this comment

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

Perf-wise this could be nuint, so all the division and modulo operations are treated as (fast) bit-operations.

That's interesting. We rarely use native width types except in interop contexts. @jkotas @EgorBo is it correct that in hot code of this type, it can have perf benefit for simple storage of counters and offsets?

Copy link
Member

@EgorBo EgorBo May 21, 2023

Choose a reason for hiding this comment

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

Afair, JIT knows that Span.Length (and Array.Length as well) are never negative so it doesn't need extra efforts from the C# side 🙂 e.g.:

int Foo(Span<int> s) => s.Length % 4;
; Method Foo(System.Span`1[int]):int:this
       mov      eax, dword ptr [rdx+08H]
       and      eax, 3
       ret      
; Total bytes of code: 7

(some of the changes landed in .NET 8.0 so might be not visible on sharplab yet)

Copy link
Member

Choose a reason for hiding this comment

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

Right -- I'm not aware of patterns where nuint gives better code.

Copy link
Member

@gfoidl gfoidl May 22, 2023

Choose a reason for hiding this comment

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

JIT knows that Span.Length (and Array.Length as well) are never negative

Thanks for the reminder -- I'll forget sometimes about the new JIT-goodness.

We rarely use native width types except in interop contexts

Quite a lot of vectorized code uses the native width types, because memory operations don't need (sign-) extending moves then (e.g. Unsafe.Add(ref ptr, intVariable) vs. Unsafe.Add(ref ptr, nuintVariable) -- https://godbolt.org/z/v8E31Wdsx).

Copy link
Member

Choose a reason for hiding this comment

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

Would Math.DivRem be more efficient here? Especially once it becomes an intrinsic.

Copy link
Contributor

Choose a reason for hiding this comment

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

Would Math.DivRem be more efficient here? Especially once it becomes an intrinsic.

I doubt it, since the JIT optimizes length % 2 != 0 to (length & 1) != 0.

Copy link
Member

@BrennanConroy BrennanConroy May 25, 2023

Choose a reason for hiding this comment

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

That comment wasn't just about the single length check, but also the division done a few lines later. DivRem in theory combines the two operations.

Copy link
Contributor

Choose a reason for hiding this comment

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

the division done a few lines later

The JIT can optimize (uint)length / 2 to (uint)length >> 1. On X86, DivRem would likely be implemented using a div instruction, which is an relatively expensive operation.

@danmoseley
Copy link
Member

went to see whether we have perf coverage and we may be missing the "from hex string" case here. if so might be good to add
https://github.com/dotnet/performance/blob/a39cc8085f5738f1bce11209ec06979003d84ce1/src/benchmarks/micro/libraries/System.Runtime.Extensions/Perf.Convert.cs#L89

@hrrrrustic hrrrrustic marked this pull request as ready for review May 21, 2023 20:50
@hrrrrustic
Copy link
Contributor Author

hrrrrustic commented May 21, 2023

@danmoseley Also created a PR with benchmark
This branch looks stable (previous CI passed with some unrelated errors on IOS), but there is a comment from Stephen #78472 (comment)

@ghost
Copy link

ghost commented May 24, 2023

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

Issue Details

Close #78472

Author: hrrrrustic
Assignees: -
Labels:

area-System.Runtime, community-contribution

Milestone: -

return true;
}

int requiredByteCount = length / 2;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
int requiredByteCount = length / 2;
int requiredByteCount = (int)((uint)length / 2);

The JIT doesn't recognize length as never negative otherwise.

@EgorBo Should we open a JIT issue?

@hrrrrustic
Copy link
Contributor Author

hrrrrustic commented Jul 30, 2023

I'll try. Pushing updates that will probably fail CI since I didn't check them yet, just a checkpoint before sleep 😆

@ghost ghost removed the needs-author-action An issue or pull request that requires more info or actions from the author. label Jul 30, 2023
@hrrrrustic hrrrrustic marked this pull request as ready for review August 2, 2023 18:28
@tannergooding
Copy link
Member

@hrrrrustic, we should be able to get this reviewed now. Could you merge in upstream so we can rerun CI?

Span<char> buffer = stackalloc char[loopCount * 2];
for (int i = 1; i < loopCount; i++)
{
byte[] data = Security.Cryptography.RandomNumberGenerator.GetBytes(i);
Copy link
Member

Choose a reason for hiding this comment

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

We usually use a seeded Random so the tests are reproducible.

Copy link
Member

Choose a reason for hiding this comment

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

I agree with Dan, but since the existing tests in this type were already using Security.Cryptography.RandomNumberGenerator it's acceptable to keep it.

@adamsitnik adamsitnik self-assigned this Oct 30, 2023
Copy link
Member

@adamsitnik adamsitnik left a comment

Choose a reason for hiding this comment

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

Overall it LGTM, I found some things that need to be adjusted before merging, but I've simply provided suggestions for all of them and I am about to apply them right now.

@hrrrrustic thank you for your contribution!

src/libraries/System.Private.CoreLib/src/System/Convert.cs Outdated Show resolved Hide resolved
src/libraries/System.Private.CoreLib/src/System/Convert.cs Outdated Show resolved Hide resolved
src/libraries/System.Private.CoreLib/src/System/Convert.cs Outdated Show resolved Hide resolved
src/libraries/System.Private.CoreLib/src/System/Convert.cs Outdated Show resolved Hide resolved
src/libraries/System.Private.CoreLib/src/System/Convert.cs Outdated Show resolved Hide resolved
if (destination.Length < quotient)
{
source = source.Slice(0, destination.Length * 2);
quotient = destination.Length;
Copy link
Member

Choose a reason for hiding this comment

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

subjective nit: using a variable called quotient to store both "quotient" and "bytesWritten" is confusing to me, we could simply assign bytesWritten to destination.Length here

src/libraries/System.Private.CoreLib/src/System/Convert.cs Outdated Show resolved Hide resolved
Span<char> buffer = stackalloc char[loopCount * 2];
for (int i = 1; i < loopCount; i++)
{
byte[] data = Security.Cryptography.RandomNumberGenerator.GetBytes(i);
Copy link
Member

Choose a reason for hiding this comment

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

I agree with Dan, but since the existing tests in this type were already using Security.Cryptography.RandomNumberGenerator it's acceptable to keep it.

Copy link
Member

@adamsitnik adamsitnik left a comment

Choose a reason for hiding this comment

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

LGTM, thank you @hrrrrustic !

@adamsitnik adamsitnik added this to the 9.0.0 milestone Oct 31, 2023
@adamsitnik adamsitnik merged commit 0a837d3 into dotnet:main Oct 31, 2023
173 of 175 checks passed
@ghost ghost locked as resolved and limited conversation to collaborators Nov 30, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-System.Runtime community-contribution Indicates that the PR has been added by a community member
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[API Proposal]: Convert.TryFromHexString
10 participants