-
-
Notifications
You must be signed in to change notification settings - Fork 673
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
mpc: More aggressive ahead of time optimization for string-key type formatter. #861
Conversation
FormatterTemplte : generates only int-key type formatter. IFomatterTemplate : common method/property between FormatterTemplate and StringKeyFormatterTemplate. CodeGenerator : objectFormatterTemplates is IFormatterTemplate[] ShouldUseFormatterResolverHelper : Optimization of IFormatterResolver.
…ed and target type is not primitive type.
Wow. I've no doubt this is an improvement. Thanks for contributing!
@neuecc, what do you think? |
There are a number of improvements in there, but I think the basic point is that mpc also does the inlined constant embedding automata that we do during dynamic code generation. I've also made custom formatters based on code generation by MPC and modified them. Also, I'd like the code to be similar to the current dynamic code generation. |
When ULong value consists purely of ASCII characters, 8 ascii chars are printed as a comment.
… now ____stringByteKeys_[MemberName].
I agree with this comment. I changed the naming rule from
The main cause of readability decrease is the little-endian ulong keys embedded in switch case or if statements. I added comment lines for each of the ulong keys which contain only ascii compatible utf8 characters. Pull Request Generated Formatter Code with 2 additional commits.namespace MessagePack.Formatters
{
using System;
using System.Buffers;
using System.Runtime.InteropServices;
using MessagePack;
public sealed class TestTypeFormatter : global::MessagePack.Formatters.IMessagePackFormatter<global::TestType>
{
private static byte[] ____stringByteKeys_Value;
private static byte[] ____stringByteKeys_Value0;
private static byte[] ____stringByteKeys_Value1;
private static byte[] ____stringByteKeys_Value2;
private static byte[] ____stringByteKeys_EightSensesFifth0;
private static byte[] ____stringByteKeys_EightSensesFifth1;
private static byte[] ____stringByteKeys_EightSensesFifth2;
private static byte[] ____stringByteKeys_EightSensesFifth3;
private static byte[] ____stringByteKeys_EightSensesFifth4;
private static byte[] ____stringByteKeys__0123456_0123456;
public TestTypeFormatter()
{
____stringByteKeys_Value = new byte[]{ 0xA5, 0x56, 0x61, 0x6C, 0x75, 0x65, };
____stringByteKeys_Value0 = new byte[]{ 0xA6, 0x56, 0x61, 0x6C, 0x75, 0x65, 0x30, };
____stringByteKeys_Value1 = new byte[]{ 0xA6, 0x56, 0x61, 0x6C, 0x75, 0x65, 0x31, };
____stringByteKeys_Value2 = new byte[]{ 0xA6, 0x56, 0x61, 0x6C, 0x75, 0x65, 0x32, };
____stringByteKeys_EightSensesFifth0 = new byte[]{ 0xB1, 0x45, 0x69, 0x67, 0x68, 0x74, 0x53, 0x65, 0x6E, 0x73, 0x65, 0x73, 0x46, 0x69, 0x66, 0x74, 0x68, 0x30, };
____stringByteKeys_EightSensesFifth1 = new byte[]{ 0xB1, 0x45, 0x69, 0x67, 0x68, 0x74, 0x53, 0x65, 0x6E, 0x73, 0x65, 0x73, 0x46, 0x69, 0x66, 0x74, 0x68, 0x31, };
____stringByteKeys_EightSensesFifth2 = new byte[]{ 0xB1, 0x45, 0x69, 0x67, 0x68, 0x74, 0x53, 0x65, 0x6E, 0x73, 0x65, 0x73, 0x46, 0x69, 0x66, 0x74, 0x68, 0x32, };
____stringByteKeys_EightSensesFifth3 = new byte[]{ 0xB1, 0x45, 0x69, 0x67, 0x68, 0x74, 0x53, 0x65, 0x6E, 0x73, 0x65, 0x73, 0x46, 0x69, 0x66, 0x74, 0x68, 0x33, };
____stringByteKeys_EightSensesFifth4 = new byte[]{ 0xB1, 0x45, 0x69, 0x67, 0x68, 0x74, 0x53, 0x65, 0x6E, 0x73, 0x65, 0x73, 0x46, 0x69, 0x66, 0x74, 0x68, 0x34, };
____stringByteKeys__0123456_0123456 = new byte[]{ 0xB0, 0x5F, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x5F, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, };
}
public void Serialize(ref MessagePackWriter writer, global::TestType value, global::MessagePack.MessagePackSerializerOptions options)
{
if (value == null)
{
writer.WriteNil();
return;
}
IFormatterResolver formatterResolver = options.Resolver;
writer.WriteMapHeader(10);
writer.WriteRaw(____stringByteKeys_Value);
writer.Write(value.Value);
writer.WriteRaw(____stringByteKeys_Value0);
writer.Write(value.Value0);
writer.WriteRaw(____stringByteKeys_Value1);
writer.Write(value.Value1);
writer.WriteRaw(____stringByteKeys_Value2);
writer.Write(value.Value2);
writer.WriteRaw(____stringByteKeys_EightSensesFifth0);
writer.Write(value.EightSensesFifth0);
writer.WriteRaw(____stringByteKeys_EightSensesFifth1);
writer.Write(value.EightSensesFifth1);
writer.WriteRaw(____stringByteKeys_EightSensesFifth2);
writer.Write(value.EightSensesFifth2);
writer.WriteRaw(____stringByteKeys_EightSensesFifth3);
writer.Write(value.EightSensesFifth3);
writer.WriteRaw(____stringByteKeys_EightSensesFifth4);
writer.Write(value.EightSensesFifth4);
writer.WriteRaw(____stringByteKeys__0123456_0123456);
formatterResolver.GetFormatterWithVerify<string>().Serialize(ref writer, value._0123456_0123456, options);
}
public global::TestType Deserialize(ref MessagePackReader reader, global::MessagePack.MessagePackSerializerOptions options)
{
if (reader.TryReadNil())
{
return null;
}
options.Security.DepthStep(ref reader);
IFormatterResolver formatterResolver = options.Resolver;
var __Value__ = default(int);
var __Value0__ = default(int);
var __Value1__ = default(int);
var __Value2__ = default(int);
var __EightSensesFifth0__ = default(int);
var __EightSensesFifth1__ = default(int);
var __EightSensesFifth2__ = default(int);
var __EightSensesFifth3__ = default(int);
var __EightSensesFifth4__ = default(int);
var ___0123456_0123456__ = default(string);
var isBigEndian = !global::System.BitConverter.IsLittleEndian;
for (int i = 0, length = reader.ReadMapHeader(); i < length; i++)
{
var stringKey = global::MessagePack.Internal.CodeGenHelpers.ReadStringSpan(ref reader);
switch(stringKey.Length)
{
case 5:
{
ulong last = stringKey[4];
last <<= 8;
last |= stringKey[3];
last <<= 8;
last |= stringKey[2];
last <<= 8;
last |= stringKey[1];
last <<= 8;
last |= stringKey[0];
switch (last)
{
default: goto FAIL;
case 0x65756C6156UL:
__Value__ = reader.ReadInt32();
continue;
}
}
case 6:
{
ulong last = stringKey[5];
last <<= 8;
last |= stringKey[4];
last <<= 8;
last |= stringKey[3];
last <<= 8;
last |= stringKey[2];
last <<= 8;
last |= stringKey[1];
last <<= 8;
last |= stringKey[0];
switch (last)
{
default: goto FAIL;
case 0x3065756C6156UL:
__Value0__ = reader.ReadInt32();
continue;
case 0x3165756C6156UL:
__Value1__ = reader.ReadInt32();
continue;
case 0x3265756C6156UL:
__Value2__ = reader.ReadInt32();
continue;
}
}
}
ReadOnlySpan<ulong> ulongs = isBigEndian ? stackalloc ulong[stringKey.Length >> 3] : MemoryMarshal.Cast<byte, ulong>(stringKey);
if(isBigEndian)
{
for(var index = 0; index < ulongs.Length; index++)
{
var index8times = index << 3;
ref var number = ref global::System.Runtime.CompilerServices.Unsafe.AsRef(ulongs[index]);
number = stringKey[index8times + 7];
for (var numberIndex = index8times + 6; numberIndex >= index8times; numberIndex--)
{
number <<= 8;
number |= stringKey[numberIndex];
}
}
}
switch(stringKey.Length)
{
default: goto FAIL;
case 16:
if (
ulongs[0] != 0x363534333231305FUL || // _0123456
ulongs[1] != 0x363534333231305FUL // _0123456
) goto FAIL;
___0123456_0123456__ = formatterResolver.GetFormatterWithVerify<string>().Deserialize(ref reader, options);
continue;
case 17:
if (ulongs[0] != 0x6E65537468676945UL) goto FAIL; // EightSen
if (ulongs[1] != 0x6874666946736573UL) goto FAIL; // sesFifth
{
uint last = stringKey[16];
switch (last)
{
default: goto FAIL;
case 0x30U:
__EightSensesFifth0__ = reader.ReadInt32();
continue;
case 0x31U:
__EightSensesFifth1__ = reader.ReadInt32();
continue;
case 0x32U:
__EightSensesFifth2__ = reader.ReadInt32();
continue;
case 0x33U:
__EightSensesFifth3__ = reader.ReadInt32();
continue;
case 0x34U:
__EightSensesFifth4__ = reader.ReadInt32();
continue;
}
}
}
FAIL:
reader.Skip();
}
var ____result = new global::TestType();
____result.Value = __Value__;
____result.Value0 = __Value0__;
____result.Value1 = __Value1__;
____result.Value2 = __Value2__;
____result.EightSensesFifth0 = __EightSensesFifth0__;
____result.EightSensesFifth1 = __EightSensesFifth1__;
____result.EightSensesFifth2 = __EightSensesFifth2__;
____result.EightSensesFifth3 = __EightSensesFifth3__;
____result.EightSensesFifth4 = __EightSensesFifth4__;
____result._0123456_0123456 = ___0123456_0123456__;
reader.Depth--;
return ____result;
}
}
} |
I made a benchmark project to know how much my pull request is faster than the original one. BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18363
Intel Core i7-8750H CPU 2.20GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores
.NET Core SDK=3.1.200
[Host] : .NET Core 3.1.2 (CoreCLR 4.700.20.6602, CoreFX 4.700.20.6702), X64 RyuJIT
Job-ITDJDY : .NET Core 3.1.2 (CoreCLR 4.700.20.6602, CoreFX 4.700.20.6702), X64 RyuJIT
Runtime=.NET Core 3.1
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well done!
Please consider to change constructor to static or move code into field initialization.
And I proposed a few style fixes to match project code style.
src/MessagePack.GeneratorCore/Generator/StringKey/StringKeyFormatterTemplate.tt
Show resolved
Hide resolved
src/MessagePack.GeneratorCore/Generator/StringKey/StringKeyFormatterTemplate.tt
Outdated
Show resolved
Hide resolved
src/MessagePack.GeneratorCore/Generator/StringKey/StringKeyFormatterTemplate.tt
Outdated
Show resolved
Hide resolved
src/MessagePack.GeneratorCore/Generator/StringKey/StringKeyFormatterTemplate.tt
Outdated
Show resolved
Hide resolved
src/MessagePack.GeneratorCore/Generator/StringKey/StringKeyFormatterTemplate.tt
Outdated
Show resolved
Hide resolved
src/MessagePack.GeneratorCore/Generator/StringKey/StringKeyFormatterTemplate.tt
Outdated
Show resolved
Hide resolved
src/MessagePack.GeneratorCore/Generator/StringKey/StringKeyFormatterTemplate.tt
Outdated
Show resolved
Hide resolved
Thank you @waitxd !
You are right. |
Found two more bugs:
In this example both will be serialized as an array: namespace SharedData
{
[MessagePackObject()]
public class SerializeMeAsArrayPlease
{
[Key(0)]
public bool Foo;
}
[MessagePackObject(true)]
public class TotallyDoNotSerializeMeAsArrayPlease
{
public bool Foo;
}
} It seems static fields and static constructor in generated code are redundant now. |
Thank you so much!
It should be embedded. I am worried that the end-user programmer at Unity environment uses these generated codes in .NET 3.5 API Level and his/her C# version is less than 7.3. |
You are still generate constructor and static fields that are not used anymore. Example public sealed class StringKeySerializerTargetFormatter : global::MessagePack.Formatters.IMessagePackFormatter<global::PerfBenchmarkDotNet.StringKeySerializerTarget>
{
private static byte[] ____stringByteKeys_MyProperty1;
static StringKeySerializerTargetFormatter()
{
____stringByteKeys_MyProperty1 = new byte[] { 0xAB, 0x4D, 0x79, 0x50, 0x72, 0x6F, 0x70, 0x65, 0x72, 0x74, 0x79, 0x31, };
}
public void Serialize(ref MessagePackWriter writer, global::PerfBenchmarkDotNet.StringKeySerializerTarget value, global::MessagePack.MessagePackSerializerOptions options)
{
if (value == null)
{
writer.WriteNil();
return;
}
writer.WriteMapHeader(1);
writer.WriteRaw(new byte[] { 0xAB, 0x4D, 0x79, 0x50, 0x72, 0x6F, 0x70, 0x65, 0x72, 0x74, 0x79, 0x31, });
writer.Write(value.MyProperty1);
}
public global::PerfBenchmarkDotNet.StringKeySerializerTarget Deserialize(ref MessagePackReader reader, global::MessagePack.MessagePackSerializerOptions options)
{
if (reader.TryReadNil())
{
return null;
}
options.Security.DepthStep(ref reader);
var __MyProperty1__ = default(int);
var isBigEndian = !global::System.BitConverter.IsLittleEndian;
for (int i = 0, length = reader.ReadMapHeader(); i < length; i++)
{
var stringKey = global::MessagePack.Internal.CodeGenHelpers.ReadStringSpan(ref reader);
ReadOnlySpan<ulong> ulongs = isBigEndian ? stackalloc ulong[stringKey.Length >> 3] : MemoryMarshal.Cast<byte, ulong>(stringKey);
if (isBigEndian)
{
for (var index = 0; index < ulongs.Length; index++)
{
var index8times = index << 3;
ref var number = ref global::System.Runtime.CompilerServices.Unsafe.AsRef(ulongs[index]);
number = stringKey[index8times + 7];
for (var numberIndex = index8times + 6; numberIndex >= index8times; numberIndex--)
{
number <<= 8;
number |= stringKey[numberIndex];
}
}
}
switch (stringKey.Length)
{
default:
FAIL:
reader.Skip();
continue;
case 11:
if (
ulongs[0] != 0x7265706F7250794DUL // MyProper
) goto FAIL;
{
uint last = stringKey[10];
last <<= 8;
last |= stringKey[9];
last <<= 8;
last |= stringKey[9];
if(last != 0x317974U) goto FAIL;
__MyProperty1__ = reader.ReadInt32();
continue;
}
}
}
var ____result = new global::PerfBenchmarkDotNet.StringKeySerializerTarget();
____result.MyProperty1 = __MyProperty1__;
reader.Depth--;
return ____result;
}
}
Yes, But for those who will read generated code byte array meaning will be unclear. writer.WriteMapHeader(9);
writer.WriteRaw(new byte[] { 0xAB, 0x4D, 0x79, 0x50, 0x72, 0x6F, 0x70, 0x65, 0x72, 0x74, 0x79, 0x31, }); //MyProperty1
writer.Write(value.MyProperty1); Or before: // MyProperty2
writer.WriteRaw(new byte[] { 0xAB, 0x4D, 0x79, 0x50, 0x72, 0x6F, 0x70, 0x65, 0x72, 0x74, 0x79, 0x32, });
writer.Write(value.MyProperty2);
That's should not be a problem. MessagePack-CSharp supports only Unity 2018.3+. It is mentioned in the readme. |
Thank you for all of your great advices!
Oh! I was releaved to hear that! |
This pull request is stale because it has been open 90 days with no activity. Remove stale label or comment or this will be closed in 5 days. |
My proposed automaton first branches by string length. The current string search automaton does not consider the length of the string, and compares it in 8-byte units. |
@neuecc How are you feeling about this change at this point? Is it close enough to the dynamic resolver that you're comfortable maintaining it? |
Serialize is good. |
Okay, I start rewriting the Deserialization method. |
Seems done. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks pretty good. One inefficiency should be corrected as noted. @neuecc are you satisfied?
src/MessagePack.GeneratorCore/Generator/ShouldUseFormatterResolverHelper.cs
Show resolved
Hide resolved
@pCYSl5EDgo
|
Fix format of StringKeyFormatterTemplate.tt reader.Skip() optimization C#9 Record preparation
It was okay. (First Skip() was string key skip and second Skip() was value skip.)
This PR uses binary sequence compare and AutomataKeyGen.GetKey. |
Summary
Current mpc generated code of string-key formatter unnecessarily allocates byte[] array and others.
This pull request reduces memory allocation, improves runtime initialization performance and embeds specialized automaton code for deserialization.
Current problems
Sample target string-key type code.
Current generated formmater code.
____stringByteKeys
Current generated
TestTypeFormatter
has a byte[][] field____stringByteKeys
.____stringByteKeys
does not need to be byte[][]. Accessing____stringByteKeys
inSerialize
method results in a performance penalty because of array boundary check.No need for runtime call of global::MessagePack.Internal.CodeGenHelpers.GetEncodedStringBytes
Initialization of
____stringByteKeys
uses unnecessary cpu power. String keys are known before runtime execution. Encoded string byte keys can be calculated by mpc and can be embedded as array initialization list.____keyMapping
____keyMapping
is aglobal::MessagePack.Internal.AutomataDictionary
type field.global::MessagePack.Internal.AutomataDictionary
allocates unnecessary arrays.AutomataDictionary
builds a trie tree from given string-keys in constructor and performs an automaton search inDeserialize
method.Achieved Pros
Generated formatter code by this pull request.
Embedding raw byte array initialization list
____stringByteKeys0 = new byte[]{ 0xA5, 0x56, 0x61, 0x6C, 0x75, 0x65, };
is compiled by C# compiler and IL codes are like this link.System.Runtime.CompilerServices.RuntimeHelpers::InitializeArray
is very performant method.Inlined searching of
Deserialize
Embedded inlined searching codes are now generated. Generated code's literal is Little-Endian. The performance penalty will appear when the runtime endian is Big-Endian.
Remove line
IFormatterResolver formatterResolver = options.Resolver;
when not needed.IFormatterResolver formatterResolver = options.Resolver;
is not needed when there are no field whose type is primitive or has a custom formatter. This pull request removes unnecessary line from generated codes.