Skip to content

Commit

Permalink
Move UTF8 encoding to code gen time (#143)
Browse files Browse the repository at this point in the history
Instead of emitting code that performs the encoding at runtime during static initialization, this does that work during code generation, and relies on the efficient way in which the C# compiler handles constant-initialized binary arrays.
  • Loading branch information
idg10 authored Oct 13, 2020
1 parent 8c5063e commit e693817
Show file tree
Hide file tree
Showing 2 changed files with 35 additions and 28 deletions.
20 changes: 8 additions & 12 deletions Solutions/Menes.Sandbox/Examples/JsonObjectExample.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,18 @@ namespace Examples
{
public static readonly Examples.JsonObjectExample Null = new Examples.JsonObjectExample(default);
public static readonly System.Func<System.Text.Json.JsonElement, Examples.JsonObjectExample> FromJsonElement = e => new Examples.JsonObjectExample(e);
private const string FirstPropertyName = "first";
private const string SecondPropertyName = "second";
private const string ThirdPropertyName = "third";
private const string ChildrenPropertyName = "children";
private const string FirstPropertyNamePath = ".first";
private const string SecondPropertyNamePath = ".second";
private const string ThirdPropertyNamePath = ".third";
private const string ChildrenPropertyNamePath = ".children";
private static readonly System.Text.Json.JsonEncodedText EncodedFirstPropertyName = System.Text.Json.JsonEncodedText.Encode(FirstPropertyName);
private static readonly System.Text.Json.JsonEncodedText EncodedSecondPropertyName = System.Text.Json.JsonEncodedText.Encode(SecondPropertyName);
private static readonly System.Text.Json.JsonEncodedText EncodedThirdPropertyName = System.Text.Json.JsonEncodedText.Encode(ThirdPropertyName);
private static readonly System.Text.Json.JsonEncodedText EncodedChildrenPropertyName = System.Text.Json.JsonEncodedText.Encode(ChildrenPropertyName);
private static readonly System.ReadOnlyMemory<byte> FirstPropertyNameBytes = System.Text.Encoding.UTF8.GetBytes(FirstPropertyName);
private static readonly System.ReadOnlyMemory<byte> SecondPropertyNameBytes = System.Text.Encoding.UTF8.GetBytes(SecondPropertyName);
private static readonly System.ReadOnlyMemory<byte> ThirdPropertyNameBytes = System.Text.Encoding.UTF8.GetBytes(ThirdPropertyName);
private static readonly System.ReadOnlyMemory<byte> ChildrenPropertyNameBytes = System.Text.Encoding.UTF8.GetBytes(ChildrenPropertyName);
private static readonly System.ReadOnlyMemory<byte> FirstPropertyNameBytes = new byte[] { 102, 105, 114, 115, 116 };
private static readonly System.ReadOnlyMemory<byte> SecondPropertyNameBytes = new byte[] { 115, 101, 99, 111, 110, 100 };
private static readonly System.ReadOnlyMemory<byte> ThirdPropertyNameBytes = new byte[] { 116, 104, 105, 114, 100 };
private static readonly System.ReadOnlyMemory<byte> ChildrenPropertyNameBytes = new byte[] { 99, 104, 105, 108, 100, 114, 101, 110 };
private static readonly System.Text.Json.JsonEncodedText EncodedFirstPropertyName = System.Text.Json.JsonEncodedText.Encode(FirstPropertyNameBytes.Span);
private static readonly System.Text.Json.JsonEncodedText EncodedSecondPropertyName = System.Text.Json.JsonEncodedText.Encode(SecondPropertyNameBytes.Span);
private static readonly System.Text.Json.JsonEncodedText EncodedThirdPropertyName = System.Text.Json.JsonEncodedText.Encode(ThirdPropertyNameBytes.Span);
private static readonly System.Text.Json.JsonEncodedText EncodedChildrenPropertyName = System.Text.Json.JsonEncodedText.Encode(ChildrenPropertyNameBytes.Span);
private static readonly System.Collections.Immutable.ImmutableArray<System.ReadOnlyMemory<byte>> KnownProperties = System.Collections.Immutable.ImmutableArray.Create(FirstPropertyNameBytes, SecondPropertyNameBytes, ThirdPropertyNameBytes, ChildrenPropertyNameBytes);
private readonly Menes.JsonString? first;
private readonly Menes.JsonInt32? second;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -985,10 +985,10 @@ private void BuildPropertyBackings(List<MemberDeclarationSyntax> members)
{
var propertyNames = new List<(string, string)>();

this.BuildPropertyNameDeclarations(members, propertyNames);
this.BuildPropertyNameDeclarations(propertyNames);
this.BuildPropertyNamePathDeclarations(propertyNames, members);
this.BuildEncodedPropertyNameDeclarations(propertyNames, members);
this.BuildPropertyNameBytesDeclarations(propertyNames, members);
this.BuildEncodedPropertyNameDeclarations(propertyNames, members);

this.AddKnownProperties(propertyNames, members);

Expand All @@ -1003,12 +1003,11 @@ private void BuildPropertyBackingDeclarations(List<MemberDeclarationSyntax> memb
}
}

private void BuildPropertyNameDeclarations(List<MemberDeclarationSyntax> members, List<(string fieldName, string jsonPropertyName)> propertyNames)
private void BuildPropertyNameDeclarations(List<(string fieldName, string jsonPropertyName)> propertyNames)
{
foreach (PropertyDeclaration property in this.Properties)
{
string propertyNameFieldName = GetPropertyNameFieldName(property);
this.BuildPropertyNameDeclaration(propertyNameFieldName, property.JsonPropertyName, members);
propertyNames.Add((propertyNameFieldName, property.JsonPropertyName));
}
}
Expand All @@ -1033,7 +1032,7 @@ private void BuildPropertyNameBytesDeclarations(List<(string, string)> propertyN
{
foreach ((string, string) propertyNameFieldName in propertyNameFieldNames)
{
this.BuildPropertyNameBytesDeclaration(propertyNameFieldName.Item1, members);
this.BuildPropertyNameBytesDeclaration(propertyNameFieldName.Item1, propertyNameFieldName.Item2, members);
}
}

Expand Down Expand Up @@ -1201,24 +1200,36 @@ private void BuildPropertyBackingDeclaration(PropertyDeclaration property, List<
}
}

private void BuildPropertyNameDeclaration(string propertyNameFieldName, string jsonPropertyName, List<MemberDeclarationSyntax> members)
{
members.Add(SF.ParseMemberDeclaration($"private const string {propertyNameFieldName} = \"{jsonPropertyName}\";" + Environment.NewLine));
}

private void BuildPropertyNamePathDeclaration(string propertyNameFieldName, string jsonPropertyName, List<MemberDeclarationSyntax> members)
{
members.Add(SF.ParseMemberDeclaration($"private const string {propertyNameFieldName}Path = \".{jsonPropertyName}\";" + Environment.NewLine));
}

private void BuildEncodedPropertyNameDeclaration(string propertyNameFieldName, List<MemberDeclarationSyntax> members)
{
members.Add(SF.ParseMemberDeclaration($"private static readonly System.Text.Json.JsonEncodedText Encoded{propertyNameFieldName} = System.Text.Json.JsonEncodedText.Encode({propertyNameFieldName});" + Environment.NewLine));
}

private void BuildPropertyNameBytesDeclaration(string propertyNameFieldName, List<MemberDeclarationSyntax> members)
{
members.Add(SF.ParseMemberDeclaration($"private static readonly System.ReadOnlyMemory<byte> {propertyNameFieldName}Bytes = System.Text.Encoding.UTF8.GetBytes({propertyNameFieldName});" + Environment.NewLine));
members.Add(SF.ParseMemberDeclaration($"private static readonly System.Text.Json.JsonEncodedText Encoded{propertyNameFieldName} = System.Text.Json.JsonEncodedText.Encode({propertyNameFieldName}Bytes.Span);" + Environment.NewLine));
}

private void BuildPropertyNameBytesDeclaration(string propertyNameFieldName, string jsonPropertyName, List<MemberDeclarationSyntax> members)
{
// The C# compiler handles constant byte array initialization like this by embedding the binary
// data directly into the compiled assembly, and uses that to initialize the array directly.
// This results in better startup times than putting the call to Encoding.UTF8.GetBytes into the
// generated code itself. It means that we do the string processing here at code-gen time, instead
// of during static initialization.
// It's possible we could go a step further, because most of places that use this data obtain a
// ReadOnlySpan<T>, and it turns out that the compiler can optimize the initialization of a span
// with a constant binary array even further: it doesn't need to allocate an array at all because
// it can produce a span that wraps the compiled byte stream directly. (Moreover, the code it
// generates to produce this span makes it possible for the JIT compiler to determine the span length,
// and there are scenarios where this can go on to improve performance by enabling the JIT to omit
// compile-time bounds checks. However, because of the limitations on span usage (it's a ref struct)
// we can't just make these properties return a ReadOnlySpan<byte>. In any case, there's currently one
// use of these properties that depends on hangs onto the Memory object: the list of all properties.
// It might be possible to create a more efficient formulation in which we have one great big binary
// array that contains all of the UTF8 data, but we'd need careful benchmarking to work out whether
// it did in fact produce an improvement.
members.Add(SF.ParseMemberDeclaration($"private static readonly System.ReadOnlyMemory<byte> {propertyNameFieldName}Bytes = new byte[] {{ {string.Join(", ", System.Text.Encoding.UTF8.GetBytes(jsonPropertyName).Select(b => b.ToString()))} }};" + Environment.NewLine));
}
}
}

0 comments on commit e693817

Please sign in to comment.