Skip to content

Commit

Permalink
Unify managed runtime type name parsers (#83484)
Browse files Browse the repository at this point in the history
Contributes to #72833 and #77868

The performance effect of this change on typical use of `Type.GetType` like `Type.GetType("MyType, MyAssembly")` is in the noise range. The typical use of `Type.GetType` spends most of the time in assembly loader and type loader. The time spent by parsing the type name is small fraction of the total and the performance improvement is hardly noticeable.

When the type name parser performance is measured in isolation, it is several times faster compared to the existing unmanaged CoreCLR type name parser. For example:
```
Type.GetType("System.Object, System.Private.CoreLib",
       assemblyResolver: (an) => typeof(object).Assembly,
       typeResolver: (assembly, name, ignoreCase) => typeof(object));
```
is about 3x faster with this change on CoreCLR.

Co-authored-by: Aaron Robinson <[email protected]>
  • Loading branch information
jkotas and AaronRobinsonMSFT authored Mar 25, 2023
1 parent 35a39e9 commit 91b93eb
Show file tree
Hide file tree
Showing 44 changed files with 1,488 additions and 2,329 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@
<Compile Include="$(BclSourcesRoot)\System\Reflection\RuntimeModule.cs" />
<Compile Include="$(BclSourcesRoot)\System\Reflection\RuntimeParameterInfo.cs" />
<Compile Include="$(BclSourcesRoot)\System\Reflection\RuntimePropertyInfo.cs" />
<Compile Include="$(BclSourcesRoot)\System\Reflection\TypeNameParser.CoreCLR.cs" />
<Compile Include="$(BclSourcesRoot)\System\Reflection\Metadata\RuntimeTypeMetadataUpdateHandler.cs" />
<Compile Include="$(BclSourcesRoot)\System\Resources\ManifestBasedResourceGroveler.CoreCLR.cs" />
<Compile Include="$(BclSourcesRoot)\System\Runtime\CompilerServices\CastHelpers.cs" />
Expand Down Expand Up @@ -237,7 +238,6 @@
<Compile Include="$(BclSourcesRoot)\System\Type.CoreCLR.cs" />
<Compile Include="$(BclSourcesRoot)\System\TypedReference.CoreCLR.cs" />
<Compile Include="$(BclSourcesRoot)\System\TypeLoadException.CoreCLR.cs" />
<Compile Include="$(BclSourcesRoot)\System\TypeNameParser.cs" />
<Compile Include="$(BclSourcesRoot)\System\ValueType.cs" />
<Compile Include="$(CommonPath)System\Collections\Generic\ArrayBuilder.cs">
<Link>Common\System\Collections\Generic\ArrayBuilder.cs</Link>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,37 +190,65 @@ public override MethodInfo? EntryPoint
}
}

[LibraryImport(RuntimeHelpers.QCall, EntryPoint = "AssemblyNative_GetType", StringMarshalling = StringMarshalling.Utf16)]
private static partial void GetType(QCallAssembly assembly,
string name,
[MarshalAs(UnmanagedType.Bool)] bool throwOnError,
[MarshalAs(UnmanagedType.Bool)] bool ignoreCase,
ObjectHandleOnStack type,
ObjectHandleOnStack keepAlive,
ObjectHandleOnStack assemblyLoadContext);
// For case-sensitive lookups, marshal the strings directly to Utf8 to avoid unnecessary string copies.
[LibraryImport(RuntimeHelpers.QCall, EntryPoint = "AssemblyNative_GetTypeCore", StringMarshalling = StringMarshalling.Utf8)]
private static partial void GetTypeCore(QCallAssembly assembly,
string typeName,
ReadOnlySpan<string> nestedTypeNames,
int nestedTypeNamesLength,
ObjectHandleOnStack retType);

[LibraryImport(RuntimeHelpers.QCall, EntryPoint = "AssemblyNative_GetTypeCoreIgnoreCase", StringMarshalling = StringMarshalling.Utf16)]
private static partial void GetTypeCoreIgnoreCase(QCallAssembly assembly,
string typeName,
ReadOnlySpan<string> nestedTypeNames,
int nestedTypeNamesLength,
ObjectHandleOnStack retType);

internal Type? GetTypeCore(string typeName, ReadOnlySpan<string> nestedTypeNames, bool throwOnError, bool ignoreCase)
{
RuntimeAssembly runtimeAssembly = this;
Type? type = null;

try
{
if (ignoreCase)
{
GetTypeCoreIgnoreCase(new QCallAssembly(ref runtimeAssembly),
typeName,
nestedTypeNames,
nestedTypeNames.Length,
ObjectHandleOnStack.Create(ref type));
}
else
{
GetTypeCore(new QCallAssembly(ref runtimeAssembly),
typeName,
nestedTypeNames,
nestedTypeNames.Length,
ObjectHandleOnStack.Create(ref type));
}
}
catch (FileNotFoundException) when (!throwOnError)
{
return null;
}

if (type == null && throwOnError)
throw new TypeLoadException(SR.Format(SR.ClassLoad_General /* TypeLoad_TypeNotFoundInAssembly */, typeName, FullName));

return type;
}

[RequiresUnreferencedCode("Types might be removed")]
public override Type? GetType(
string name, // throw on null strings regardless of the value of "throwOnError"
bool throwOnError, bool ignoreCase)
{
ArgumentNullException.ThrowIfNull(name);

RuntimeType? type = null;
object? keepAlive = null;
AssemblyLoadContext? assemblyLoadContextStack = AssemblyLoadContext.CurrentContextualReflectionContext;
ArgumentException.ThrowIfNullOrEmpty(name);

RuntimeAssembly runtimeAssembly = this;
GetType(new QCallAssembly(ref runtimeAssembly),
name,
throwOnError,
ignoreCase,
ObjectHandleOnStack.Create(ref type),
ObjectHandleOnStack.Create(ref keepAlive),
ObjectHandleOnStack.Create(ref assemblyLoadContextStack));
GC.KeepAlive(keepAlive);

return type;
return TypeNameParser.GetType(name, topLevelAssembly: this,
throwOnError: throwOnError, ignoreCase: ignoreCase);
}

[LibraryImport(RuntimeHelpers.QCall, EntryPoint = "AssemblyNative_GetExportedTypes")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -420,14 +420,10 @@ public override void GetObjectData(SerializationInfo info, StreamingContext cont
string className, // throw on null strings regardless of the value of "throwOnError"
bool throwOnError, bool ignoreCase)
{
ArgumentNullException.ThrowIfNull(className);
ArgumentException.ThrowIfNullOrEmpty(className);

RuntimeType? retType = null;
object? keepAlive = null;
RuntimeModule thisAsLocal = this;
GetType(new QCallModule(ref thisAsLocal), className, throwOnError, ignoreCase, ObjectHandleOnStack.Create(ref retType), ObjectHandleOnStack.Create(ref keepAlive));
GC.KeepAlive(keepAlive);
return retType;
return TypeNameParser.GetType(className, topLevelAssembly: Assembly,
throwOnError: throwOnError, ignoreCase: ignoreCase);
}

[RequiresAssemblyFiles(UnknownStringMessageInRAF)]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Loader;
using System.Text;
using System.Threading;

namespace System.Reflection
{
internal unsafe ref partial struct TypeNameParser
{
private Func<AssemblyName, Assembly?>? _assemblyResolver;
private Func<Assembly?, string, bool, Type?>? _typeResolver;
private bool _throwOnError;
private bool _ignoreCase;
private bool _extensibleParser;
private Assembly? _requestingAssembly;
private Assembly? _topLevelAssembly;

[RequiresUnreferencedCode("The type might be removed")]
internal static Type? GetType(
string typeName,
Assembly requestingAssembly,
bool throwOnError = false,
bool ignoreCase = false)
{
return GetType(typeName, assemblyResolver: null, typeResolver: null, requestingAssembly: requestingAssembly,
throwOnError: throwOnError, ignoreCase: ignoreCase, extensibleParser: false);
}

[RequiresUnreferencedCode("The type might be removed")]
internal static Type? GetType(
string typeName,
Func<AssemblyName, Assembly?>? assemblyResolver,
Func<Assembly?, string, bool, Type?>? typeResolver,
Assembly? requestingAssembly,
bool throwOnError = false,
bool ignoreCase = false,
bool extensibleParser = true)
{
ArgumentNullException.ThrowIfNull(typeName);

// Compat: Empty name throws TypeLoadException instead of
// the natural ArgumentException
if (typeName.Length == 0)
{
if (throwOnError)
throw new TypeLoadException(SR.Arg_TypeLoadNullStr);
return null;
}

return new TypeNameParser(typeName)
{
_assemblyResolver = assemblyResolver,
_typeResolver = typeResolver,
_throwOnError = throwOnError,
_ignoreCase = ignoreCase,
_extensibleParser = extensibleParser,
_requestingAssembly = requestingAssembly
}.Parse();
}

[RequiresUnreferencedCode("The type might be removed")]
internal static Type? GetType(
string typeName,
bool throwOnError,
bool ignoreCase,
Assembly topLevelAssembly)
{
return new TypeNameParser(typeName)
{
_throwOnError = throwOnError,
_ignoreCase = ignoreCase,
_topLevelAssembly = topLevelAssembly,
_requestingAssembly = topLevelAssembly
}.Parse();
}

private bool CheckTopLevelAssemblyQualifiedName()
{
if (_topLevelAssembly is not null)
{
if (_throwOnError)
throw new ArgumentException(SR.Argument_AssemblyGetTypeCannotSpecifyAssembly);
return false;
}
return true;
}

private Assembly? ResolveAssembly(string assemblyName)
{
Assembly? assembly;
if (_assemblyResolver is not null)
{
assembly = _assemblyResolver(new AssemblyName(assemblyName));
if (assembly is null && _throwOnError)
{
throw new FileNotFoundException(SR.Format(SR.FileNotFound_ResolveAssembly, assemblyName));
}
}
else
{
assembly = RuntimeAssembly.InternalLoad(new AssemblyName(assemblyName), ref Unsafe.NullRef<StackCrawlMark>(), AssemblyLoadContext.CurrentContextualReflectionContext,
requestingAssembly: (RuntimeAssembly?)_requestingAssembly, throwOnFileNotFound: _throwOnError);
}
return assembly;
}

[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode",
Justification = "TypeNameParser.GetType is marked as RequiresUnreferencedCode.")]
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2075:UnrecognizedReflectionPattern",
Justification = "TypeNameParser.GetType is marked as RequiresUnreferencedCode.")]
private Type? GetType(string typeName, ReadOnlySpan<string> nestedTypeNames, string? assemblyNameIfAny)
{
Assembly? assembly;

if (assemblyNameIfAny is not null)
{
assembly = ResolveAssembly(assemblyNameIfAny);
if (assembly is null)
return null;
}
else
{
assembly = _topLevelAssembly;
}

Type? type;

// Resolve the top level type.
if (_typeResolver is not null)
{
string escapedTypeName = EscapeTypeName(typeName);

type = _typeResolver(assembly, escapedTypeName, _ignoreCase);

if (type is null)
{
if (_throwOnError)
{
throw new TypeLoadException(assembly is null ?
SR.Format(SR.TypeLoad_ResolveType, escapedTypeName) :
SR.Format(SR.TypeLoad_ResolveTypeFromAssembly, escapedTypeName, assembly.FullName));
}
return null;
}
}
else
{
if (assembly is null)
{
return GetTypeFromDefaultAssemblies(typeName, nestedTypeNames);
}

if (assembly is RuntimeAssembly runtimeAssembly)
{
// Compat: Non-extensible parser allows ambiguous matches with ignore case lookup
if (!_extensibleParser || !_ignoreCase)
{
return runtimeAssembly.GetTypeCore(typeName, nestedTypeNames, throwOnError: _throwOnError, ignoreCase: _ignoreCase);
}
type = runtimeAssembly.GetTypeCore(typeName, default, throwOnError: _throwOnError, ignoreCase: _ignoreCase);
}
else
{
// This is a third-party Assembly object. Emulate GetTypeCore() by calling the public GetType()
// method. This is wasteful because it'll probably reparse a type string that we've already parsed
// but it can't be helped.
type = assembly.GetType(EscapeTypeName(typeName), throwOnError: _throwOnError, ignoreCase: _ignoreCase);
}

if (type is null)
return null;
}

for (int i = 0; i < nestedTypeNames.Length; i++)
{
BindingFlags bindingFlags = BindingFlags.NonPublic | BindingFlags.Public;
if (_ignoreCase)
bindingFlags |= BindingFlags.IgnoreCase;

type = type.GetNestedType(nestedTypeNames[i], bindingFlags);

if (type is null)
{
if (_throwOnError)
{
throw new TypeLoadException(SR.Format(SR.TypeLoad_ResolveNestedType,
nestedTypeNames[i], (i > 0) ? nestedTypeNames[i - 1] : typeName));
}
return null;
}
}

return type;
}

private Type? GetTypeFromDefaultAssemblies(string typeName, ReadOnlySpan<string> nestedTypeNames)
{
RuntimeAssembly? requestingAssembly = (RuntimeAssembly?)_requestingAssembly;
if (requestingAssembly is not null)
{
Type? type = ((RuntimeAssembly)requestingAssembly).GetTypeCore(typeName, nestedTypeNames, throwOnError: false, ignoreCase: _ignoreCase);
if (type is not null)
return type;
}

RuntimeAssembly coreLib = (RuntimeAssembly)typeof(object).Assembly;
if (requestingAssembly != coreLib)
{
Type? type = ((RuntimeAssembly)coreLib).GetTypeCore(typeName, nestedTypeNames, throwOnError: false, ignoreCase: _ignoreCase);
if (type is not null)
return type;
}

RuntimeAssembly? resolvedAssembly = AssemblyLoadContext.OnTypeResolve(requestingAssembly, EscapeTypeName(typeName, nestedTypeNames));
if (resolvedAssembly is not null)
{
Type? type = resolvedAssembly.GetTypeCore(typeName, nestedTypeNames, throwOnError: false, ignoreCase: _ignoreCase);
if (type is not null)
return type;
}

if (_throwOnError)
throw new TypeLoadException(SR.Format(SR.TypeLoad_ResolveTypeFromAssembly, EscapeTypeName(typeName), (requestingAssembly ?? coreLib).FullName));

return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -544,12 +544,6 @@ private static partial void GetTypeByName(string name, [MarshalAs(UnmanagedType.
ObjectHandleOnStack assemblyLoadContext,
ObjectHandleOnStack type, ObjectHandleOnStack keepalive);

// Wrapper function to reduce the need for ifdefs.
internal static RuntimeType? GetTypeByName(string name, bool throwOnError, bool ignoreCase, ref StackCrawlMark stackMark)
{
return GetTypeByName(name, throwOnError, ignoreCase, ref stackMark, AssemblyLoadContext.CurrentContextualReflectionContext!);
}

internal static RuntimeType? GetTypeByName(string name, bool throwOnError, bool ignoreCase, ref StackCrawlMark stackMark,
AssemblyLoadContext assemblyLoadContext)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1779,15 +1779,6 @@ internal FieldInfo GetField(RuntimeFieldHandleInternal field)
#region Static Members

#region Internal
internal static RuntimeType? GetType(string typeName, bool throwOnError, bool ignoreCase,
ref StackCrawlMark stackMark)
{
ArgumentNullException.ThrowIfNull(typeName);

return RuntimeTypeHandle.GetTypeByName(
typeName, throwOnError, ignoreCase, ref stackMark);
}

[RequiresUnreferencedCode("Trimming changes metadata tokens")]
internal static MethodBase? GetMethodBase(RuntimeModule scope, int typeMetadataToken)
{
Expand All @@ -1808,7 +1799,7 @@ internal FieldInfo GetField(RuntimeFieldHandleInternal field)

[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2070:UnrecognizedReflectionPattern",
Justification = "The code in this method looks up the method by name, but it always starts with a method handle." +
"To get here something somwhere had to get the method handle and thus the method must exist.")]
"To get here something somewhere had to get the method handle and thus the method must exist.")]
internal static MethodBase? GetMethodBase(RuntimeType? reflectedType, RuntimeMethodHandleInternal methodHandle)
{
Debug.Assert(!methodHandle.IsNullHandle());
Expand Down
Loading

0 comments on commit 91b93eb

Please sign in to comment.