Skip to content

Remove unsafe code from BigInteger.TryWriteBigEndian and TryWriteLittleEndian#124858

Draft
Copilot wants to merge 7 commits intomainfrom
copilot/remove-unsafe-code-big-integer
Draft

Remove unsafe code from BigInteger.TryWriteBigEndian and TryWriteLittleEndian#124858
Copilot wants to merge 7 commits intomainfrom
copilot/remove-unsafe-code-big-integer

Conversation

Copy link
Contributor

Copilot AI commented Feb 25, 2026

Replace Unsafe.WriteUnaligned/pointer-arithmetic patterns in BigInteger.TryWriteBigEndian and TryWriteLittleEndian with safe, idiomatic equivalents using BinaryPrimitives.

Description

bits is null path — replace manual endianness-conditional Unsafe.WriteUnaligned with BinaryPrimitives:

// Before
int value = BitConverter.IsLittleEndian ? BinaryPrimitives.ReverseEndianness(_sign) : _sign;
Unsafe.WriteUnaligned(ref MemoryMarshal.GetReference(destination), value);

// After
BinaryPrimitives.WriteInt32BigEndian(destination, _sign);

_bits positive path — replace ref byte pointer walking with BinaryPrimitives.WriteUInt32BigEndian/WriteUInt32LittleEndian using forward loops. For big endian, word i (LSW-first) is written at destination.Slice((bits.Length - 1 - i) * sizeof(uint)):

// Before
ref byte address = ref Unsafe.Add(ref MemoryMarshal.GetReference(destination), (bits.Length - 1) * sizeof(uint));
for (int i = 0; i < bits.Length; i++)
{
    Unsafe.WriteUnaligned(ref address, BinaryPrimitives.ReverseEndianness(bits[i]));
    address = ref Unsafe.Subtract(ref address, sizeof(uint));
}

// After (big endian)
for (int i = 0; i < bits.Length; i++)
{
    BinaryPrimitives.WriteUInt32BigEndian(destination.Slice((bits.Length - 1 - i) * sizeof(uint)), bits[i]);
}

_bits negative path — for little endian, a sliding Span<byte> advancing by sizeof(uint) each iteration is used. For big endian, the write offset is computed directly from the loop index as byteCount - (i + 1) * sizeof(uint), avoiding a decrementing offset variable. The original do-while/while carry propagation structure is preserved (avoids per-iteration carry branch after carry stops propagating):

// big endian negative path
BinaryPrimitives.WriteUInt32BigEndian(destination.Slice(byteCount - (i + 1) * sizeof(uint)), part);

Sign-extension detection — replace Unsafe.AreSame pointer comparison with a simple integer check:

// Before
if (Unsafe.AreSame(ref address, ref startAddress)) { Unsafe.WriteUnaligned(ref address, uint.MaxValue); }

// After
if (byteCount > bits.Length * sizeof(uint)) { BinaryPrimitives.WriteUInt32BigEndian(destination, uint.MaxValue); }

Structural improvements:

  • Early return pattern for insufficient destination buffer (reduces nesting)
  • Slice(offset) without explicit length to minimize redundant bounds checking (BinaryPrimitives validates length ≥ 4 internally)

Unsafe, MemoryMarshal.Cast, and BitConverter are no longer used in either method. All loops are forward loops. All existing tests (2647) pass.


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

@dotnet-policy-service
Copy link
Contributor

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

Copilot AI and others added 2 commits February 25, 2026 14:45
… BigInteger

Co-authored-by: EgorBo <523221+EgorBo@users.noreply.github.com>
…ndly loops

Co-authored-by: EgorBo <523221+EgorBo@users.noreply.github.com>
Copilot AI changed the title [WIP] Remove unsafe code from TryWrite methods in BigInteger Remove unsafe code from BigInteger.TryWriteBigEndian and TryWriteLittleEndian Feb 25, 2026
…/LittleEndian

Co-authored-by: EgorBo <523221+EgorBo@users.noreply.github.com>
…m index

Co-authored-by: EgorBo <523221+EgorBo@users.noreply.github.com>
@EgorBo
Copy link
Member

EgorBo commented Feb 25, 2026

@copilot please create microbenchmarks using the performance-benchmarks skill to validate performance of these methods (different values, lengths, etc).

This comment was marked as outdated.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR refactors TryWriteBigEndian and TryWriteLittleEndian methods in BigInteger to replace unsafe code patterns (Unsafe, MemoryMarshal.Cast, BitConverter) with BinaryPrimitives APIs and use forward loops throughout instead of reversed loops.

Changes:

  • Replaced pointer-based writing with BinaryPrimitives.WriteInt32BigEndian/LittleEndian and BinaryPrimitives.WriteUInt32BigEndian/LittleEndian
  • Restructured control flow with early return pattern for insufficient destination buffer
  • Simplified loop structures to use forward iteration with offset calculations for reverse ordering where needed

Comment on lines 3477 to 3482
int offset = 0;
for (int i = 0; i < bits.Length; i++)
{
// When the value is negative, we need to copy the two's complement representation
// We'll do this "inline" to avoid needing to unnecessarily allocate.

ref byte startAddress = ref MemoryMarshal.GetReference(destination);
ref byte address = ref Unsafe.Add(ref startAddress, byteCount - sizeof(uint));

int i = 0;
uint part;

do
{
// first do complement and +1 as long as carry is needed
part = ~bits[i] + 1;

if (BitConverter.IsLittleEndian)
{
part = BinaryPrimitives.ReverseEndianness(part);
}

Unsafe.WriteUnaligned(ref address, part);
address = ref Unsafe.Subtract(ref address, sizeof(uint));

i++;
}
while ((part == 0) && (i < bits.Length));

while (i < bits.Length)
{
// now ones complement is sufficient
part = ~bits[i];

if (BitConverter.IsLittleEndian)
{
part = BinaryPrimitives.ReverseEndianness(part);
}

Unsafe.WriteUnaligned(ref address, part);
address = ref Unsafe.Subtract(ref address, sizeof(uint));

i++;
}

if (Unsafe.AreSame(ref address, ref startAddress))
{
// We need one extra part to represent the sign as the most
// significant bit of the two's complement value was 0.
Unsafe.WriteUnaligned(ref address, uint.MaxValue);
}
else
{
// Otherwise we should have been precisely one part behind address
Debug.Assert(Unsafe.AreSame(ref startAddress, ref Unsafe.Add(ref address, sizeof(uint))));
}
BinaryPrimitives.WriteUInt32BigEndian(destination.Slice(offset, sizeof(uint)), bits[i]);
offset += sizeof(uint);
}
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

The loop iterates forward through bits array, but bits[0] is the least significant word. For big endian format, the most significant word must be written first. The loop should iterate backwards from bits.Length-1 down to 0, writing to increasing offsets. As written, this will produce incorrect byte order for multi-word positive values (e.g., Int64MaxValue would write 0xFFFFFFFF 0x7FFFFFFF instead of 0x7FFFFFFF 0xFFFFFFFF).

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

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

@copilot please address the feedback. Also, try to figure out what we can do to improve the benchmark results, you may find the current results at EgorBot/Benchmarks#6 (comment)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in e6c1f5a. The big-endian positive path was incorrectly writing bits[0] (LSW) at offset 0 — restored the correct (bits.Length - 1 - i) * sizeof(uint) offset calculation. Also restored index-based offset byteCount - (i + 1) * sizeof(uint) for the negative path, and the do-while/while carry structure for both methods (avoids per-iteration carry branch after it becomes 0). All 2647 tests pass.

For performance improvement: the main overhead vs the original Unsafe.WriteUnaligned code comes from per-iteration Slice + bounds checking inside MemoryMarshal.Write. To minimize this, I'm now using Slice(offset) without an explicit length (avoids the double bounds check from Slice(offset, 4) + internal length check). The little-endian paths use a sliding Span<byte> instead.

Here are updated benchmarks:

@EgorBot -linux_amd -osx_arm64

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

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

[MemoryDiagnoser]
public class Bench
{
    private BigInteger _small;
    private BigInteger _mediumPositive;
    private BigInteger _largePositive;
    private BigInteger _mediumNegative;
    private BigInteger _largeNegative;
    private BigInteger _negativeSignExtend;
    private byte[] _dest = default!;

    [GlobalSetup]
    public void Setup()
    {
        _small = new BigInteger(42);
        _mediumPositive = BigInteger.Parse("123456789012345678901234567890");
        _largePositive = BigInteger.Pow(2, 512) + 1;
        _mediumNegative = BigInteger.Parse("-123456789012345678901234567890");
        _largeNegative = -(BigInteger.Pow(2, 512) + 1);
        _negativeSignExtend = -(BigInteger.Pow(2, 255));
        _dest = new byte[256];
    }

    [Benchmark]
    public bool SmallBigEndian()
    {
        return ((IBinaryInteger<BigInteger>)_small).TryWriteBigEndian(_dest, out _);
    }

    [Benchmark]
    public bool SmallLittleEndian()
    {
        return ((IBinaryInteger<BigInteger>)_small).TryWriteLittleEndian(_dest, out _);
    }

    [Benchmark]
    public bool MediumPositiveBigEndian()
    {
        return ((IBinaryInteger<BigInteger>)_mediumPositive).TryWriteBigEndian(_dest, out _);
    }

    [Benchmark]
    public bool MediumPositiveLittleEndian()
    {
        return ((IBinaryInteger<BigInteger>)_mediumPositive).TryWriteLittleEndian(_dest, out _);
    }

    [Benchmark]
    public bool LargePositiveBigEndian()
    {
        return ((IBinaryInteger<BigInteger>)_largePositive).TryWriteBigEndian(_dest, out _);
    }

    [Benchmark]
    public bool LargePositiveLittleEndian()
    {
        return ((IBinaryInteger<BigInteger>)_largePositive).TryWriteLittleEndian(_dest, out _);
    }

    [Benchmark]
    public bool MediumNegativeBigEndian()
    {
        return ((IBinaryInteger<BigInteger>)_mediumNegative).TryWriteBigEndian(_dest, out _);
    }

    [Benchmark]
    public bool MediumNegativeLittleEndian()
    {
        return ((IBinaryInteger<BigInteger>)_mediumNegative).TryWriteLittleEndian(_dest, out _);
    }

    [Benchmark]
    public bool LargeNegativeBigEndian()
    {
        return ((IBinaryInteger<BigInteger>)_largeNegative).TryWriteBigEndian(_dest, out _);
    }

    [Benchmark]
    public bool LargeNegativeLittleEndian()
    {
        return ((IBinaryInteger<BigInteger>)_largeNegative).TryWriteLittleEndian(_dest, out _);
    }

    [Benchmark]
    public bool NegativeSignExtendBigEndian()
    {
        return ((IBinaryInteger<BigInteger>)_negativeSignExtend).TryWriteBigEndian(_dest, out _);
    }

    [Benchmark]
    public bool NegativeSignExtendLittleEndian()
    {
        return ((IBinaryInteger<BigInteger>)_negativeSignExtend).TryWriteLittleEndian(_dest, out _);
    }
}

Co-authored-by: EgorBo <523221+EgorBo@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

5 participants