Remove unsafe code from BigInteger.TryWriteBigEndian and TryWriteLittleEndian#124858
Remove unsafe code from BigInteger.TryWriteBigEndian and TryWriteLittleEndian#124858
Conversation
|
Tagging subscribers to this area: @dotnet/runtime-infrastructure |
… BigInteger Co-authored-by: EgorBo <523221+EgorBo@users.noreply.github.com>
…ndly loops Co-authored-by: EgorBo <523221+EgorBo@users.noreply.github.com>
src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs
Outdated
Show resolved
Hide resolved
…/LittleEndian Co-authored-by: EgorBo <523221+EgorBo@users.noreply.github.com>
src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs
Outdated
Show resolved
Hide resolved
…m index Co-authored-by: EgorBo <523221+EgorBo@users.noreply.github.com>
|
@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.
This comment was marked as outdated.
There was a problem hiding this comment.
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/LittleEndianandBinaryPrimitives.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
| 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); | ||
| } |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
@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)
There was a problem hiding this comment.
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>
Replace
Unsafe.WriteUnaligned/pointer-arithmetic patterns inBigInteger.TryWriteBigEndianandTryWriteLittleEndianwith safe, idiomatic equivalents usingBinaryPrimitives.Description
bits is nullpath — replace manual endianness-conditionalUnsafe.WriteUnalignedwithBinaryPrimitives:_bitspositive path — replaceref bytepointer walking withBinaryPrimitives.WriteUInt32BigEndian/WriteUInt32LittleEndianusing forward loops. For big endian, wordi(LSW-first) is written atdestination.Slice((bits.Length - 1 - i) * sizeof(uint)):_bitsnegative path — for little endian, a slidingSpan<byte>advancing bysizeof(uint)each iteration is used. For big endian, the write offset is computed directly from the loop index asbyteCount - (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):Sign-extension detection — replace
Unsafe.AreSamepointer comparison with a simple integer check:Structural improvements:
Slice(offset)without explicit length to minimize redundant bounds checking (BinaryPrimitives validates length ≥ 4 internally)Unsafe,MemoryMarshal.Cast, andBitConverterare 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.