Skip to content
Permalink
Browse files
IGNITE-16341 .NET: Emit efficient user object serialization code (#599)
* Use IL.Emit in `ObjectSerializerHandler` to compile efficient serialization code that reads data directly into user object fields, which provides maximum performance and minimum allocations.
* Use fields instead of properties, so that all scenarios are supported (user-defined fields, automatic properties, records, anonymous types).
* Cache tables and views so that compiled delegates can be reused.

Benchmark results (`ReadObject` and `WriteObject` correspond to changes in this ticket, `Old` is previous implementation, `Tuple` is `RecordBinaryView`):
```
 |           Method |       Mean |   Error |  StdDev | Ratio | RatioSD |  Gen 0 | Allocated |
 |----------------- |-----------:|--------:|--------:|------:|--------:|-------:|----------:|
 | ReadObjectManual |   210.9 ns | 0.73 ns | 0.65 ns |  1.00 |    0.00 | 0.0126 |      80 B |
 |       ReadObject |   257.5 ns | 1.41 ns | 1.25 ns |  1.22 |    0.01 | 0.0124 |      80 B |
 |        ReadTuple |   561.0 ns | 3.09 ns | 2.89 ns |  2.66 |    0.01 | 0.0849 |     536 B |
 |    ReadObjectOld | 1,020.9 ns | 9.05 ns | 8.47 ns |  4.84 |    0.05 | 0.0744 |     472 B |

 |            Method |     Mean |   Error |  StdDev | Ratio | RatioSD |  Gen 0 | Allocated |
 |------------------ |---------:|--------:|--------:|------:|--------:|-------:|----------:|
 | WriteObjectManual | 155.8 ns | 1.15 ns | 1.07 ns |  1.00 |    0.00 | 0.0062 |      40 B |
 |       WriteObject | 167.0 ns | 0.76 ns | 0.75 ns |  1.07 |    0.01 | 0.0062 |      40 B |
 |        WriteTuple | 324.7 ns | 4.35 ns | 4.07 ns |  2.08 |    0.02 | 0.0229 |     144 B |
 |    WriteObjectOld | 798.5 ns | 5.10 ns | 4.77 ns |  5.13 |    0.04 | 0.0381 |     240 B |
```
  • Loading branch information
ptupitsyn committed Jan 28, 2022
1 parent d9f30cf commit 34d7af9a97d69461e6bdf344dc24069dd5ee6112
Showing 23 changed files with 1,330 additions and 104 deletions.
@@ -18,13 +18,13 @@
namespace Apache.Ignite.Benchmarks
{
using BenchmarkDotNet.Running;
using Proto;
using Table.Serialization;

internal static class Program
{
private static void Main()
{
BenchmarkRunner.Run<WriteGuidBenchmarks>();
BenchmarkRunner.Run<SerializerHandlerReadBenchmarks>();
}
}
}
@@ -0,0 +1,142 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

namespace Apache.Ignite.Benchmarks.Table.Serialization
{
using System;
using System.Reflection;
using Internal.Proto;
using Internal.Table;
using Internal.Table.Serialization;
using MessagePack;

/// <summary>
/// Old object serializer handler implementation as a baseline for benchmarks.
/// </summary>
/// <typeparam name="T">Object type.</typeparam>
internal class ObjectSerializerHandlerOld<T> : IRecordSerializerHandler<T>
where T : class
{
/// <inheritdoc/>
public T Read(ref MessagePackReader reader, Schema schema, bool keyOnly = false)
{
var columns = schema.Columns;
var count = keyOnly ? schema.KeyColumnCount : columns.Count;
var res = Activator.CreateInstance<T>();
var type = typeof(T);

for (var index = 0; index < count; index++)
{
if (reader.TryReadNoValue())
{
continue;
}

var col = columns[index];
var prop = GetPropertyIgnoreCase(type, col.Name);

if (prop != null)
{
var value = reader.ReadObject(col.Type);
prop.SetValue(res, value);
}
else
{
reader.Skip();
}
}

return (T)(object)res;
}

/// <inheritdoc/>
public T ReadValuePart(ref MessagePackReader reader, Schema schema, T key)
{
var columns = schema.Columns;
var res = Activator.CreateInstance<T>();
var type = typeof(T);

for (var i = 0; i < columns.Count; i++)
{
var col = columns[i];
var prop = GetPropertyIgnoreCase(type, col.Name);

if (i < schema.KeyColumnCount)
{
if (prop != null)
{
prop.SetValue(res, prop.GetValue(key));
}
}
else
{
if (reader.TryReadNoValue())
{
continue;
}

if (prop != null)
{
prop.SetValue(res, reader.ReadObject(col.Type));
}
else
{
reader.Skip();
}
}
}

return res;
}

/// <inheritdoc/>
public void Write(ref MessagePackWriter writer, Schema schema, T record, bool keyOnly = false)
{
var columns = schema.Columns;
var count = keyOnly ? schema.KeyColumnCount : columns.Count;
var type = record.GetType();

for (var index = 0; index < count; index++)
{
var col = columns[index];
var prop = GetPropertyIgnoreCase(type, col.Name);

if (prop == null)
{
writer.WriteNoValue();
}
else
{
writer.WriteObject(prop.GetValue(record));
}
}
}

private static PropertyInfo? GetPropertyIgnoreCase(Type type, string name)
{
foreach (var p in type.GetProperties())
{
if (p.Name.Equals(name, StringComparison.OrdinalIgnoreCase))
{
return p;
}
}

return null;
}
}
}
@@ -0,0 +1,92 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

namespace Apache.Ignite.Benchmarks.Table.Serialization
{
using System;
using BenchmarkDotNet.Engines;
using Ignite.Table;
using Internal.Buffers;
using Internal.Proto;
using Internal.Table;
using Internal.Table.Serialization;

/// <summary>
/// Base class for <see cref="IRecordSerializerHandler{T}"/> benchmarks.
/// </summary>
public abstract class SerializerHandlerBenchmarksBase
{
internal static readonly Car Object = new()
{
Id = Guid.NewGuid(),
BodyType = "Sedan",
Seats = 5
};

internal static readonly IgniteTuple Tuple = new()
{
[nameof(Car.Id)] = Object.Id,
[nameof(Car.BodyType)] = Object.BodyType,
[nameof(Car.Seats)] = Object.Seats
};

internal static readonly Schema Schema = new(1, 1, new[]
{
new Column(nameof(Car.Id), ClientDataType.Uuid, Nullable: false, IsKey: true, SchemaIndex: 0),
new Column(nameof(Car.BodyType), ClientDataType.String, Nullable: false, IsKey: false, SchemaIndex: 1),
new Column(nameof(Car.Seats), ClientDataType.Int32, Nullable: false, IsKey: false, SchemaIndex: 2)
});

internal static readonly byte[] SerializedData = GetSerializedData();

internal static readonly ObjectSerializerHandler<Car> ObjectSerializerHandler = new();

internal static readonly ObjectSerializerHandlerOld<Car> ObjectSerializerHandlerOld = new();

protected Consumer Consumer { get; } = new();

internal static void VerifyWritten(PooledArrayBufferWriter pooledWriter)
{
var bytesWritten = pooledWriter.GetWrittenMemory().Length;

if (bytesWritten != 29)
{
throw new Exception("Unexpected number of bytes written: " + bytesWritten);
}
}

private static byte[] GetSerializedData()
{
using var pooledWriter = new PooledArrayBufferWriter();
var writer = pooledWriter.GetMessageWriter();

TupleSerializerHandler.Instance.Write(ref writer, Schema, Tuple);

writer.Flush();
return pooledWriter.GetWrittenMemory().Slice(4).ToArray();
}

protected internal class Car
{
public Guid Id { get; set; }

public string BodyType { get; set; } = null!;

public int Seats { get; set; }
}
}
}
@@ -0,0 +1,82 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

namespace Apache.Ignite.Benchmarks.Table.Serialization
{
using System.Diagnostics.CodeAnalysis;
using BenchmarkDotNet.Attributes;
using Internal.Proto;
using Internal.Table.Serialization;
using MessagePack;

/// <summary>
/// Benchmarks for <see cref="IRecordSerializerHandler{T}.Read"/> implementations.
/// Results on Intel Core i7-9700K, .NET SDK 3.1.416, Ubuntu 20.04:
/// | Method | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Allocated |
/// |----------------- |-----------:|--------:|--------:|------:|--------:|-------:|----------:|
/// | ReadObjectManual | 210.9 ns | 0.73 ns | 0.65 ns | 1.00 | 0.00 | 0.0126 | 80 B |
/// | ReadObject | 257.5 ns | 1.41 ns | 1.25 ns | 1.22 | 0.01 | 0.0124 | 80 B |
/// | ReadTuple | 561.0 ns | 3.09 ns | 2.89 ns | 2.66 | 0.01 | 0.0849 | 536 B |
/// | ReadObjectOld | 1,020.9 ns | 9.05 ns | 8.47 ns | 4.84 | 0.05 | 0.0744 | 472 B |.
/// </summary>
[SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Benchmarks.")]
[MemoryDiagnoser]
public class SerializerHandlerReadBenchmarks : SerializerHandlerBenchmarksBase
{
[Benchmark(Baseline = true)]
public void ReadObjectManual()
{
var reader = new MessagePackReader(SerializedData);

var res = new Car
{
Id = reader.TryReadNoValue() ? default : reader.ReadGuid(),
BodyType = reader.TryReadNoValue() ? default! : reader.ReadString(),
Seats = reader.TryReadNoValue() ? default : reader.ReadInt32()
};

Consumer.Consume(res);
}

[Benchmark]
public void ReadObject()
{
var reader = new MessagePackReader(SerializedData);
var res = ObjectSerializerHandler.Read(ref reader, Schema);

Consumer.Consume(res);
}

[Benchmark]
public void ReadTuple()
{
var reader = new MessagePackReader(SerializedData);
var res = TupleSerializerHandler.Instance.Read(ref reader, Schema);

Consumer.Consume(res);
}

[Benchmark]
public void ReadObjectOld()
{
var reader = new MessagePackReader(SerializedData);
var res = ObjectSerializerHandlerOld.Read(ref reader, Schema);

Consumer.Consume(res);
}
}
}

0 comments on commit 34d7af9

Please sign in to comment.