Skip to content

Remove a few instances of unsafe code #116111

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

Merged
merged 6 commits into from
May 31, 2025
Merged

Conversation

EgorBo
Copy link
Member

@EgorBo EgorBo commented May 29, 2025

No description provided.

@github-actions github-actions bot added the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label May 29, 2025
@EgorBo EgorBo removed the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label May 29, 2025

return (int)(hash1 + (hash2 * 1566083941));
}
return GetNonRandomizedHashCode(new ReadOnlySpan<char>(ref _firstChar, _stringLength));
Copy link
Member

@stephentoub stephentoub May 29, 2025

Choose a reason for hiding this comment

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

Does this negatively impact perf?

The implementation as it existed was benefiting from strings being null terminated. The span-based overload can't rely on that and pays a tad for it.

See large comment above.

Copy link
Member Author

Choose a reason for hiding this comment

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

At the same time the Span one special cased 1,2,3 length, it was an improvement last time I ran benchmark, I'll do again

@EgorBo EgorBo force-pushed the remove-unsafe-529 branch from ac3bc3b to ab67815 Compare May 30, 2025 00:07
@EgorBo

This comment was marked as resolved.

@@ -39,16 +39,15 @@ private static unsafe bool TryReadMultisegment<T>(ref SequenceReader<byte> reade
Debug.Assert(reader.UnreadSpan.Length < sizeof(T));

// Not enough data in the current segment, try to peek for the data we need.
T buffer = default;
Span<byte> tempSpan = new Span<byte>(&buffer, sizeof(T));
Span<byte> tempSpan = stackalloc byte[sizeof(T)];
Copy link
Member

Choose a reason for hiding this comment

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

This seems like a regression?
SharpLab

Copy link
Member Author

@EgorBo EgorBo May 30, 2025

Choose a reason for hiding this comment

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

I am sure whoever wrote the initial code was trying to avoid the stack probe/GS cookie with this workaround. I think this sets a bad example and we should never accept code like this anymore, but does it mean we're going to leave all the existing places as is?

To be honest, at this point I suspect we're going to focus on unsafe only in places nobody use on the hot path because so far in most places regressions are not avoidable like the one with null terminator, etc.

Copy link
Member

Choose a reason for hiding this comment

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

I don't see a reason why a stack probe is any more necessary in M1 than in M2, or in M3 in SharpLab. The JIT should be able to avoid them with const sized stackallocs into spans, just as it does for locals then wrapped in spans.

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't see a reason why a stack probe is any more necessary in M1 than in M2, or in M3

We've discussed that in #52979

All stack buffers should get stack cookie by default, irrespective of the exact pattern used to create them. We may be missing it for some patterns today. ... we really should be performing stack checks more frequently. If the JIT is able to prove that all code operating on the buffer is safe with proper bounds checks, it should be ok to omit the stack cookie. I am not sure how often would this optimization help in practice.

I am not sure we can build any model in JIT like that, cc @dotnet/jit-contrib

Copy link
Member

Choose a reason for hiding this comment

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

If we're serious about that, then we should fix the JIT to emit the cookie in all cases, including like M3. If M1 needs a cookie because, as quoted in that issue, someone may the use unsafe code to manipulate the span, then M2 and M3 do as well, and really any code where a ref is taken to a local and passed around. Otherwise, also quoting from that issue:

I agree that it is more reasonable to omit stack cookies for 100% safe Span-based code

Copy link
Member Author

Choose a reason for hiding this comment

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

I've reverted the change since there is nothing we can do

Copy link
Member

Choose a reason for hiding this comment

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

Current thinking is to re-examine the stack protections in the JIT for .NET 11.

Aside from the concerns above, we are placing increasing amounts of state on the stack that traditionally only lived in the heap.

@EgorBo EgorBo marked this pull request as ready for review May 30, 2025 14:17
@EgorBo
Copy link
Member Author

EgorBo commented May 30, 2025

@stephentoub anything else here?
there is a small extra stack-spill in ReadByte I observe in the diffs that I filed #116112 for.

@@ -178,6 +178,10 @@ private void ComputeHash(IncrementalHash sha256, string path, DateTime lastChang
sha256.AppendData(_byteBuffer, 0, length);
sha256.AppendData(Separator, 0, Separator.Length);

#if NET
Copy link
Member

Choose a reason for hiding this comment

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

I think you could change this for all builds to be:

BinaryPrimitives.WriteInt64LittleEndian(_byteBuffer, lastChangedUtc.Ticks);

The endianness shouldn't matter here, as it's just being used in an in-memory hash to determine whether something changes or not.

Could probably also use:

long ticks = lastChangedUtc.Ticks;
MemoryMarshal.Write(_byteBuffer, ref ticks);

Copy link
Member Author

Choose a reason for hiding this comment

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

Could probably also use:

I was avoiding this one because it is a candidate for "requires unsafe" due to its generic nature

@EgorBo EgorBo merged commit c44f6a9 into dotnet:main May 31, 2025
86 checks passed
@EgorBo EgorBo deleted the remove-unsafe-529 branch May 31, 2025 01:29
{
Write(new ReadOnlySpan<byte>(&value, 1));
Write([value]);
Copy link
Member

Choose a reason for hiding this comment

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

Does this new up an array?

Copy link
Member

@stephentoub stephentoub Jun 2, 2025

Choose a reason for hiding this comment

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

No, Write accepts a ReadOnlySpan. It emits the equivalent of var tmp = value; Write(new ReadOnlySpan<byte>(in tmp));.

@github-actions github-actions bot locked and limited conversation to collaborators Jul 2, 2025
Sign up for free to 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.

5 participants