netserializer icon indicating copy to clipboard operation
netserializer copied to clipboard

Improve float serialization, add .NET 5 Half support.

Open PJB3005 opened this issue 4 years ago • 2 comments

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>

PJB3005 avatar Nov 21 '20 01:11 PJB3005

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.

tomba avatar Nov 29 '20 09:11 tomba

Any plan to release new version of NetSerializer with .NET 5.0 and higher? Do you have any timeline?

tjs-shah avatar Aug 04 '21 14:08 tjs-shah