BitConverter.GetBytes in place

本文关键字:place in GetBytes BitConverter | 更新日期: 2023-09-27 17:58:16

我需要将UInt16UInt64中的值作为Byte[]。目前我使用的是BitConverter.GetBytes,但这种方法每次都会给我一个新的数组实例。

我想使用一种方法,允许我将这些值"复制"到现有的数组中,比如:

.ToBytes(UInt64 value, Byte[] array, Int32 offset);
.ToBytes(UInt16 value, Byte[] array, Int32 offset);

我一直在用ILSpy查看.NET源代码,但我不太确定这段代码是如何工作的,以及如何安全地修改它以满足我的要求:

public unsafe static byte[] GetBytes(long value)
{
    byte[] array = new byte[8];
    fixed (byte* ptr = array)
    {
            *(long*)ptr = value;
    }
    return array;
}

实现这一目标的正确方法是什么?

更新:我不能使用不安全的代码。它不应该创建新的数组实例。

BitConverter.GetBytes in place

您可以这样做:

static unsafe void ToBytes(ulong value, byte[] array, int offset)
{
    fixed (byte* ptr = &array[offset])
        *(ulong*)ptr = value;
}

用法:

byte[] array = new byte[9];
ToBytes(0x1122334455667788, array, 1);

您只能以字节为单位设置偏移量。

如果你想要有管理的方式:

static void ToBytes(ulong value, byte[] array, int offset)
{
    byte[] valueBytes = BitConverter.GetBytes(value);
    Array.Copy(valueBytes, 0, array, offset, valueBytes.Length);
}

或者您可以自己填写数值:

static void ToBytes(ulong value, byte[] array, int offset)
{
    for (int i = 0; i < 8; i++)
    {
        array[offset + i] = (byte)value;
        value >>= 8;
    }
}

现在.NET已经添加了Span<T>支持,以便在没有多余分配的情况下更好地处理数组、非托管内存等,他们还添加了System.Buffer.Binary.BinaryPrimities.

这可以按照你的意愿工作,例如WriteUInt64BigEndian有这样的签名:

public static void WriteUInt64BigEndian (Span<byte> destination, ulong value);

这避免了分配。

您说要避免创建新数组,不能使用unsafe。要么将Ulugbek Umirov的答案用于缓存数组(小心线程问题),要么:

static void ToBytes(ulong value, byte[] array, int offset) {
 unchecked {
  array[offset + 0] = (byte)(value >> (8*7));
  array[offset + 1] = (byte)(value >> (8*6));
  array[offset + 2] = (byte)(value >> (8*5));
  array[offset + 3] = (byte)(value >> (8*4));
  //...
 }
}

出于某种原因,您似乎希望避免创建任何临时的新数组。您还希望避免不安全的代码。

您可以固定对象,然后复制到数组中。

public static void ToBytes(ulong value, byte[] array, int offset) 
{
    GCHandle handle = GCHandle.Alloc(value, GCHandleType.Pinned);
    try
    {
        Marshal.Copy(handle.AddrOfPinnedObject(), array, offset, 8);
    }
    finally
    {
        handle.Free();
    }
}

BinaryWriter是一个很好的解决方案。

var writer = new BinaryWriter(new MemoryStream(yourbuffer, youroffset, yourbuffer.Length-youroffset));
writer.Write(someuint64);

当你需要将大量数据连续转换到缓冲区时,这很有用

var writer = new BinaryWriter(new MemoryStream(yourbuffer));
foreach(var value in yourints){
    writer.Write(value);
}

或者当您只想写入文件时,最好使用BinaryWriter。

var writer = new BinaryWriter(yourFileStream);
foreach(var value in yourints){
    writer.Write(value);
}

对于其他偶然发现这一点的人。如果不需要Big Endian支持,并且允许Unsafe,那么我专门编写了一个库,以最大限度地减少单线程序列化程序序列化期间的GC分配,例如在TCP套接字之间进行序列化时。

注意:仅支持Little Endian示例X86/X64(将来可能会添加big Endian支持)

https://github.com/tcwicks/ChillX/tree/master/src/ChillX.Serialization

在这个过程中,还重写了BitConverter,使其工作方式类似于BitPrimitives,但也有很多额外功能,如支持数组。https://github.com/tcwicks/ChillX/blob/master/src/ChillX.Serialization/BitConverterExtended.cs

Random rnd = new Random();
RentedBuffer<byte> buffer = RentedBuffer<byte>.Shared.Rent(BitConverterExtended.SizeOfUInt64
    + (20 * BitConverterExtended.SizeOfUInt16)
    + (20 * BitConverterExtended.SizeOfTimeSpan)
    + (10 * BitConverterExtended.SizeOfSingle);
UInt64 exampleLong = long.MaxValue;
int startIndex = 0;
startIndex += BitConverterExtended.GetBytes(exampleLong, buffer.BufferSpan, startIndex);
UInt16[] shortArray = new UInt16[20];
for (int I = 0; I < shortArray.Length; I++) { shortArray[I] = (ushort)rnd.Next(0, UInt16.MaxValue); }
//When using reflection / expression trees CLR cannot distinguish between UInt16 and Int16 or Uint64 and Int64 etc...
//Therefore Uint methods are renamed.
startIndex += BitConverterExtended.GetBytesUShortArray(shortArray, buffer.BufferSpan, startIndex);
TimeSpan[] timespanArray = new TimeSpan[20];
for (int I = 0; I < timespanArray.Length; I++) { timespanArray[I] = TimeSpan.FromSeconds(rnd.Next(0, int.MaxValue)); }
startIndex += BitConverterExtended.GetBytes(timespanArray, buffer.BufferSpan, startIndex);
float[] floatArray = new float[10];
for (int I = 0; I < floatArray.Length; I++) { floatArray[I] = MathF.PI * rnd.Next(short.MinValue, short.MaxValue); }
startIndex += BitConverterExtended.GetBytes(floatArray, buffer.BufferSpan, startIndex);
//Do stuff with buffer and then
buffer.Return();
//Or
buffer = null;
//and let RentedBufferContract do this automatically

根本问题实际上有两个方面。即使您使用比特基元,我们仍然存在写入/读取字节数组缓冲区的问题。如果我们有很多数组字段T[],例如int[],问题会进一步复杂化

使用标准的BitConverter会导致80%的时间花在GC上。bitprimitives要好得多,但我们仍然需要管理byte[]缓冲区。如果我们为1000个int数组中的每个int创建一个byte[]缓冲区,即要垃圾收集的1000个byte[4]数组,这意味着从ArrayPool租用一个足够大的缓冲区来容纳所有1000个int(int[4000]),但我们必须管理返回这些缓冲区。更复杂的是,我们必须跟踪实际使用的每个缓冲区的长度,因为ArrayPool将返回比请求的大的数组。System.Buffers.ArrayPool.Shared.Rent(4000);通常会返回一个字节[4096],甚至可能返回一个比特[8192]

此序列化代码:在ArrayPool周围使用包装器,以使其使用透明:https://github.com/tcwicks/ChillX/blob/master/src/ChillX.Core/Structures/ManagedPool.cs

用于管理ArrayPool缓冲区的租用和归还:

https://github.com/tcwicks/ChillX/blob/master/src/ChillX.Core/Structures/RentedBuffer.cs

操作员过载+操作员从跨度自动分配

用于防止内存泄漏,并管理使用和忘记风格保证归还租用的缓冲区:

https://github.com/tcwicks/ChillX/blob/master/src/ChillX.Core/Structures/RentedBufferContract.cs

示例:

//Assume ArrayProperty_Long is a long[] array.
RentedBuffer<long> ClonedArray = RentedBuffer<long>.Shared.Rent(ArrayProperty_Long.Length);
ClonedArray += ArrayProperty_Long;

ClonedArray将返回到ArrayPool。当ClonedAarray超出范围(由RentedBufferContract处理)时,会自动共享。或者调用ClonedArray.Return();

p.s.如有反馈,不胜感激。

以下基准测试比较了使用租用的缓冲区和此序列化程序实现与使用没有租用缓冲区的MessagePack的性能。Messagepack(如果是微基准)是一个更快的序列化程序。这种性能差异纯粹是由于通过池化/租用缓冲区来减少GC收集开销。

----------------------------------------------------------------------------------
Benchmarking ChillX Serializer: Test Object: Data class with 31 properties / fields of different types inlcuding multiple arrays of different types
Num Reps: 50000  -  Array Size: 256
----------------------------------------------------------------------------------
Entity Size bytes: 20,678  -  Count: 00,050,000  -  Threads: 01  -  Entities Per Second: 00,025,523  -  Mbps: 503.32  -  Time: 00:00:01.9589880
Entity Size bytes: 20,678  -  Count: 00,050,000  -  Threads: 01  -  Entities Per Second: 00,026,129  -  Mbps: 515.26  -  Time: 00:00:01.9135886
Entity Size bytes: 20,678  -  Count: 00,050,000  -  Threads: 01  -  Entities Per Second: 00,026,291  -  Mbps: 518.46  -  Time: 00:00:01.9018027
----------------------------------------------------------------------------------
Benchmarking MessagePack Serializer: Test Object: Data class with 31 properties / fields of different types inlcuding multiple arrays of different types
Num Reps: 50000  -  Array Size: 256
----------------------------------------------------------------------------------
Entity Size bytes: 19,811  -  Count: 00,050,000  -  Threads: 01  -  Entities Per Second: 00,012,386  -  Mbps: 234.01  -  Time: 00:00:04.0668261
Entity Size bytes: 19,811  -  Count: 00,050,000  -  Threads: 01  -  Entities Per Second: 00,012,285  -  Mbps: 232.10  -  Time: 00:00:04.0523329
Entity Size bytes: 19,811  -  Count: 00,050,000  -  Threads: 01  -  Entities Per Second: 00,012,642  -  Mbps: 238.84  -  Time: 00:00:03.9811276