Skip to content

Commit 1aade21

Browse files
committed
Unsafe optimizations
1 parent 7ee349e commit 1aade21

14 files changed

+208
-50
lines changed

Benchmarks/Benchmarks.csproj

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<PropertyGroup>
44
<OutputType>Exe</OutputType>
5-
<TargetFramework>net8.0</TargetFramework>
5+
<TargetFrameworks>net7.0;net8.0</TargetFrameworks>
66
<ImplicitUsings>enable</ImplicitUsings>
77
<Nullable>enable</Nullable>
88
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
@@ -38,7 +38,7 @@
3838
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
3939
<Visible>True</Visible>
4040
</None>
41-
<None Include="$(MSBuildThisFileDirectory)..\native\libprotozero.dll" Condition="Exists('$(MSBuildThisFileDirectory)..\native\protozero.dll')">
41+
<None Include="$(MSBuildThisFileDirectory)..\native\protozero.dll" Condition="Exists('$(MSBuildThisFileDirectory)..\native\protozero.dll')">
4242
<Link>protozero.dll</Link>
4343
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
4444
<Visible>True</Visible>

Benchmarks/PerfectSerializer.cs

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
namespace Benchmarks;
2+
3+
public class PerfectSerializer
4+
{
5+
#if NET8_0
6+
[System.Runtime.CompilerServices.InlineArray(16)]
7+
public struct FixeSizeByteBuffer16
8+
{
9+
private byte item;
10+
}
11+
12+
[System.Runtime.CompilerServices.InlineArray(8)]
13+
public struct FixeSizeByteBuffer8
14+
{
15+
private byte item;
16+
}
17+
18+
public struct InnerMessageStruct
19+
{
20+
public int id;
21+
public FixeSizeByteBuffer16 name;
22+
}
23+
24+
[System.Runtime.CompilerServices.InlineArray(9)]
25+
public struct FixeSizeInnerMessageBuffer9
26+
{
27+
private InnerMessageStruct item;
28+
}
29+
30+
public struct RootMessageStruct
31+
{
32+
public ulong a;
33+
public long b;
34+
public FixeSizeByteBuffer16 d;
35+
public FixeSizeByteBuffer8 e1;
36+
public FixeSizeByteBuffer8 e2;
37+
public FixeSizeByteBuffer8 e3;
38+
public FixeSizeByteBuffer8 e4;
39+
public FixeSizeInnerMessageBuffer9 f;
40+
}
41+
#endif
42+
}

Benchmarks/ProtoZeroVsCanonical.cs

+41-5
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
1+
using System.Runtime.CompilerServices;
12
using System.Runtime.InteropServices;
23
using BenchmarkDotNet.Attributes;
4+
using BenchmarkDotNet.Jobs;
35
using Google.Protobuf;
46
using ProtoZeroSharp;
57
using ProtoZeroSharp.Tests;
68

79
namespace Benchmarks;
810

9-
[SimpleJob]
11+
[SimpleJob(RuntimeMoniker.Net80)]
1012
[MemoryDiagnoser]
1113
public partial class ProtoZeroVsCanonical
1214
{
13-
private byte[] output = new byte[64 * 1024 * 1024]; // I have precalculated the message and I know this is enough to store it all
15+
private byte[] output = new byte[128 * 1024 * 1024]; // I have precalculated the message and I know this is enough to store it all
1416

1517
[LibraryImport("protozero", EntryPoint = "WriteProto")]
1618
public static unsafe partial int NativeProto(int messagesCount, byte* output);
@@ -30,7 +32,41 @@ public unsafe void ProtoZeroNative()
3032
}
3133

3234
[Benchmark]
33-
public void ProtoZeroSharp()
35+
public unsafe void PerfectSerializer()
36+
{
37+
#if NET8_0
38+
ChunkedArray array = new ChunkedArray();
39+
for (int j = 0; j < 200000; ++j)
40+
{
41+
var span = array.ReserveContiguousSpan(sizeof(PerfectSerializer.RootMessageStruct));
42+
43+
ref PerfectSerializer.RootMessageStruct refStruct = ref Unsafe.As<byte, PerfectSerializer.RootMessageStruct>(ref span.GetPinnableReference());
44+
45+
refStruct.a = ulong.MaxValue;
46+
refStruct.b = long.MinValue;
47+
"Hello, World!"u8.CopyTo(refStruct.d);
48+
"Msg 1"u8.CopyTo(refStruct.e1);
49+
"Msg 2"u8.CopyTo(refStruct.e2);
50+
"Msg 3"u8.CopyTo(refStruct.e3);
51+
"Msg 4"u8.CopyTo(refStruct.e4);
52+
53+
for (int i = 0; i < 9; ++i)
54+
{
55+
refStruct.f[i].id = i;
56+
"Inner Message"u8.CopyTo(refStruct.f[i].name);
57+
}
58+
}
59+
60+
array.CopyTo(output);
61+
62+
array.Free();
63+
#endif
64+
}
65+
66+
[Benchmark]
67+
[Arguments(true)]
68+
[Arguments(false)]
69+
public void ProtoZeroSharp(bool optimizeSizeOverPerformance)
3470
{
3571
ProtoWriter writer = new ProtoWriter();
3672

@@ -52,10 +88,10 @@ public void ProtoZeroSharp()
5288
writer.AddVarInt(SubMessage.IdFieldNumber, i);
5389
writer.AddBytes(SubMessage.NameFieldNumber, "Inner Message"u8);
5490

55-
writer.CloseSub();
91+
writer.CloseSub(optimizeSizeOverPerformance);
5692
}
5793

58-
writer.CloseSub();
94+
writer.CloseSub(optimizeSizeOverPerformance);
5995
}
6096

6197
writer.CopyTo(output);

ProtoZeroSharp.Tests/ProtoWriterTests.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public unsafe void Encode_Zero_Decode_Canonical()
2828
writer.AddVarInt(SubMessage.IdFieldNumber, i);
2929
writer.AddBytes(SubMessage.NameFieldNumber, Encoding.UTF8.GetBytes($"Name {i}"));
3030

31-
writer.CloseSub();
31+
writer.CloseSub(false);
3232
}
3333
}
3434

ProtoZeroSharp/AssemblyInfo.cs

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
using System.Runtime.CompilerServices;
2+
3+
[assembly: InternalsVisibleTo("Benchmarks")]

ProtoZeroSharp/ChunkedArray.Chunk.cs

+2
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@ public struct Chunk
2525

2626
public Span<byte> GetSpan(int offset, int spanLength)
2727
{
28+
#if DEBUG
2829
if (spanLength > Length - offset)
2930
throw new InvalidOperationException("Requested span is larger than the available data.");
31+
#endif
3032
return new Span<byte>(data + offset, spanLength);
3133
}
3234

ProtoZeroSharp/ChunkedArray.cs

+6-3
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,9 @@ public void Free()
5151
/// <exception cref="ObjectDisposedException"></exception>
5252
public Span<byte> ReserveContiguousSpan(int length)
5353
{
54+
#if DEBUG
5455
AssertNotDisposed();
55-
56+
#endif
5657
if (last->FreeBytes < length)
5758
{
5859
last->Next = Chunk.AllocChunk(Math.Max(DefaultChunkSize, length));
@@ -71,10 +72,11 @@ public Span<byte> ReserveContiguousSpan(int length)
7172
/// <exception cref="InvalidOperationException"></exception>
7273
public void MoveForward(int length)
7374
{
75+
#if DEBUG
7476
AssertNotDisposed();
75-
7677
if (last->Used + length > last->Length)
7778
throw new InvalidOperationException("Moving forward would exceed the current chunk's capacity.");
79+
#endif
7880
last->Used += length;
7981
}
8082

@@ -83,8 +85,9 @@ public void MoveForward(int length)
8385
/// </summary>
8486
public int GetTotalLength()
8587
{
88+
#if DEBUG
8689
AssertNotDisposed();
87-
90+
#endif
8891
int totalLength = 0;
8992
var chunk = first;
9093
while (chunk != null)

ProtoZeroSharp/ProtoWriter.cs

+16-8
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Diagnostics;
23
using System.IO;
34
using System.Runtime.CompilerServices;
45
using System.Runtime.InteropServices;
@@ -91,19 +92,26 @@ public void StartSub(int messageId)
9192
currentMessageLength = 0;
9293
}
9394

94-
public void CloseSub()
95+
public void CloseSub(bool optimizeSizeOverPerformance = true)
9596
{
96-
var lastSubMessageStart = submessagesStack.Peek();
97-
submessagesStack.Pop();
97+
var lastSubMessageStart = submessagesStack.PeekAndPop();
9898

9999
var lengthSpan = lastSubMessageStart.Chunk->GetSpan(lastSubMessageStart.Offset);
100-
int written = ProtobufFormat.WriteLengthFieldLength(lengthSpan, currentMessageLength);
101-
if (written < VarInt.MaxBytesCount)
100+
int written;
101+
if (optimizeSizeOverPerformance)
102102
{
103-
lastSubMessageStart.Chunk->Erase(lastSubMessageStart.Offset + written, VarInt.MaxBytesCount - written);
103+
written = ProtobufFormat.WriteLengthFieldLength(lengthSpan, currentMessageLength);
104+
if (written < VarInt.MaxBytesCount)
105+
{
106+
lastSubMessageStart.Chunk->Erase(lastSubMessageStart.Offset + written, VarInt.MaxBytesCount - written);
107+
}
108+
}
109+
else
110+
{
111+
written = ProtobufFormat.WriteLengthFieldLength(lengthSpan, currentMessageLength, VarInt.MaxBytesCount);
112+
Debug.Assert(written == VarInt.MaxBytesCount);
104113
}
105114

106-
currentMessageLength = lengthsStack.Peek() + written + currentMessageLength;
107-
lengthsStack.Pop();
115+
currentMessageLength = lengthsStack.PeekAndPop() + written + currentMessageLength;
108116
}
109117
}

ProtoZeroSharp/ProtobufFormat.cs

+15
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
using System;
2+
using System.Runtime.CompilerServices;
23
using System.Text;
34

45
namespace ProtoZeroSharp;
56

67
internal static class ProtobufFormat
78
{
9+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
810
private static ulong EncodeKey(int fieldNumber, ProtoWireType wireType)
911
{
1012
return ((ulong)fieldNumber << 3) | (ulong)wireType;
@@ -23,6 +25,7 @@ private static ulong EncodeKey(int fieldNumber, ProtoWireType wireType)
2325
/// <summary>
2426
/// Returns the upper bound of the length of a bytes field.
2527
/// </summary>
28+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
2629
internal static int BytesFieldLenUpperBound(int payloadLength) => VarInt.MaxBytesCount * 2 + payloadLength; // message id + length + payload
2730

2831
/// <summary>
@@ -33,25 +36,35 @@ private static ulong EncodeKey(int fieldNumber, ProtoWireType wireType)
3336
/// <param name="fieldNumber">Proto message id</param>
3437
/// <param name="value">Value of the message</param>
3538
/// <returns>Number of bytes written</returns>
39+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
3640
internal static int WriteVarIntField(Span<byte> output, int fieldNumber, ulong value)
3741
{
3842
int written = VarInt.WriteVarint(output, EncodeKey(fieldNumber, ProtoWireType.VarInt));
3943
written += VarInt.WriteVarint(output.Slice(written), value);
4044
return written;
4145
}
4246

47+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
4348
internal static int WriteLengthFieldHeader(Span<byte> output, int messageId)
4449
{
4550
int written = VarInt.WriteVarint(output, EncodeKey(messageId, ProtoWireType.Length));
4651
return written;
4752
}
4853

54+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
4955
internal static int WriteLengthFieldLength(Span<byte> output, int length)
5056
{
5157
int written = VarInt.WriteVarint(output, length);
5258
return written;
5359
}
5460

61+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
62+
internal static int WriteLengthFieldLength(Span<byte> output, int length, int fixedBytesSize)
63+
{
64+
int written = VarInt.WriteVarintFixedSize(output, (ulong)length, fixedBytesSize);
65+
return written;
66+
}
67+
5568
/// <summary>
5669
/// Writes a bytes buffer message to a buffer.
5770
/// The buffer must be at least 20 bytes + payload length long to fit any buffer.
@@ -60,6 +73,7 @@ internal static int WriteLengthFieldLength(Span<byte> output, int length)
6073
/// <param name="fieldNumber">Proto message id</param>
6174
/// <param name="payload">Buffer to write</param>
6275
/// <returns>Number of bytes written</returns>
76+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
6377
internal static int WriteBytes(Span<byte> output, int fieldNumber, ReadOnlySpan<byte> payload)
6478
{
6579
#if DEBUG
@@ -82,6 +96,7 @@ internal static int WriteBytes(Span<byte> output, int fieldNumber, ReadOnlySpan<
8296
/// <param name="payloadBytesCount">Precalculatd number of bytes of utf-8 encoding of the given payload</param>
8397
/// <param name="payload">String to write</param>
8498
/// <returns>Number of bytes written</returns>
99+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
85100
internal static unsafe int WriteString(Span<byte> output, int fieldNumber, int payloadBytesCount, string payload)
86101
{
87102
#if DEBUG

ProtoZeroSharp/StackArray.cs

+13
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,28 @@ public void Add(in T t)
3232

3333
public ref T Peek()
3434
{
35+
#if DEBUG
3536
if (count == 0)
3637
throw new InvalidOperationException("StackArray is empty.");
38+
#endif
3739
return ref this[count - 1];
3840
}
3941

4042
public void Pop()
4143
{
44+
#if DEBUG
4245
if (count == 0)
4346
throw new InvalidOperationException("StackArray is empty.");
47+
#endif
4448
count--;
4549
}
50+
51+
public T PeekAndPop()
52+
{
53+
#if DEBUG
54+
if (count == 0)
55+
throw new InvalidOperationException("StackArray is empty.");
56+
#endif
57+
return this[--count];
58+
}
4659
}

ProtoZeroSharp/VarInt.cs

+27
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,33 @@ internal static int WriteVarint(Span<byte> output, ulong value)
3232
return index;
3333
}
3434

35+
/// <summary>
36+
/// Writes a variable-length integer to the given output span in a fixed size.
37+
/// It might be redundant, if the value is small, but it's useful for writing fixed-size varints.
38+
/// </summary>
39+
/// <param name="output">Buffer to write varint to</param>
40+
/// <param name="value">Value to write</param>
41+
/// <param name="bytes">Number of bytes to write</param>
42+
/// <returns>Number of bytes written.</returns>
43+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
44+
internal static int WriteVarintFixedSize(Span<byte> output, ulong value, int bytes)
45+
{
46+
#if DEBUG
47+
if (output.Length < bytes)
48+
throw new ArgumentException($"Output buffer must be at least {bytes} bytes long to fit the value");
49+
#endif
50+
int index = 0;
51+
52+
for (int i = 0; i < bytes - 1; ++i)
53+
{
54+
output[index++] = (byte)(value | 0x80);
55+
value >>= 7;
56+
}
57+
output[index++] = (byte)value;
58+
59+
return index;
60+
}
61+
3562
[MethodImpl(MethodImplOptions.AggressiveInlining)]
3663
internal static int WriteVarint(Span<byte> output, int value)
3764
{

0 commit comments

Comments
 (0)