Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Argon/Serialization/DefaultSerializationBinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ static Type GetTypeFromTypeNameKey(TypeNameKey key)
--scope;
if (scope == 0)
{
var typeArgAssemblyQualifiedName = typeName.Substring(typeArgStartIndex, i - typeArgStartIndex);
var typeArgAssemblyQualifiedName = typeName.AsSpan(typeArgStartIndex, i - typeArgStartIndex);

var typeNameKey = ReflectionUtils.SplitFullyQualifiedTypeName(typeArgAssemblyQualifiedName);
genericTypeArguments.Add(GetTypeByName(typeNameKey));
Expand Down
23 changes: 19 additions & 4 deletions src/Argon/Utilities/ReflectionUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -444,20 +444,35 @@ public static List<MemberInfo> GetFieldsAndProperties(this Type type, BindingFla

public static TypeNameKey SplitFullyQualifiedTypeName(string fullTypeName)
{
var assemblyDelimiterIndex = GetAssemblyDelimiterIndex(fullTypeName);
var assemblyDelimiterIndex = GetAssemblyDelimiterIndex(fullTypeName.AsSpan());

if (assemblyDelimiterIndex == null)
{
return new(null, fullTypeName);
}

var delimiterIndex = assemblyDelimiterIndex.Value;
var type = fullTypeName.Trim(0, delimiterIndex);
var assembly = fullTypeName.Trim(delimiterIndex + 1, fullTypeName.Length - delimiterIndex - 1);
var type = fullTypeName.AsSpan(0, delimiterIndex).Trim().ToString();
var assembly = fullTypeName.AsSpan(delimiterIndex + 1).Trim().ToString();
return new(assembly, type);
}

public static TypeNameKey SplitFullyQualifiedTypeName(CharSpan fullTypeName)
{
var assemblyDelimiterIndex = GetAssemblyDelimiterIndex(fullTypeName);

if (assemblyDelimiterIndex == null)
{
return new(null, fullTypeName.ToString());
}

var delimiterIndex = assemblyDelimiterIndex.Value;
var type = fullTypeName[..delimiterIndex].Trim().ToString();
var assembly = fullTypeName[(delimiterIndex + 1)..].Trim().ToString();
return new(assembly, type);
}

static int? GetAssemblyDelimiterIndex(string fullyQualifiedTypeName)
static int? GetAssemblyDelimiterIndex(CharSpan fullyQualifiedTypeName)
{
// we need to get the first comma following all surrounded in brackets because of generic types
// e.g. System.Collections.Generic.Dictionary`2[[System.String, mscorlib,Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
Expand Down
144 changes: 144 additions & 0 deletions src/ArgonTests/Benchmarks/SplitFullyQualifiedTypeNameBench.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// Copyright (c) 2007 James Newton-King. All rights reserved.
// Use of this source code is governed by The MIT License,
// as found in the license.md file.

using BenchmarkDotNet.Attributes;

[MemoryDiagnoser]
public class SplitFullyQualifiedTypeNameBench
{
// Mirrors the kind of value passed at DefaultSerializationBinder line ~121:
// a generic typename with nested assembly-qualified type-argument blocks.
const string TypeName =
"System.Collections.Generic.Dictionary`2[" +
"[System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]," +
"[System.Collections.Generic.List`1[" +
"[System.Int32, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]" +
"], mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]";

static readonly (int Start, int Length)[] argSlices = LocateArgSlices(TypeName);

static (int, int)[] LocateArgSlices(string typeName)
{
var openBracketIndex = typeName.IndexOf('[');
var slices = new List<(int, int)>();
var scope = 0;
var argStart = 0;
var endIndex = typeName.Length - 1;
for (var i = openBracketIndex + 1; i < endIndex; ++i)
{
switch (typeName[i])
{
case '[':
if (scope == 0)
{
argStart = i + 1;
}

++scope;
break;
case ']':
--scope;
if (scope == 0)
{
slices.Add((argStart, i - argStart));
}

break;
}
}

return slices.ToArray();
}

[Benchmark(Baseline = true)]
public int Old_SubstringPlusStringSplit()
{
var sum = 0;
foreach (var (start, length) in argSlices)
{
var arg = TypeName.Substring(start, length);
var key = OldSplitFullyQualifiedTypeName(arg);
sum += key.Type.Length + (key.Assembly?.Length ?? 0);
}

return sum;
}

[Benchmark]
public int New_AsSpanPlusSpanSplit()
{
var sum = 0;
foreach (var (start, length) in argSlices)
{
var arg = TypeName.AsSpan(start, length);
var key = ReflectionUtils.SplitFullyQualifiedTypeName(arg);
sum += key.Type.Length + (key.Assembly?.Length ?? 0);
}

return sum;
}

static TypeNameKey OldSplitFullyQualifiedTypeName(string fullTypeName)
{
var assemblyDelimiterIndex = OldGetAssemblyDelimiterIndex(fullTypeName);

if (assemblyDelimiterIndex == null)
{
return new(null, fullTypeName);
}

var delimiterIndex = assemblyDelimiterIndex.Value;
var type = OldTrim(fullTypeName, 0, delimiterIndex);
var assembly = OldTrim(fullTypeName, delimiterIndex + 1, fullTypeName.Length - delimiterIndex - 1);
return new(assembly, type);
}

static int? OldGetAssemblyDelimiterIndex(string fullyQualifiedTypeName)
{
var scope = 0;
for (var i = 0; i < fullyQualifiedTypeName.Length; i++)
{
switch (fullyQualifiedTypeName[i])
{
case '[':
scope++;
break;
case ']':
scope--;
break;
case ',':
if (scope == 0)
{
return i;
}

break;
}
}

return null;
}

static string OldTrim(string s, int start, int length)
{
var end = start + length - 1;
for (; start < end; start++)
{
if (!char.IsWhiteSpace(s[start]))
{
break;
}
}

for (; end >= start; end--)
{
if (!char.IsWhiteSpace(s[end]))
{
break;
}
}

return s.Substring(start, end - start + 1);
}
}
2 changes: 1 addition & 1 deletion src/Benchmark.Tests/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public static void Main(string[] args)
var attribute = (AssemblyFileVersionAttribute)typeof(JsonConvert).Assembly.GetCustomAttribute(typeof(AssemblyFileVersionAttribute))!;
Console.WriteLine($"Json.NET Version: {attribute.Version}");

var switcher = new BenchmarkSwitcher([typeof(WriteEscapedJavaScriptString), typeof(SerializeJTokenList), typeof(CamelCaseBenchmarks), typeof(ReadQuotedNumbers), typeof(WriteBase64Benchmark)]);
var switcher = new BenchmarkSwitcher([typeof(WriteEscapedJavaScriptString), typeof(SerializeJTokenList), typeof(CamelCaseBenchmarks), typeof(ReadQuotedNumbers), typeof(WriteBase64Benchmark), typeof(SplitFullyQualifiedTypeNameBench)]);
if (args.Length == 0)
{
switcher.Run(["*"]);
Expand Down
Loading