Skip to content

Commit 46ba9bd

Browse files
committed
serialization benchmarks
1 parent c6e9661 commit 46ba9bd

File tree

5 files changed

+262
-33
lines changed

5 files changed

+262
-33
lines changed

.github/workflows/publish.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ on:
66
description: 'Publish Nuget.org ?'
77
type: boolean
88
required: false
9-
default: false
9+
default: true
1010

1111
release_github:
1212
description: 'Publish GitHub Packages ?'
1313
type: boolean
1414
required: false
15-
default: true
15+
default: false
1616

1717
env:
1818
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true

benchmarks/Backdash.Benchmarks/Backdash.Benchmarks.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
</PropertyGroup>
1010
<ItemGroup>
1111
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
12+
<PackageReference Include="MemoryPack" Version="1.21.4" />
1213

1314
</ItemGroup>
1415
<ItemGroup>
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
// ReSharper disable UnassignedField.Global, NonReadonlyMemberInGetHashCode
2+
3+
#pragma warning disable S2328, S4035
4+
5+
using System.Buffers;
6+
using System.Diagnostics;
7+
using System.Runtime.InteropServices;
8+
using System.Text.Json;
9+
using System.Text.Json.Serialization;
10+
using Backdash.Core;
11+
using Backdash.Data;
12+
using Backdash.Network;
13+
using Backdash.Serialization;
14+
using MemoryPack;
15+
16+
namespace Backdash.Benchmarks.Cases;
17+
18+
[RPlotExporter]
19+
[InProcess, MemoryDiagnoser, RankColumn]
20+
public class SerializationBenchmark
21+
{
22+
TestData data = null!;
23+
TestData result = null!;
24+
25+
readonly ArrayBufferWriter<byte> buffer = new((int)ByteSize.FromMebiBytes(10).ByteCount);
26+
27+
Utf8JsonWriter jsonWriter = null!;
28+
29+
readonly JsonSerializerOptions jsonOptions = new()
30+
{
31+
PreferredObjectCreationHandling = JsonObjectCreationHandling.Populate,
32+
WriteIndented = false,
33+
IncludeFields = true,
34+
};
35+
36+
static TestData NewTestData(Random random)
37+
{
38+
TestData testData = new()
39+
{
40+
Field1 = random.NextBool(),
41+
Field2 = random.Next<ulong>(),
42+
};
43+
44+
for (int i = 0; i < testData.Field3.Length; i++)
45+
{
46+
ref var entry = ref testData.Field3[i];
47+
entry.Field1 = random.Next();
48+
entry.Field2 = random.Next<uint>();
49+
entry.Field3 = random.Next<ulong>();
50+
entry.Field4 = random.Next<long>();
51+
entry.Field5 = random.Next<short>();
52+
entry.Field6 = random.Next<ushort>();
53+
entry.Field7 = random.Next<byte>();
54+
entry.Field8 = random.Next<sbyte>();
55+
random.Next(entry.Field9.AsSpan());
56+
}
57+
58+
return testData;
59+
}
60+
61+
[GlobalSetup]
62+
public void Setup()
63+
{
64+
Random random = new(42);
65+
data = NewTestData(random);
66+
jsonWriter = new(buffer);
67+
}
68+
69+
[IterationSetup]
70+
public void BeforeEach()
71+
{
72+
buffer.Clear();
73+
jsonWriter.Reset();
74+
result = new();
75+
}
76+
77+
[Benchmark]
78+
public void Backdash()
79+
{
80+
var writer = new BinaryBufferWriter(buffer, Endianness.LittleEndian);
81+
writer.Write(data);
82+
int offset = 0;
83+
var reader = new BinaryBufferReader(buffer.WrittenSpan, ref offset, Endianness.LittleEndian);
84+
reader.Read(result);
85+
Debug.Assert(data == result);
86+
}
87+
88+
[Benchmark]
89+
public void MemoryPack()
90+
{
91+
MemoryPackSerializer.Serialize(buffer, data);
92+
MemoryPackSerializer.Deserialize(buffer.WrittenSpan, ref result!);
93+
Debug.Assert(data == result);
94+
}
95+
96+
[Benchmark]
97+
public void SystemJson()
98+
{
99+
JsonSerializer.Serialize(jsonWriter, data, jsonOptions);
100+
Utf8JsonReader reader = new(buffer.WrittenSpan);
101+
result = JsonSerializer.Deserialize<TestData>(ref reader, jsonOptions)!;
102+
Debug.Assert(data == result);
103+
}
104+
}
105+
106+
[MemoryPackable]
107+
public sealed partial class TestData : IBinarySerializable, IEquatable<TestData>
108+
{
109+
public bool Field1;
110+
public ulong Field2;
111+
public TestEntryData[] Field3;
112+
113+
public TestData()
114+
{
115+
Field3 = new TestEntryData[1_000];
116+
for (var i = 0; i < Field3.Length; i++)
117+
Field3[i].Field9 = new int[10_000];
118+
}
119+
120+
public void Serialize(ref readonly BinaryBufferWriter writer)
121+
{
122+
writer.Write(in Field1);
123+
writer.Write(in Field2);
124+
writer.Write(in Field3);
125+
}
126+
127+
public void Deserialize(ref readonly BinaryBufferReader reader)
128+
{
129+
reader.Read(ref Field1);
130+
reader.Read(ref Field2);
131+
reader.Read(in Field3);
132+
}
133+
134+
public override int GetHashCode() => throw new InvalidOperationException();
135+
136+
public bool Equals(TestData? other) => Equals(this, other);
137+
public override bool Equals(object? obj) => ReferenceEquals(this, obj) || (obj is TestData other && Equals(other));
138+
139+
public static bool Equals(TestData? left, TestData? right)
140+
{
141+
if (ReferenceEquals(left, right)) return true;
142+
if (left is null || right is null) return false;
143+
144+
return left.Field1 == right.Field1
145+
&& left.Field2 == right.Field2
146+
&& left.Field3.AsSpan().SequenceEqual(right.Field3);
147+
}
148+
149+
public static bool operator ==(TestData? left, TestData? right) => Equals(left, right);
150+
public static bool operator !=(TestData? left, TestData? right) => !Equals(left, right);
151+
}
152+
153+
[MemoryPackable]
154+
public partial struct TestEntryData() : IBinarySerializable, IEquatable<TestEntryData>
155+
{
156+
public int Field1;
157+
public uint Field2;
158+
public ulong Field3;
159+
public long Field4;
160+
public short Field5;
161+
public ushort Field6;
162+
public byte Field7;
163+
public sbyte Field8;
164+
public int[] Field9 = [];
165+
166+
public readonly void Serialize(ref readonly BinaryBufferWriter writer)
167+
{
168+
writer.Write(in Field1);
169+
writer.Write(in Field2);
170+
writer.Write(in Field3);
171+
writer.Write(in Field4);
172+
writer.Write(in Field5);
173+
writer.Write(in Field6);
174+
writer.Write(in Field7);
175+
writer.Write(in Field8);
176+
writer.Write(Field9);
177+
}
178+
179+
public void Deserialize(ref readonly BinaryBufferReader reader)
180+
{
181+
Field1 = reader.ReadInt32();
182+
Field2 = reader.ReadUInt32();
183+
Field3 = reader.ReadUInt64();
184+
Field4 = reader.ReadInt64();
185+
Field5 = reader.ReadInt16();
186+
Field6 = reader.ReadUInt16();
187+
Field7 = reader.ReadByte();
188+
Field8 = reader.ReadSByte();
189+
reader.Read(Field9);
190+
}
191+
192+
public override readonly int GetHashCode() => throw new InvalidOperationException();
193+
194+
public readonly bool Equals(TestEntryData other) => Equals(in this, in other);
195+
public override readonly bool Equals(object? obj) => obj is TestEntryData other && Equals(in this, in other);
196+
197+
public static bool Equals(in TestEntryData left, in TestEntryData right) =>
198+
left.Field1 == right.Field1 &&
199+
left.Field2 == right.Field2 &&
200+
left.Field3 == right.Field3 &&
201+
left.Field4 == right.Field4 &&
202+
left.Field5 == right.Field5 &&
203+
left.Field6 == right.Field6 &&
204+
left.Field7 == right.Field7 &&
205+
left.Field8 == right.Field8 &&
206+
left.Field9.AsSpan().SequenceEqual(right.Field9);
207+
208+
public static bool operator ==(TestEntryData left, TestEntryData right) => Equals(in left, in right);
209+
public static bool operator !=(TestEntryData left, TestEntryData right) => !Equals(in left, in right);
210+
}
211+
212+
public static class Extensions
213+
{
214+
public static T Next<T>(this Random random) where T : unmanaged
215+
{
216+
var result = new T();
217+
var bytes = Mem.AsBytes(ref result);
218+
random.NextBytes(bytes);
219+
return result;
220+
}
221+
222+
public static void Next<T>(this Random random, Span<T> buffer) where T : unmanaged =>
223+
random.NextBytes(MemoryMarshal.AsBytes(buffer));
224+
225+
public static bool NextBool(this Random random) => random.Next(0, 2) == 1;
226+
}

src/Backdash/Core/Mem.cs

Lines changed: 30 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -130,39 +130,41 @@ public static int PopCount<T>(in ReadOnlySpan<T> values) where T : unmanaged
130130
var bytes = MemoryMarshal.AsBytes(values);
131131
var index = 0;
132132
var count = 0;
133+
133134
while (index < bytes.Length)
134135
{
135136
var remaining = bytes[index..];
136137

137-
if (remaining.Length >= sizeof(ulong))
138-
{
139-
var value = MemoryMarshal.Read<ulong>(remaining[..sizeof(ulong)]);
140-
index += sizeof(ulong);
141-
count += BitOperations.PopCount(value);
142-
continue;
143-
}
144-
145-
if (remaining.Length >= sizeof(uint))
146-
{
147-
var value = MemoryMarshal.Read<uint>(remaining[..sizeof(uint)]);
148-
index += sizeof(uint);
149-
count += BitOperations.PopCount(value);
150-
continue;
151-
}
152-
153-
if (remaining.Length >= sizeof(ushort))
154-
{
155-
var value = MemoryMarshal.Read<ushort>(remaining[..sizeof(ushort)]);
156-
index += sizeof(ushort);
157-
count += ushort.PopCount(value);
158-
continue;
159-
}
160-
161-
if (remaining.Length >= sizeof(byte))
138+
switch (remaining.Length)
162139
{
163-
var value = remaining[0];
164-
index += sizeof(byte);
165-
count += byte.PopCount(value);
140+
case >= sizeof(ulong):
141+
{
142+
var value = MemoryMarshal.Read<ulong>(remaining[..sizeof(ulong)]);
143+
index += sizeof(ulong);
144+
count += BitOperations.PopCount(value);
145+
continue;
146+
}
147+
case >= sizeof(uint):
148+
{
149+
var value = MemoryMarshal.Read<uint>(remaining[..sizeof(uint)]);
150+
index += sizeof(uint);
151+
count += BitOperations.PopCount(value);
152+
continue;
153+
}
154+
case >= sizeof(ushort):
155+
{
156+
var value = MemoryMarshal.Read<ushort>(remaining[..sizeof(ushort)]);
157+
index += sizeof(ushort);
158+
count += ushort.PopCount(value);
159+
continue;
160+
}
161+
case >= sizeof(byte):
162+
{
163+
var value = remaining[0];
164+
index += sizeof(byte);
165+
count += byte.PopCount(value);
166+
break;
167+
}
166168
}
167169
}
168170

src/Backdash/Serialization/BinaryBufferReader.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public BinaryBufferReader(ReadOnlySpan<byte> buffer, ref int offset, Endianness
5757
public void Advance(int count) => offset += count;
5858

5959
[MethodImpl(MethodImplOptions.AggressiveInlining)]
60-
Span<T> GetListSpan<T>(in List<T> values) where T : unmanaged
60+
Span<T> GetListSpan<T>(in List<T> values) where T : struct
6161
{
6262
var count = ReadInt32();
6363
CollectionsMarshal.SetCount(values, count);
@@ -429,11 +429,11 @@ public void Read<T>(in Span<T> values) where T : IBinarySerializable
429429
/// <summary>Reads an array of <see cref="IBinarySerializable"/> <paramref name="values"/> from buffer.</summary>
430430
/// <typeparam name="T">A type that implements <see cref="IBinarySerializable"/>.</typeparam>
431431
[MethodImpl(MethodImplOptions.AggressiveInlining)]
432-
public void Read<T>(in T[] values) where T : unmanaged, IBinarySerializable => Read(values.AsSpan());
432+
public void Read<T>(in T[] values) where T : struct, IBinarySerializable => Read(values.AsSpan());
433433

434434
/// <summary>Reads an array of unmanaged <see cref="IBinarySerializable"/> <paramref name="values"/> from buffer.</summary>
435435
/// <typeparam name="T">A value type that implements <see cref="IBinarySerializable"/>.</typeparam>
436-
public void Read<T>(in List<T> values) where T : unmanaged, IBinarySerializable => Read(GetListSpan(in values));
436+
public void Read<T>(in List<T> values) where T : struct, IBinarySerializable => Read(GetListSpan(in values));
437437

438438
/// <summary>Reads a circular buffer of unmanaged <see cref="IBinarySerializable"/> <paramref name="values"/> from buffer.</summary>
439439
/// <typeparam name="T">A value type that implements <see cref="IBinarySerializable"/>.</typeparam>

0 commit comments

Comments
 (0)