Endian을 고려해서 원시 형식 값을 메모리 할당 없이 버퍼에 쓰는 방법

안녕하세요?

요즘 우리 닷넷데브에서 엄청 핫한 토픽이 있는데요ㅎ

코드를 리뷰하다 개선되면 좋을 것 같은 부분이 있어 닷넷에서 원시 형식 값을 메모리 할당 없이 버퍼에 쓰는 방법에 대해서 적어볼까 합니다.


구현된 코드를 보면 List<byte> 형식의 버퍼를 만들고 BitConverter.GetBytes()로 배열을 반환한 뒤 필요에 따라 엔디안 변환을 수행한 뒤 그 결과를 버퍼에 복사합니다.

private List<byte> packetData = new List<byte> ();
...
public static byte[] GetBytes(int intByte, bool isLittleEndian = true)
{
    byte[] bytes = BitConverter.GetBytes (intByte);

    if (BitConverter.IsLittleEndian != isLittleEndian)
        Array.Reverse(bytes);

    return bytes;
}

원시 형식 값을 직렬화 하는 과정에서 가장 간단하면서도 보편적인 방법이 아닐까 싶은데요, 이 방법은 BitConverter.GetBytes() 함수가 매번 힙에 바이트 배열을 할당해서 반환합니다. 배열 할당 없이 버퍼에 값을 바로 쓸 수 있다면 메모리 할당과 성능적인 부분에서 많은 이점이 있을 것입니다.

포인터를 이용한 원시 형식 값 기록

원시 형식 값을 버퍼에 기록하는 가장 빠른 방법은 byte 배열에 원시 형식 포인터로 접근해서 값을 써 주는 방법입니다.

var buffer = stackalloc byte[10];
*(int*)(buffer + 2) = 0x12345678;

하지만 여기에는 몇 가지 제약 사항이 있습니다.

  1. 데이터 길이가 동적일 경우 List와 같이 버퍼 확장을 할 수 없음
  2. Endian 처리가 불가능

닷넷이 발전하면서 이를 해소하는데 도움이 되는 몇 가지 기능들이 추가되었습니다.

이러한 메모리 할당 문제 해결에 도움을 주는 Span<T>의 등장과 함께 관련 라이브러리들이 업데이트 되고 있습니다.

대부분 닷넷데브에 이미 소개되었던 것들이라 익숙하신 분듯도 계실 듯 합니다.

CollectionMarshal

버퍼 확장은 .NET 5에서 추가된 CollectionMarshal을 통해 List의 요소를 Span으로 접근할 수 있게 되었습니다.
CollectionsMarshal 클래스 (System.Runtime.InteropServices) | Microsoft Learn
.NET Framework: 2095. C# - .NET5부터 도입된 CollectionsMarshal (sysnet.pe.kr)

하지만 아무래도 해당 기능은 값을 쓰기는 시나리오 보다는 읽는데 더 효과적인 클래스로 쓰기의 경우 List에 공간을 예약하고 값을 기록해야는 번거로움이 있습니다.

BinaryPrimitives

Endian 처리는 .NET Core 2.1부터 추가된 BinaryPrimitives 클래스를 사용하면 Span을 통해 Little Endian과 Big Endian 형태의 값을 읽고 쓸 수 있습니다.
BinaryPrimitives 클래스 (System.Buffers.Binary) | Microsoft Learn

Span<byte> span = new (buffer, 10)[2..];

if (_isLittleEndian)
{
    BinaryPrimitives.WriteInt64LittleEndian(span, value);
}
else
{
    BinaryPrimitives.WriteInt64BigEndian(span, value);
}

ArrayBufferWriter<T>

.NET Core 3.0에서는 Span<T>List<T>처럼 사용할 수 있게 해주는 ArrayBufferWriter<T> 클래스가 추가되었습니다.
ArrayBufferWriter 클래스 (System.Buffers) | Microsoft Learn

아래와 같이 사용할 수 있습니다.

var writer = new ArrayBufferWriter<byte>(10);

Span<byte> span = writer.GetSpan(4)[2..];
BinaryPrimitives.WriteInt32LittleEndian(span, 0x12345678);
writer.Advance(4);

writer로 부터 GetSpan()을 호출할 때 사용할 길이를 넣어주면 이 길이에 대해 예약된 공간이 부족하면 List와 동일하게 버퍼를 자동으로 확장한 뒤 Span을 반환해 줍니다. Span에 기록이 끝나면 Advance() 메서드로 포인터를 이동해 주면 됩니다. 이 조금 번거로운 과정을 간소화 하기 위해 아래와 같은 확장 메서드를 정의할 수 있습니다.

public static class ArrayBufferWriterExtension
{
    public static ReservedSpan Reserve(this ArrayBufferWriter<byte> writer, int length) 
        => new ReservedSpan(writer, length);

    public static byte[] ToArray(this ArrayBufferWriter<byte> writer) 
        => writer.WrittenSpan.ToArray();
}

public ref struct ReservedSpan
{
    private int _length;
    private ArrayBufferWriter<byte> _list;

    public ReservedSpan(ArrayBufferWriter<byte> list, int length)
    {
        _length = length;
        _list = list;
        Span = list.GetSpan(length);
    }

    public Span<byte> Span { get; }

    public static implicit operator Span<byte>(ReservedSpan reserved) => reserved.Span;

    public void Dispose() => _list.Advance(_length);
}

GetSpan(length)Advance(length)using 패턴으로 사용할 수 있도록 한 것이죠.

using(var span = writer.Reserve(4))
{
    BinaryPrimitives.WriteInt32LittleEndian(span, 0x12345678);
}

using(var span = writer.Reserve(8))
{
    BinaryPrimitives.WriteInt64LittleEndian(span, 0x1234567890ABCDEF);
}
using var span = writer.Reserve(4);

BinaryPrimitives.WriteInt32BigEndian(span, 0x12345678);

적용해 보기

자, 이렇게 ArrayBufferWriter<T>와 확장 메서드, 그리고 BinaryPrimitives를 활용해서 @이광석 님의 코드를 수정해 봤습니다.

public PacketBuilder @byte(byte data)
{
    using (var span = packetData.Reserve(sizeof(byte)))
    {
        span.Span[0] = data;
    }
    return this;
}

public PacketBuilder @bytes(IEnumerable<byte> datas)
{
    if (!(datas is byte[] b))
    {
       b = datas.ToArray();
    }
    
    packetData.Write(b);
    
    return this;
}

public PacketBuilder @string(string ascii)
{
    var length = Encoding.ASCII.GetByteCount(ascii);
    
    using var span = packetData.Reserve(length);
    Encoding.ASCII.GetBytes(ascii, span);
    
    return this;
}

public PacketBuilder @int(int value)
{
    using var span = packetData.Reserve(sizeof(int));
    
    if (_isLittleEndian)
    {
       BinaryPrimitives.WriteInt32LittleEndian(span, value);
    }
    else
    {
       BinaryPrimitives.WriteInt32BigEndian(span, value);
    }
    return this;
}

벤치마크 결과

원래 버전과 수정된 버전에 대해 아래 코드로 벤치마크 해봤습니다. Build() 메서드는 List<T>.ToArray()Span<T>.ToArray()가 거의 동일하므로 호출하지 않았습니다.
(추후 버퍼의 내용을 노출할 때도 ToArray()로 힙할당을 할 필요 없이 ReadOnlySpan<byte> 형식의 ArrayBufferWriter.WrittenSpan 속성을 반환하면 됩니다.)

var caseBinary = new PacketBuilder()
    .@byte(1)
    .@short(1)
    .@ushort(1)
    .@int(1)
    .@uint(1)
    .@long(1)
    .@ulong(1)
    .@byte(1)
    .@short(1)
    .@ushort(1)
    .@int(1)
    .@uint(1)
    .@long(1)
    .@ulong(1);

var caseString = new PacketBuilder()
    .@string(new string('A', 65));
Method Mean Error StdDev Ratio RatioSD Gen0 Allocated Alloc Ratio
OriginalLE 231.55 ns 4.42 ns 6.76 ns 1.00 0.00 0.086 544 B 1.00
SpanLE 72.41 ns 0.93 ns 0.77 ns 0.31 0.01 0.029 184 B 0.34
OriginalBE 249.45 ns 4.73 ns 9.88 ns 1.10 0.05 0.086 544 B 1.00
SpanBE 77.62 ns 1.58 ns 3.96 ns 0.35 0.02 0.029 184 B 0.34
Method Mean Error StdDev Ratio RatioSD Gen0 Allocated Alloc Ratio
OriginalString 47.69 ns 0.978 ns 2.361 ns 1.00 0.00 0.0395 248 B 1.00
SpanString 28.69 ns 0.608 ns 1.215 ns 0.60 0.04 0.0293 184 B 0.74

보시는 바와 같이 성능과 메모리 사용량 모두 3배 이상 향상되었습니다.

두 케이스 모드 버퍼 크기를 64바이트로 예약하고 코드를 실행 할 경우 개선된 버전에서는 최초 생성자 호출 시점(PacketBuilder2.ctor()) 이후에는 힙 할당이 일어나지 않음을 알 수 있습니다.

추가 벤치마크 (초기화 이후 Append 성능만 측정)

Method Mean Error StdDev Ratio RatioSD Gen0 Allocated Alloc Ratio
OriginalLE 227.91 ns 4.113 ns 3.435 ns 1.00 0.00 0.0610 384 B 1.00
SpanLE 58.52 ns 1.192 ns 1.171 ns 0.26 0.01 - - 0.00
OriginalBE 232.99 ns 1.798 ns 1.501 ns 1.02 0.02 0.0610 384 B 1.00
SpanBE 56.73 ns 0.478 ns 0.423 ns 0.25 0.00 - - 0.00
Method Mean Error StdDev Ratio Gen0 Allocated Alloc Ratio
OriginalString 35.78 ns 0.748 ns 1.458 ns 1.00 0.0140 88 B 1.00
SpanString 15.95 ns 0.137 ns 0.114 ns 0.43 - - 0.00

※ 생성자에서 byteKeyPoint_configuration 필드의 할당이 일어나서 메모리 사용량이 좀 왜곡되어 보여 초기화 및 64바이트 버퍼를 미리 예약한 된 상태에서 Append 오퍼레이션에 대해서만 벤치마크 이터레이션을 돌렸습니다.


닷넷에서 원시 형식 값을 메모리 할당 없이 버퍼에 쓰는 방법에 대해서 알아봤습니다. Span에 대해서 생소하셨던 분들에게는 조금이나마 도움이 되었으면 좋겠네요.

요즘 모던 닷넷의 트렌드처럼 Span을 적극적으로 활용해서 메모리 할당을 최소화 해 봅시다!

19 Likes

우와 감사합니다…꼼꼼히 읽어보겠습니다…!

5 Likes

우와 벤치마크까지… :+1::+1::+1:

6 Likes

TestCode

var builder1 = new PacketBuilder ()
   .AppendInt16 (1)
   .AppendInt32 (2)
   .AppendInt64 (3)
   .AppendUInt16 (4)
   .AppendUInt32 (5)
   .AppendUInt64 (6)
   .Build ();

적용 전(현재 2.0.0)
image

적용 후
image


List타입으로 패킷을 모으는 성질을 가지고 있는 PacketBuilder에만 적용하는게 좋을거 같은생각이 드네요 :slight_smile:

  • 다른 시나리오 테스트 진행
  • BIGENDIANBYTESWAPLITTLEENDIANBYTESWAP 로직 적용 테스트

이 두가지 진행 후 2.1.0버전으로 올리도록해보겠습니다!

5 Likes

Senario1 : 위 TestCode
Senario2 : Compute(Checksum)

적용 전
image

적용 후
image

2 Likes

하하 코드 리뷰가 필요할 듯 하네요 :sweat_smile:
PR로 올려보시겠습니까?

4 Likes

엇… 현재 성능개선테스트라고 브런치 따서 계속 올리고 있습니다.
@al6uiz 님이 알려주신대로 했는데 극적으로(3배 이상)이 안되서 의문이긴합니다…


아 찾았습니다…
함수 호출 할 때마다 매번 arraybufferWriter를생성했네요…ㅎㅎ 아닌데…뭐지…

2 Likes

BenchmarkDotNet 결과 중 Gen0, Allocated, Alloc Ratio 값을 살펴보시는 건 어떨까요?
힙 할당 빈번함 → 적음 → 제로 할당의 방향으로 최적화를 해가시면 될 듯 합니다.

2 Likes

네 확인해보겠습니다 :slight_smile:

1 Like

불필요한 if문을 줄이고, Swap에 대응할수 있도록

internal interface IPacketWriter {
    public static IPacketWriter LittleEndian 
        => BitConverter.IsLittleEndian ? PacketWriter.Instance : ReversePacketWriter.Instance;

    public static IPacketWriter BigEndian 
        => BitConverter.IsLittleEndian ? ReversePacketWriter.Instance : PacketWriter.Instance;
    public static IPacketWriter LittleEndianSwap
        => BitConverter.IsLittleEndian ? SwapPacketWriter.Instance : ReverseSwapPacketWriter.Instance;

    public static IPacketWriter BigEndianSwap
        => BitConverter.IsLittleEndian ? ReverseSwapPacketWriter.Instance : SwapPacketWriter.Instance;

    void @short(Span<byte> buffer, int value);
    void @int(Span<byte> buffer, int value);
    void @long(Span<byte> buffer, int value);
}

internal class PacketWriter : IPacketWriter
{
    public static PacketWriter Instance { get; } = new();

    public void @short(Span<byte> buffer, int value)
    {
        Unsafe.WriteUnaligned(ref buffer[0], value);
        //MemoryMarshal.Write(buffer, value);
    }

    public void @int(Span<byte> buffer, int value)
    {
        Unsafe.WriteUnaligned(ref buffer[0], value);
        //MemoryMarshal.Write(buffer, value);
    }

    public void @long(Span<byte> buffer, int value)
    {
        Unsafe.WriteUnaligned(ref buffer[0], value);
        //MemoryMarshal.Write(buffer, value);
    }
}

internal class ReversePacketWriter : IPacketWriter
{
    public static ReversePacketWriter Instance { get; } = new();

    public void @short(Span<byte> buffer, int value)
    {
        Unsafe.WriteUnaligned(ref buffer[0], BinaryPrimitives.ReverseEndianness(value));
        //MemoryMarshal.Write(buffer, BinaryPrimitives.ReverseEndianness(value));
    }

    public void @int(Span<byte> buffer, int value)
    {
        Unsafe.WriteUnaligned(ref buffer[0], BinaryPrimitives.ReverseEndianness(value));
        //MemoryMarshal.Write(buffer, BinaryPrimitives.ReverseEndianness(value));
    }

    public void @long(Span<byte> buffer, int value)
    {
        Unsafe.WriteUnaligned(ref buffer[0], BinaryPrimitives.ReverseEndianness(value));
        //MemoryMarshal.Write(buffer, BinaryPrimitives.ReverseEndianness(value));
    }
}
internal class SwapPacketWriter : IPacketWriter
{
    public static SwapPacketWriter Instance { get; } = new();

    public void @int(Span<byte> buffer, int value)
    {
        Unsafe.WriteUnaligned(ref buffer[0], Swap(value));
        //MemoryMarshal.Write(buffer, BinaryPrimitives.ReverseEndianness(value));
    }

    public void @long(Span<byte> buffer, int value)
    {
        Unsafe.WriteUnaligned(ref buffer[0], Swap(value));
        //MemoryMarshal.Write(buffer, BinaryPrimitives.ReverseEndianness(value));
    }

    public void @short(Span<byte> buffer, int value)
    {
        Unsafe.WriteUnaligned(ref buffer[0], Swap(value));
        //MemoryMarshal.Write(buffer, BinaryPrimitives.ReverseEndianness(value));
    }
    
    public static short Swap(short value)
    {
        return BinaryPrimitives.ReverseEndianness(value);
    }

    public static int Swap(int value)
    {
        return unchecked(((value & (int)0xFF00FF00) >> 8) + ((value & 0x00FF00FF) << 8));
    }

    public static long Swap(long value)
    {
        return unchecked(((value & (long)0xFF00FF00FF00FF00) >> 8) + ((value & 0x00FF00FF00FF00FF) << 8));
    }
}

internal class ReverseSwapPacketWriter : IPacketWriter
{
    public static ReverseSwapPacketWriter Instance { get; } = new();

    public void @int(Span<byte> buffer, int value)
    {
        Unsafe.WriteUnaligned(ref buffer[0], ReverseSwap(value));
        //MemoryMarshal.Write(buffer, BinaryPrimitives.ReverseEndianness(value));
    }

    public void @long(Span<byte> buffer, int value)
    {
        Unsafe.WriteUnaligned(ref buffer[0], ReverseSwap(value));
        //MemoryMarshal.Write(buffer, BinaryPrimitives.ReverseEndianness(value));
    }

    public void @short(Span<byte> buffer, int value)
    {
        Unsafe.WriteUnaligned(ref buffer[0], ReverseSwap(value));
        //MemoryMarshal.Write(buffer, BinaryPrimitives.ReverseEndianness(value));
    }
    
    public static short ReverseSwap(short value)
    {
        return value;
    }

    public static int ReverseSwap(int value)
    {
        return (int)BitOperations.RotateLeft((uint)value, 16);
    }

    public static long ReverseSwap(long value)
    {
        return ((long)BitOperations.RotateLeft((uint)value, 16) << 32) + BitOperations.RotateLeft((uint)(value >> 32), 16);
    }
}

이와 같이 각 Endain에 대한 Writer를 생성하고


    private readonly IPacketWriter writer = endian switch {
        Endian.BIG => IPacketWriter.BigEndian,
        Endian.LITTLE => IPacketWriter.LittleEndian,
        Endian.BIGBYTESWAP => IPacketWriter.BigEndianSwap,
        Endian.LITTLEBYTESWAP => IPacketWriter.LittleEndianSwap,
        _ => PacketWriter.Instance,
    };
    
    public PacketBuilder @short(short value)
    {
        using var span = packetData.Reserve(sizeof(int));
        writer.@int(span, value);
        return this;
    }

    public PacketBuilder @int(int value)
    {
        using var span = packetData.Reserve(sizeof(int));
        writer.@int(span, value);
        return this;
    }

    public PacketBuilder @long(long value)
    {
        using var span = packetData.Reserve(sizeof(long));
        writer.@int(span, value);
        return this;
    }

최초에 엔디안에 해당하는 Writer를 초기화하는 형식은 어떨련지요?

2 Likes
public PacketBuilder @bytes(IEnumerable<byte> datas)
{
    switch (datas)
    {
    case byte[] arr:
        packetData.Write(arr);
        return this;
    case ImmutableArray<byte> arr:
        packetData.Write(arr.AsSpan());
        return this;
    case List<byte> list:
        packetData.Write(CollectionsMarshal.AsSpan(list));
        return this;
    case FrozenSet<byte> set:
        set.CopyTo(packetData.GetSpan(set.Count));
        packetData.Advance(set.Count);
        return this;
    default:
        packetData.Write(datas.ToArray());
        return this;
    }
}

그리고 다른 컬렉션에 대한 처리도 추가하면 좋을 것 같네요.

2 Likes

1. LINQ 사용 지양

Append에서 최적화 된 성능을 GetBytes() 메서드에서 ToArray() + LINQ + ToArray()으로 다 깎아먹고 있습니다.

    private byte[] GetBytes(int start)
        => this.packetData.ToArray().Skip(start).ToArray ();
    
    private byte[] GetBytes(int start, int count)
        => this.packetData.ToArray().Skip (start).Take (count).ToArray ();

Span<T>버퍼로부터 원하는 부분을 취할때는 Slice() 메소드를 사용하시면 됩니다.

private byte[] GetBytes(int start)
    => this.packetData.WrittenSpan.Slice(start).ToArray ();

private byte[] GetBytes(int start, int count)
    => this.packetData.WrittenSpan.Slice(start, count).ToArray ();

2. Mythosia 라이브러리의 설계 방향성

해당 라이브러리는 Span을 지원하지 않고 성능보다는 범용성에 초점을 둔 것 같습니다. 입력 자체를 IEnumerable 시작하고 있기 때문에 내부적으로 반복문에 대한 오버헤드가 큽니다.
CRC32.Compute() 메서드의 구현입니다.

IEnumerable 형식에 대한 Count() 메서드와 ElementAt() 메서드가 IList<T> 형식에 대해 최적화되어 있다고 하지만 동일한 크기의 byte[]를 입력으로 IEnumerable<byte> 방식과 직접 for 루프를 수행할 때의 성능 비교해보면 아래와 같습니다.

public static uint Test1Array(byte[] data)
{
    long result = 0;
    if (data.Count() <= 0) return (uint)result;

    uint crc = 0xffffffff;
    for (int i = 0; i < data.Count(); i++)
    {
        var c = data.ElementAt(i);
        crc = (crc >> 8);
    }

    return ~crc; //(crc ^ (-1)) >> 0;
}

public static uint Test2Array(byte[] data)
{
    long result = 0;
    if (data.Legnth <= 0) return (uint)result;

    uint crc = 0xffffffff;
    for (int i = 0; i < data.Length; i++)
    {
        var c = data[i];
        crc = (crc >> 8);
    }

    return ~crc; //(crc ^ (-1)) >> 0;
}
Method Mean Error StdDev Allocated
Test1Array 569.56 ns 9.709 ns 8.607 ns -
Test2Array 14.87 ns 0.200 ns 0.177 ns -
Test1List 292.47 ns 1.414 ns 1.253 ns -
Test2List 20.36 ns 0.122 ns 0.108 ns -

이미 여기서 상당한 시간을 소모하기고 있기 떄문에 PacketBuilder의 성능에 대한 벤치마크가 무의미해지죠.

결과 값 반환 또한 List<byte> 형식으로 하고 있기 때문에 현재 구현에서는 결과를 가져오는 시점에 ToArray()에 의해 재차 힙 할당이 발생하게 됩니다.

해당 라이브러리 참고하셔서 Span을 고려한 구현을 직접 해보시는게 어떻까 합니다ㅎ

4 Likes

한번 확인해보겠습니다 :slight_smile:

어쩔수없이 가져오는 방안을 고려해야되겠군요.

1 Like

CRC16을 Span<byte>로 구현했을 때 초기화 및 Build() 메서드 호출을 제외한 성능 비교 입니다.

BenchmarkDotNet=v0.13.1, OS=Windows 10.0.22631
Intel Core i7-10750H CPU 2.60GHz, 1 CPU, 12 logical and 6 physical cores
.NET SDK=8.0.200
  [Host]     : .NET 7.0.16 (7.0.1624.6629), X64 RyuJIT
  DefaultJob : .NET 7.0.16 (7.0.1624.6629), X64 RyuJIT
Method Mean Error StdDev Ratio RatioSD Gen 0 Allocated
Scenario2 573.2 ns 8.82 ns 7.37 ns 2.47 0.03 0.0381 240 B
Scenario2Direct 231.9 ns 1.20 ns 0.93 ns 1.00 0.00 - -
public class CRC
{ 
    public static ushort ComputeCRC16(ReadOnlySpan<byte> data)
    {
        ushort result = 0;
        if (data.Length <= 0) return result;

        for (int i = 0; i < data.Length; i++)
        {
            result = UpdateCRC16(result, data[i]);
        }

        return result;
    }

    private static ushort UpdateCRC16(ushort crc, byte c)
    {
        var short_c = (ushort)(0x00ff & c);

        var tmp = (ushort)(crc ^ short_c);
        return (ushort)((crc >> 8) ^ crc_tab16[tmp & 0xff]);
    }

    private static ushort[] crc_tab16 = GenerateCRC16Table();

    private static ushort[] GenerateCRC16Table()
    {
        const ushort P_16 = 0xA001;
        var crc_tab16 = new ushort[256];

        for (int i = 0; i < 256; i++)
        {
            ushort crc = 0;
            ushort c = (ushort)i;

            for (int j = 0; j < 8; j++)
            {
                if (((crc ^ c) & 0x0001) != 0) crc = (ushort)((crc >> 1) ^ P_16);
                else crc = (ushort)(crc >> 1);

                c = (ushort)(c >> 1);
            }

            crc_tab16[i] = crc;
        }

        return crc_tab16;
    }
}
public PacketBuilder Compute2(string key, CRC16Type type)
{
    if (this.packetData.WrittenCount == 0)
        return this;
    if (!byteKeyPoint.TryGetValue(key, out var range))
        return this;

    var span = packetData.WrittenSpan.Slice(range.start, range.count);
    @ushort(CRC.ComputeCRC16(span));
    return this;
}
3 Likes

CollectionsMarshal 의 경우 core 5.0이지 않나요?

현재 .net standard2.1 기준인지라…


제안해주신 로직 적용하였습니다.
정상동작은 되긴하나… 작성해주신 코드 문법이 9.0이상인지라…일부 하드코딩을진행하였습니다…

2 Likes
    public interface IPacketWriter
    {
        void @short(ReservedSpan span, short value);
        void @int(ReservedSpan span, int value);
        void @long(ReservedSpan span, long value);
        void @ushort(ReservedSpan span, ushort value);
        void @uint(ReservedSpan span, uint value);
        void @ulong(ReservedSpan span, ulong value);
    }

    public class PacketWriter : IPacketWriter
    {
        public static PacketWriter Instance { get; } = new PacketWriter ();

        public void @short(ReservedSpan span, short value)
        {
            MemoryMarshal.Write(span, ref value); 
        }

        public void @int(ReservedSpan span, int value)
        {
            MemoryMarshal.Write(span, ref value);
        }

        public void @long(ReservedSpan span, long value)
        {
            MemoryMarshal.Write(span, ref value);
        }

        public void @ushort(ReservedSpan span, ushort value)
        {
            MemoryMarshal.Write(span, ref value);
        }

        public void @uint(ReservedSpan span, uint value)
        {
            MemoryMarshal.Write(span, ref value);
        }

        public void @ulong(ReservedSpan span, ulong value)
        {
            MemoryMarshal.Write(span, ref value);
        }
    }

    public class ReversePacketWriter : IPacketWriter
    {
        public static ReversePacketWriter Instance { get; } = new ReversePacketWriter ();
        public void @short(ReservedSpan span, short value)
        {
            value = BinaryPrimitives.ReverseEndianness(value);
            MemoryMarshal.Write(span, ref value);
        }

        public void @int(ReservedSpan span, int value)
        {
            value = BinaryPrimitives.ReverseEndianness(value);
            MemoryMarshal.Write(span, ref value);
        }

        public void @long(ReservedSpan span, long value)
        {
            value = BinaryPrimitives.ReverseEndianness(value);
            MemoryMarshal.Write(span, ref value);
        }

        public void @ushort(ReservedSpan span, ushort value)
        {
            value = BinaryPrimitives.ReverseEndianness(value);
            MemoryMarshal.Write(span, ref value);
        }

        public void @uint(ReservedSpan span, uint value)
        {
            value = BinaryPrimitives.ReverseEndianness(value);
            MemoryMarshal.Write(span, ref value);
        }

        public void @ulong(ReservedSpan span, ulong value)
        {
            value = BinaryPrimitives.ReverseEndianness(value);
            MemoryMarshal.Write(span, ref value);
        }
    }
    public class SwapPacketWriter : IPacketWriter
    {
        public static SwapPacketWriter Instance { get; } = new SwapPacketWriter ();

        public void @short(ReservedSpan span, short value)
        {
            value = Swap(value);
            MemoryMarshal.Write(span, ref value);
        }

        public void @int(ReservedSpan span, int value)
        {
            value = Swap(value);
            MemoryMarshal.Write(span, ref value);
        }

        public void @long(ReservedSpan span, long value)
        {
            value = Swap(value);
            MemoryMarshal.Write(span, ref value);
        }

        public void @ushort(ReservedSpan span, ushort value)
        {
            value = Swap(value);
            MemoryMarshal.Write(span, ref value);
        }

        public void @uint(ReservedSpan span, uint value)
        {
            value = Swap(value);
            MemoryMarshal.Write(span, ref value);
        }

        public void @ulong(ReservedSpan span, ulong value)
        {
            value = Swap(value);
            MemoryMarshal.Write(span, ref value);
        }

        public static short Swap(short value)
        {
            return BinaryPrimitives.ReverseEndianness (value);
        }
        public static ushort Swap(ushort value)
        {
            return BinaryPrimitives.ReverseEndianness (value);
        }

        public static int Swap(int value)
        {
            return unchecked(((value & (int)0xFF00FF00) >> 8) + ((value & 0x00FF00FF) << 8));
        }
        public static uint Swap(uint value)
        {
            return unchecked(((value & (uint)0xFF00FF00) >> 8) + ((value & 0x00FF00FF) << 8));
        }
        public static long Swap(long value)
        {
            return unchecked(((value & (long)0xFF00FF00FF00FF00) >> 8) + ((value & 0x00FF00FF00FF00FF) << 8));
        }

        public static ulong Swap(ulong value)
        {
            return unchecked(((value & (ulong)0xFF00FF00FF00FF00) >> 8) + ((value & 0x00FF00FF00FF00FF) << 8));
        }
    }

    public class ReverseSwapPacketWriter : IPacketWriter
    {
        public static ReverseSwapPacketWriter Instance { get; } = new ReverseSwapPacketWriter();

        public void @short(ReservedSpan span, short value)
        {
            value = ReverseSwap(value);
            MemoryMarshal.Write(span, ref value);
        }

        public void @int(ReservedSpan span, int value)
        {
            value = ReverseSwap(value);
            MemoryMarshal.Write(span, ref value);
        }

        public void @long(ReservedSpan span, long value)
        {
            value = ReverseSwap(value);
            MemoryMarshal.Write(span, ref value);
        }
        public void @ushort(ReservedSpan span, ushort value)
        {
            value = ReverseSwap(value);
            MemoryMarshal.Write(span, ref value);
        }

        public void @uint(ReservedSpan span, uint value)
        {
            value = ReverseSwap(value);
            MemoryMarshal.Write(span, ref value);
        }

        public void @ulong(ReservedSpan span, ulong value)
        {
            value = ReverseSwap(value);
            MemoryMarshal.Write(span, ref value);
        }

        public static short ReverseSwap(short value)
        {
            return value;
        }

        public static ushort ReverseSwap(ushort value)
        {
            return value;
        }

        public static int ReverseSwap(int value)
        {
            return (value << 16) | (value >> 16);
        }

        public static uint ReverseSwap(uint value)
        {
            return (value << 16) | (value >> 16);
        }

        public static long ReverseSwap(long value)
        {
            return BinaryPrimitives.ReverseEndianness(unchecked(((value & (long)0xFF00FF00FF00FF00) >> 8) + ((value & 0x00FF00FF00FF00FF) << 8)));
        }

        public static ulong ReverseSwap(ulong value)
        {
            return BinaryPrimitives.ReverseEndianness(unchecked(((value & 0xFF00FF00FF00FF00) >> 8) + ((value & 0x00FF00FF00FF00FF) << 8)));
        }
    }

아 버전을 확인하지 못했네요.
.NET Standard 2.1 기준으로 위 코드 처럼 변경하시면 됩니다.

3 Likes

다음번에
PR로 올려주세요!

그래야 해당 라이브러리에 컨트리뷰터로 기여할 수 있으니…!

1 Like

chatgpt의 도움으로 하드코딩되었던 부분도
제시해주신것과 비슷하게 구현해놨습니다!

성능적인 면에서 크게 차이가 없기 때문에
Nuget 업데이트를 진행하지 않겠습니다!

1 Like

현재

Compute 부분 이관작업중인데…와우…

1 Like