Skip to content

Commit

Permalink
Improve deserialization perf with changes to property name lookup (do…
Browse files Browse the repository at this point in the history
  • Loading branch information
steveharter committed Oct 14, 2019
1 parent e71bb5c commit e707e2c
Show file tree
Hide file tree
Showing 4 changed files with 271 additions and 70 deletions.
183 changes: 121 additions & 62 deletions src/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Converters;
Expand All @@ -16,7 +17,9 @@ namespace System.Text.Json
internal sealed partial class JsonClassInfo
{
// The length of the property name embedded in the key (in bytes).
private const int PropertyNameKeyLength = 6;
// The key is a ulong (8 bytes) containing the first 7 bytes of the property name
// followed by a byte representing the length.
private const int PropertyNameKeyLength = 7;

// The limit to how many property names from the JSON are cached in _propertyRefsSorted before using PropertyCache.
private const int PropertyNameCountCacheThreshold = 64;
Expand Down Expand Up @@ -257,85 +260,111 @@ private JsonPropertyInfo GetPropertyWithUniqueAttribute(Type attributeType, Dict
return property;
}

// AggressiveInlining used although a large method it is only called from one location and is on a hot path.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public JsonPropertyInfo GetProperty(ReadOnlySpan<byte> propertyName, ref ReadStackFrame frame)
{
JsonPropertyInfo info = null;

// Keep a local copy of the cache in case it changes by another thread.
PropertyRef[] localPropertyRefsSorted = _propertyRefsSorted;

ulong key = GetKey(propertyName);

// If there is an existing cache, then use it.
if (localPropertyRefsSorted != null)
{
ulong key = GetKey(propertyName);

// Start with the current property index, and then go forwards\backwards.
int propertyIndex = frame.PropertyIndex;

int count = localPropertyRefsSorted.Length;
int iForward = Math.Min(propertyIndex, count);
int iBackward = iForward - 1;

while (iForward < count || iBackward >= 0)
while (true)
{
if (iForward < count)
{
if (TryIsPropertyRefEqual(localPropertyRefsSorted[iForward], propertyName, key, ref info))
PropertyRef propertyRef = localPropertyRefsSorted[iForward];
if (TryIsPropertyRefEqual(propertyRef, propertyName, key, ref info))
{
return info;
}

++iForward;
}

if (iBackward >= 0)
if (iBackward >= 0)
{
propertyRef = localPropertyRefsSorted[iBackward];
if (TryIsPropertyRefEqual(propertyRef, propertyName, key, ref info))
{
return info;
}

--iBackward;
}
}
else if (iBackward >= 0)
{
if (TryIsPropertyRefEqual(localPropertyRefsSorted[iBackward], propertyName, key, ref info))
PropertyRef propertyRef = localPropertyRefsSorted[iBackward];
if (TryIsPropertyRefEqual(propertyRef, propertyName, key, ref info))
{
return info;
}

--iBackward;
}
else
{
// Property was not found.
break;
}
}
}

// No cached item was found. Try the main list which has all of the properties.

string stringPropertyName = JsonHelpers.Utf8GetString(propertyName);
if (PropertyCache.TryGetValue(stringPropertyName, out info))
if (!PropertyCache.TryGetValue(stringPropertyName, out info))
{
// Check if we should add this to the cache.
// Only cache up to a threshold length and then just use the dictionary when an item is not found in the cache.
int count;
if (localPropertyRefsSorted != null)
{
count = localPropertyRefsSorted.Length;
}
else
info = JsonPropertyInfo.s_missingProperty;
}

Debug.Assert(info != null);

// Three code paths to get here:
// 1) info == s_missingProperty. Property not found.
// 2) key == info.PropertyNameKey. Exact match found.
// 3) key != info.PropertyNameKey. Match found due to case insensitivity.
Debug.Assert(info == JsonPropertyInfo.s_missingProperty || key == info.PropertyNameKey || Options.PropertyNameCaseInsensitive);

// Check if we should add this to the cache.
// Only cache up to a threshold length and then just use the dictionary when an item is not found in the cache.
int cacheCount = 0;
if (localPropertyRefsSorted != null)
{
cacheCount = localPropertyRefsSorted.Length;
}

// Do a quick check for the stable (after warm-up) case.
if (cacheCount < PropertyNameCountCacheThreshold)
{
// Do a slower check for the warm-up case.
if (frame.PropertyRefCache != null)
{
count = 0;
cacheCount += frame.PropertyRefCache.Count;
}
// Do a quick check for the stable (after warm-up) case.
if (count < PropertyNameCountCacheThreshold)

// Check again to append the cache up to the threshold.
if (cacheCount < PropertyNameCountCacheThreshold)
{
// Do a slower check for the warm-up case.
if (frame.PropertyRefCache != null)
if (frame.PropertyRefCache == null)
{
count += frame.PropertyRefCache.Count;
frame.PropertyRefCache = new List<PropertyRef>();
}

// Check again to append the cache up to the threshold.
if (count < PropertyNameCountCacheThreshold)
{
if (frame.PropertyRefCache == null)
{
frame.PropertyRefCache = new List<PropertyRef>();
}

ulong key = info.PropertyNameKey;
PropertyRef propertyRef = new PropertyRef(key, info);
frame.PropertyRefCache.Add(propertyRef);
}
PropertyRef propertyRef = new PropertyRef(key, info);
frame.PropertyRefCache.Add(propertyRef);
}
}

Expand All @@ -360,12 +389,13 @@ private Dictionary<string, JsonPropertyInfo> CreatePropertyCache(int capacity)

public JsonPropertyInfo PolicyProperty { get; private set; }

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool TryIsPropertyRefEqual(in PropertyRef propertyRef, ReadOnlySpan<byte> propertyName, ulong key, ref JsonPropertyInfo info)
{
if (key == propertyRef.Key)
{
// We compare the whole name, although we could skip the first 7 bytes (but it's not any faster)
if (propertyName.Length <= PropertyNameKeyLength ||
// We compare the whole name, although we could skip the first 6 bytes (but it's likely not any faster)
propertyName.SequenceEqual(propertyRef.Info.Name))
{
info = propertyRef.Info;
Expand All @@ -376,58 +406,87 @@ private static bool TryIsPropertyRefEqual(in PropertyRef propertyRef, ReadOnlySp
return false;
}

private static bool IsPropertyRefEqual(ref PropertyRef propertyRef, PropertyRef other)
{
if (propertyRef.Key == other.Key)
{
if (propertyRef.Info.Name.Length <= PropertyNameKeyLength ||
propertyRef.Info.Name.AsSpan().SequenceEqual(other.Info.Name.AsSpan()))
{
return true;
}
}

return false;
}

/// <summary>
/// Get a key from the property name.
/// The key consists of the first 7 bytes of the property name and then the length.
/// </summary>
public static ulong GetKey(ReadOnlySpan<byte> propertyName)
{
const int BitsInByte = 8;
ulong key;
int length = propertyName.Length;

// Embed the propertyName in the first 6 bytes of the key.
if (length > 3)
if (length > 7)
{
key = MemoryMarshal.Read<ulong>(propertyName);

// Max out the length byte.
// The comparison logic tests for equality against the full contents instead of just
// the key if the property name length is >7.
key |= 0xFF00000000000000;
}
else if (length > 3)
{
key = MemoryMarshal.Read<uint>(propertyName);
if (length > 4)

if (length == 7)
{
key |= (ulong) propertyName[6] << (6 * BitsInByte)
| (ulong) propertyName[5] << (5 * BitsInByte)
| (ulong) propertyName[4] << (4 * BitsInByte)
| (ulong) 7 << (7 * BitsInByte);
}
else if (length == 6)
{
key |= (ulong) propertyName[5] << (5 * BitsInByte)
| (ulong) propertyName[4] << (4 * BitsInByte)
| (ulong) 6 << (7 * BitsInByte);
}
else if (length == 5)
{
key |= (ulong)propertyName[4] << 32;
key |= (ulong) propertyName[4] << (4 * BitsInByte)
| (ulong) 5 << (7 * BitsInByte);
}
if (length > 5)
else
{
key |= (ulong)propertyName[5] << 40;
key |= (ulong) 4 << (7 * BitsInByte);
}
}
else if (length > 1)
{
key = MemoryMarshal.Read<ushort>(propertyName);
if (length > 2)

if (length == 3)
{
key |= (ulong)propertyName[2] << 16;
key |= (ulong) propertyName[2] << (2 * BitsInByte)
| (ulong) 3 << (7 * BitsInByte);
}
else
{
key |= (ulong) 2 << (7 * BitsInByte);
}
}
else if (length == 1)
{
key = propertyName[0];
key = propertyName[0]
| (ulong) 1 << (7 * BitsInByte);
}
else
{
// An empty name is valid.
key = 0;
}

// Embed the propertyName length in the last two bytes.
key |= (ulong)propertyName.Length << 48;
// Verify key contains the embedded bytes as expected.
Debug.Assert(
(length < 1 || propertyName[0] == ((key & ((ulong)0xFF << 8 * 0)) >> 8 * 0)) &&
(length < 2 || propertyName[1] == ((key & ((ulong)0xFF << 8 * 1)) >> 8 * 1)) &&
(length < 3 || propertyName[2] == ((key & ((ulong)0xFF << 8 * 2)) >> 8 * 2)) &&
(length < 4 || propertyName[3] == ((key & ((ulong)0xFF << 8 * 3)) >> 8 * 3)) &&
(length < 5 || propertyName[4] == ((key & ((ulong)0xFF << 8 * 4)) >> 8 * 4)) &&
(length < 6 || propertyName[5] == ((key & ((ulong)0xFF << 8 * 5)) >> 8 * 5)) &&
(length < 7 || propertyName[6] == ((key & ((ulong)0xFF << 8 * 6)) >> 8 * 6)));

return key;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@
using System.Buffers;
using System.Collections;
using System.Diagnostics;
using System.Runtime.CompilerServices;

namespace System.Text.Json
{
public static partial class JsonSerializer
{
// AggressiveInlining used although a large method it is only called from one locations and is on a hot path.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void HandlePropertyName(
JsonSerializerOptions options,
ref Utf8JsonReader reader,
Expand Down Expand Up @@ -52,7 +55,7 @@ private static void HandlePropertyName(
}

JsonPropertyInfo jsonPropertyInfo = state.Current.JsonClassInfo.GetProperty(propertyName, ref state.Current);
if (jsonPropertyInfo == null)
if (jsonPropertyInfo == JsonPropertyInfo.s_missingProperty)
{
JsonPropertyInfo dataExtProperty = state.Current.JsonClassInfo.DataExtensionProperty;
if (dataExtProperty == null)
Expand Down Expand Up @@ -94,9 +97,10 @@ private static void HandlePropertyName(
state.Current.JsonPropertyInfo.JsonPropertyName = propertyNameArray;
}
}

state.Current.PropertyIndex++;
}

// Increment the PropertyIndex so JsonClassInfo.GetProperty() starts with the next property.
state.Current.PropertyIndex++;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Runtime.CompilerServices;

namespace System.Text.Json
{
public static partial class JsonSerializer
{
private static bool HandleValue(JsonTokenType tokenType, JsonSerializerOptions options, ref Utf8JsonReader reader, ref ReadStack state)
// AggressiveInlining used although a large method it is only called from two locations and is on a hot path.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void HandleValue(JsonTokenType tokenType, JsonSerializerOptions options, ref Utf8JsonReader reader, ref ReadStack state)
{
if (state.Current.SkipProperty)
{
return false;
return;
}

JsonPropertyInfo jsonPropertyInfo = state.Current.JsonPropertyInfo;
Expand All @@ -23,10 +27,7 @@ private static bool HandleValue(JsonTokenType tokenType, JsonSerializerOptions o
jsonPropertyInfo = state.Current.JsonClassInfo.CreatePolymorphicProperty(jsonPropertyInfo, typeof(object), options);
}

bool lastCall = (!state.Current.IsProcessingEnumerableOrDictionary && state.Current.ReturnValue == null);

jsonPropertyInfo.Read(tokenType, ref state, ref reader);
return lastCall;
}
}
}
Loading

0 comments on commit e707e2c

Please sign in to comment.