Permalink
Show file tree
Hide file tree
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
Showing
23 changed files
with
1,330 additions
and
104 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; } | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
} | ||
} |
Oops, something went wrong.