Skip to content

Reduce Unsafe usage in TryWriteSignificand/TryHash helpers#127921

Draft
EgorBo wants to merge 2 commits intodotnet:mainfrom
EgorBo:reduce-unsafe-tryhash-trywrite-significand
Draft

Reduce Unsafe usage in TryWriteSignificand/TryHash helpers#127921
EgorBo wants to merge 2 commits intodotnet:mainfrom
EgorBo:reduce-unsafe-tryhash-trywrite-significand

Conversation

@EgorBo
Copy link
Copy Markdown
Member

@EgorBo EgorBo commented May 7, 2026

No description provided.

Replace MemoryMarshal.GetReference + Unsafe.WriteUnaligned with
BinaryPrimitives.WriteXxxBigEndian/LittleEndian (or MemoryMarshal.Write
for NFloat where the underlying type is arch-conditional) in:

* Decimal.IFloatingPoint<decimal>.TryWriteSignificand{Big,Little}Endian
* NFloat.IFloatingPoint<NFloat>.TryWrite{Exponent,Significand}{Big,Little}Endian
* XxHash3.TryHash
* XxHash128.WriteBigEndian128 (used by TryHash / GetCurrentHash)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 7, 2026 15:51
@dotnet-policy-service
Copy link
Copy Markdown
Contributor

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

@EgorBo
Copy link
Copy Markdown
Member Author

EgorBo commented May 7, 2026

@jkotas @tannergooding so what would be the way to replace MemoryMarshal.Write here?

BinaryPrimitives won't work nicely as we have #ifdef-dependent types here.
bool ok = BitConverter.TryWriteBytes + Debug.Assert(ok) doesn't look nice/safe
BitConverter.TryWriteBytes + throw helper?

UPD: ah, BitConverter.TryWriteBytes won't work - it doesn't support byte/sbyte

{E7207EB3-49DE-40D1-8AB5-4434CC0F6027}

One of the motivational use cases for #127606 api proposal.

@EgorBo
Copy link
Copy Markdown
Member Author

EgorBo commented May 7, 2026

@MihuBot

This comment was marked as resolved.

@tannergooding
Copy link
Copy Markdown
Member

tannergooding commented May 7, 2026

Change the impl on float/double from:

bool IFloatingPoint<T>.TryWriteExponentBigEndian(Span<byte> destination, out int bytesWritten)
{
    ...
}

to:

internal bool TryWriteExponentBigEndian(Span<byte> destination, out int bytesWritten)
{
    ...
}

bool IFloatingPoint<T>.TryWriteExponentBigEndian(Span<byte> destination, out int bytesWritten)
{
    return TryWriteExponentBigEndian(destination, out bytesWritten);
}

and then change the NFloat implementation to be:

bool IFloatingPoint<NFloat>.TryWriteExponentBigEndian(Span<byte> destination, out int bytesWritten)
{
    return _value.TryWriteExponentBigEndian(destination, out int bytesWritten);
}

Which avoids all the ifdef and other messiness. It's just one of the downsides of these interfaces being explicitly implemented. The alternative is to define a generic helper and call that, but that's extra generic bloat:

static bool TryWriteExponentBigEndian<T>(T value, Span<byte> destination, out int bytesWritten)
    where T : IFloatingPoint<T>
{
    return value.TryWriteExponentBigEndian(destination, out int bytesWritten);
}

Per review feedback, expose internal TryWrite{Exponent,Significand}{Big,Little}Endian
methods on Single/Double next to the explicit interface implementations,
and have NFloat's IFloatingPoint<NFloat>.TryWriteXxx forward directly to
_value.TryWriteXxx. This removes the TARGET_32BIT ifdefs, manual
ReverseEndianness, and the BitConverter.TryWriteBytes + ThrowHelper
boilerplate from NFloat.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@EgorBo
Copy link
Copy Markdown
Member Author

EgorBo commented May 7, 2026

@MihuBot

@EgorBo
Copy link
Copy Markdown
Member Author

EgorBo commented May 7, 2026

Note

This comment was generated with the help of GitHub Copilot.

NFloat TryWriteSignificand/TryWriteExponent (now delegating to the underlying Double/Single helpers):

@EgorBot -linux_amd -osx_arm64

using System;
using System.Numerics;
using System.Runtime.InteropServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

BenchmarkSwitcher.FromAssembly(typeof(Bench).Assembly).Run(args);

public class Bench
{
    private NFloat _value = (NFloat)3.14159265358979;
    private byte[] _buffer = new byte[16];

    [Benchmark]
    public bool SignificandBE() => WriteSignificandBE(_value, _buffer);

    [Benchmark]
    public bool SignificandLE() => WriteSignificandLE(_value, _buffer);

    [Benchmark]
    public bool ExponentBE() => WriteExponentBE(_value, _buffer);

    [Benchmark]
    public bool ExponentLE() => WriteExponentLE(_value, _buffer);

    private static bool WriteSignificandBE<T>(T v, Span<byte> buf) where T : IFloatingPoint<T>
        => v.TryWriteSignificandBigEndian(buf, out _);
    private static bool WriteSignificandLE<T>(T v, Span<byte> buf) where T : IFloatingPoint<T>
        => v.TryWriteSignificandLittleEndian(buf, out _);
    private static bool WriteExponentBE<T>(T v, Span<byte> buf) where T : IFloatingPoint<T>
        => v.TryWriteExponentBigEndian(buf, out _);
    private static bool WriteExponentLE<T>(T v, Span<byte> buf) where T : IFloatingPoint<T>
        => v.TryWriteExponentLittleEndian(buf, out _);
}

@EgorBo
Copy link
Copy Markdown
Member Author

EgorBo commented May 7, 2026

Note

This comment was generated with the help of GitHub Copilot.

XxHash128 incremental Append + GetCurrentHash (which calls GetCurrentHashCore -> WriteBigEndian128) across a few input sizes — the change replaces Unsafe.WriteUnaligned + ReverseEndianness with BinaryPrimitives.WriteUInt64BigEndian. Smaller payloads should make the writing portion more visible.

@EgorBot -linux_amd -osx_arm64

using System;
using System.IO.Hashing;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

BenchmarkSwitcher.FromAssembly(typeof(Bench).Assembly).Run(args);

public class Bench
{
    [Params(8, 32, 128, 1024)]
    public int Length { get; set; }

    private byte[] _data = default!;
    private byte[] _dest = new byte[16];
    private XxHash128 _hash = default!;

    [GlobalSetup]
    public void Setup()
    {
        _data = new byte[Length];
        new Random(42).NextBytes(_data);
        _hash = new XxHash128();
    }

    [Benchmark]
    public int AppendAndGetCurrentHash()
    {
        _hash.Reset();
        _hash.Append(_data);
        return _hash.GetCurrentHash(_dest);
    }

    [Benchmark]
    public int StaticTryHash()
    {
        XxHash128.TryHash(_data, _dest, out int written);
        return written;
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants