netserializer
netserializer copied to clipboard
Improve float serialization, add .NET 5 Half support.
Apologies for doing this all in one PR. Doing it separately would've produced three conflicting PRs which is even less ideal.
Optimized float serialization to be faster and write less bytes in almost all cases. Add .NET 5 Half
support. See commits for details.
Benchmarks for writing/reading code mentioned
BenchmarkDotNet=v0.12.1, OS=arch
Intel Core i7-8750H CPU 2.20GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores
.NET Core SDK=5.0.100
[Host] : .NET Core 5.0.0 (CoreCLR 5.0.20.51904, CoreFX 5.0.20.51904), X64 RyuJIT
DefaultJob : .NET Core 5.0.0 (CoreCLR 5.0.20.51904, CoreFX 5.0.20.51904), X64 RyuJIT
Method | Mean | Error | StdDev | Median |
---|---|---|---|---|
BenchWrite16Span | 13.65 ns | 0.305 ns | 0.775 ns | 13.38 ns |
BenchWrite32Span | 13.65 ns | 0.172 ns | 0.152 ns | 13.62 ns |
BenchWrite64Span | 13.09 ns | 0.278 ns | 0.298 ns | 13.02 ns |
BenchRead16Span | 13.52 ns | 0.250 ns | 0.267 ns | 13.43 ns |
BenchRead32Span | 13.32 ns | 0.287 ns | 0.255 ns | 13.36 ns |
BenchRead64Span | 12.57 ns | 0.175 ns | 0.164 ns | 12.49 ns |
BenchWrite16Byte | 10.28 ns | 0.227 ns | 0.279 ns | 10.24 ns |
BenchWrite32Byte | 16.89 ns | 0.360 ns | 0.400 ns | 16.98 ns |
BenchWrite64Byte | 30.58 ns | 0.639 ns | 1.261 ns | 30.42 ns |
BenchRead16Byte | 11.30 ns | 0.238 ns | 0.274 ns | 11.26 ns |
BenchRead32Byte | 16.09 ns | 0.342 ns | 0.267 ns | 16.10 ns |
BenchRead64Byte | 29.17 ns | 0.630 ns | 1.836 ns | 28.89 ns |
Span code is used on .NET Core/.NET 5 for 32/64 bit since it's faster.
Benchmark code:
using System;
using System.Buffers.Binary;
using System.IO;
using BenchmarkDotNet.Attributes;
namespace Content.Benchmarks
{
[SimpleJob]
public class NetSerializerIntBenchmark
{
private MemoryStream _writeStream;
private MemoryStream _readStream;
private ushort _x16 = 5;
private uint _x32 = 5;
private ulong _x64 = 5;
private ushort _read16;
private uint _read32;
private ulong _read64;
[GlobalSetup]
public void Setup()
{
_writeStream = new MemoryStream(64);
_readStream = new MemoryStream();
_readStream.Write(new byte[]{0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8});
}
[Benchmark]
public void BenchWrite16Span()
{
_writeStream.Position = 0;
WriteUInt16Span(_writeStream, _x16);
}
[Benchmark]
public void BenchWrite32Span()
{
_writeStream.Position = 0;
WriteUInt32Span(_writeStream, _x32);
}
[Benchmark]
public void BenchWrite64Span()
{
_writeStream.Position = 0;
WriteUInt64Span(_writeStream, _x64);
}
[Benchmark]
public void BenchRead16Span()
{
_readStream.Position = 0;
_read16 = ReadUInt16Span(_readStream);
}
[Benchmark]
public void BenchRead32Span()
{
_readStream.Position = 0;
_read32 = ReadUInt32Span(_readStream);
}
[Benchmark]
public void BenchRead64Span()
{
_readStream.Position = 0;
_read64 = ReadUInt64Span(_readStream);
}
[Benchmark]
public void BenchWrite16Byte()
{
_writeStream.Position = 0;
WriteUInt16Byte(_writeStream, _x16);
}
[Benchmark]
public void BenchWrite32Byte()
{
_writeStream.Position = 0;
WriteUInt32Byte(_writeStream, _x32);
}
[Benchmark]
public void BenchWrite64Byte()
{
_writeStream.Position = 0;
WriteUInt64Byte(_writeStream, _x64);
}
[Benchmark]
public void BenchRead16Byte()
{
_readStream.Position = 0;
_read16 = ReadUInt16Byte(_readStream);
}
[Benchmark]
public void BenchRead32Byte()
{
_readStream.Position = 0;
_read32 = ReadUInt32Byte(_readStream);
}
[Benchmark]
public void BenchRead64Byte()
{
_readStream.Position = 0;
_read64 = ReadUInt64Byte(_readStream);
}
private static void WriteUInt16Byte(Stream stream, ushort value)
{
stream.WriteByte((byte) value);
stream.WriteByte((byte) (value >> 8));
}
private static void WriteUInt32Byte(Stream stream, uint value)
{
stream.WriteByte((byte) value);
stream.WriteByte((byte) (value >> 8));
stream.WriteByte((byte) (value >> 16));
stream.WriteByte((byte) (value >> 24));
}
private static void WriteUInt64Byte(Stream stream, ulong value)
{
stream.WriteByte((byte) value);
stream.WriteByte((byte) (value >> 8));
stream.WriteByte((byte) (value >> 16));
stream.WriteByte((byte) (value >> 24));
stream.WriteByte((byte) (value >> 32));
stream.WriteByte((byte) (value >> 40));
stream.WriteByte((byte) (value >> 48));
stream.WriteByte((byte) (value >> 56));
}
private static ushort ReadUInt16Byte(Stream stream)
{
ushort a = 0;
for (var i = 0; i < 16; i += 8)
{
var val = stream.ReadByte();
if (val == -1)
throw new EndOfStreamException();
a |= (ushort) (val << i);
}
return a;
}
private static uint ReadUInt32Byte(Stream stream)
{
uint a = 0;
for (var i = 0; i < 32; i += 8)
{
var val = stream.ReadByte();
if (val == -1)
throw new EndOfStreamException();
a |= (uint) (val << i);
}
return a;
}
private static ulong ReadUInt64Byte(Stream stream)
{
ulong a = 0;
for (var i = 0; i < 64; i += 8)
{
var val = stream.ReadByte();
if (val == -1)
throw new EndOfStreamException();
a |= (ulong) (val << i);
}
return a;
}
private static void WriteUInt16Span(Stream stream, ushort value)
{
Span<byte> buf = stackalloc byte[2];
BinaryPrimitives.WriteUInt16LittleEndian(buf, value);
stream.Write(buf);
}
private static void WriteUInt32Span(Stream stream, uint value)
{
Span<byte> buf = stackalloc byte[4];
BinaryPrimitives.WriteUInt32LittleEndian(buf, value);
stream.Write(buf);
}
private static void WriteUInt64Span(Stream stream, ulong value)
{
Span<byte> buf = stackalloc byte[8];
BinaryPrimitives.WriteUInt64LittleEndian(buf, value);
stream.Write(buf);
}
private static ushort ReadUInt16Span(Stream stream)
{
Span<byte> buf = stackalloc byte[2];
var wSpan = buf;
while (true)
{
var read = stream.Read(wSpan);
if (read == 0)
throw new EndOfStreamException();
if (read == wSpan.Length)
break;
wSpan = wSpan[read..];
}
return BinaryPrimitives.ReadUInt16LittleEndian(buf);
}
private static uint ReadUInt32Span(Stream stream)
{
Span<byte> buf = stackalloc byte[4];
var wSpan = buf;
while (true)
{
var read = stream.Read(wSpan);
if (read == 0)
throw new EndOfStreamException();
if (read == wSpan.Length)
break;
wSpan = wSpan[read..];
}
return BinaryPrimitives.ReadUInt32LittleEndian(buf);
}
private static ulong ReadUInt64Span(Stream stream)
{
Span<byte> buf = stackalloc byte[8];
var wSpan = buf;
while (true)
{
var read = stream.Read(wSpan);
if (read == 0)
throw new EndOfStreamException();
if (read == wSpan.Length)
break;
wSpan = wSpan[read..];
}
return BinaryPrimitives.ReadUInt64LittleEndian(buf);
}
}
}
Proof that compressed int writing for floats does not help
[Test]
public void TestAllHalf()
{
ushort i = 0;
var ms = new MemoryStream();
do
{
var half = Unsafe.As<ushort, Half>(ref i);
if (Half.IsNormal(half) || Half.IsInfinity(half) || Half.IsNaN(half))
{
Primitives.WritePrimitive(ms, half);
Assert.That(ms.Position != 1, $"Failed: {i}");
ms.Position = 0;
}
i += 1;
} while (i != ushort.MaxValue);
}
[Test]
public void TestAllFloat()
{
Assert.Multiple(() =>
{
uint i = 0;
var ms = new MemoryStream();
do
{
var single = Unsafe.As<uint, float>(ref i);
if (float.IsNormal(single) || float.IsInfinity(single) || float.IsNaN(single))
{
Primitives.WritePrimitive(ms, single);
if (ms.Position < 3)
{
Assert.Fail($"Failed: {i}");
}
ms.Position = 0;
}
i += 1;
} while (i != uint.MaxValue);
});
}
```cs
</details>
The "Fix indentation" looks fine, but please don't add "fix an earlier commit in this pull request" commits to a pull request. Just fix the earlier commits, and push the new branch over the old pull request. Or if there are a lot of changes, perhaps create a new pull request.
Github is broken and doesn't support the above model properly, but I don't want such fixes merged. They just make the history more confusing, and in some cases (not here, though) break bisect.
Any plan to release new version of NetSerializer with .NET 5.0 and higher? Do you have any timeline?