Skip to content

TypeName.FullName: reduce allocations #112350

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

Merged
merged 4 commits into from
Feb 12, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,17 @@ namespace System.Reflection
internal static class AssemblyNameFormatter
{
public static string ComputeDisplayName(string name, Version? version, string? cultureName, byte[]? pkt, AssemblyNameFlags flags = 0, AssemblyContentType contentType = 0, byte[]? pk = null)
{
ValueStringBuilder vsb = new(stackalloc char[256]);
AppendDisplayName(ref vsb, name, version, cultureName, pkt, flags, contentType, pk);
return vsb.ToString();
}

public static void AppendDisplayName(ref ValueStringBuilder vsb, string name, Version? version, string? cultureName, byte[]? pkt, AssemblyNameFlags flags = 0, AssemblyContentType contentType = 0, byte[]? pk = null)
{
const int PUBLIC_KEY_TOKEN_LEN = 8;
Debug.Assert(name.Length != 0);

var vsb = new ValueStringBuilder(stackalloc char[256]);
vsb.AppendQuoted(name);

if (version != null)
Expand Down Expand Up @@ -89,8 +95,6 @@ public static string ComputeDisplayName(string name, Version? version, string? c
vsb.Append(", ContentType=WindowsRuntime");

// NOTE: By design (desktop compat) AssemblyName.FullName and ToString() do not include ProcessorArchitecture.

return vsb.ToString();
}

private static void AppendQuoted(this ref ValueStringBuilder vsb, string s)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,23 +112,37 @@ public string FullName
{
if (_fullName is null)
{
bool isPublicKey = (Flags & AssemblyNameFlags.PublicKey) != 0;
ValueStringBuilder vsb = new(stackalloc char[256]);
AppendFullName(ref vsb);
_fullName = vsb.ToString();
}

byte[]? publicKeyOrToken =
return _fullName;
}
}

internal void AppendFullName(ref ValueStringBuilder vsb)
{
if (_fullName is not null)
{
vsb.Append(_fullName);
}
else
{
bool isPublicKey = (Flags & AssemblyNameFlags.PublicKey) != 0;

byte[]? publicKeyOrToken =
#if SYSTEM_PRIVATE_CORELIB
PublicKeyOrToken;
#elif NET8_0_OR_GREATER
!PublicKeyOrToken.IsDefault ? Runtime.InteropServices.ImmutableCollectionsMarshal.AsArray(PublicKeyOrToken) : null;
#else
!PublicKeyOrToken.IsDefault ? PublicKeyOrToken.ToArray() : null;
#endif
_fullName = AssemblyNameFormatter.ComputeDisplayName(Name, Version, CultureName,
pkt: isPublicKey ? null : publicKeyOrToken,
ExtractAssemblyNameFlags(_flags), ExtractAssemblyContentType(_flags),
pk: isPublicKey ? publicKeyOrToken : null);
}

return _fullName;
AssemblyNameFormatter.AppendDisplayName(ref vsb, Name, Version, CultureName,
pkt: isPublicKey ? null : publicKeyOrToken,
ExtractAssemblyNameFlags(_flags), ExtractAssemblyContentType(_flags),
pk: isPublicKey ? publicKeyOrToken : null);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,39 @@ private TypeName(string? fullName,
/// If <see cref="AssemblyName"/> returns null, simply returns <see cref="FullName"/>.
/// </remarks>
public string AssemblyQualifiedName
=> _assemblyQualifiedName ??= AssemblyName is null ? FullName : $"{FullName}, {AssemblyName.FullName}"; // see recursion comments in FullName
{
get
{
if (_assemblyQualifiedName is null)
{
if (_fullName is not null && AssemblyName is null)
{
// _fullName may carry more information than FullName property, so we need to use FullName property.
_assemblyQualifiedName = FullName;
}
else
{
ValueStringBuilder builder = new(stackalloc char[256]);
AppendFullName(ref builder); // see recursion comments in AppendFullName
if (AssemblyName is not null)
{
builder.Append(", ");
AssemblyName.AppendFullName(ref builder);
}
_assemblyQualifiedName = builder.ToString();

if (AssemblyName is null)
{
// If the type name was not created from a assembly-qualified name,
// the FullName and AssemblyQualifiedName are the same.
_fullName = _assemblyQualifiedName;
}
}
}

return _assemblyQualifiedName;
}
}

/// <summary>
/// Returns assembly name which contains this type, or null if this <see cref="TypeName"/> was not
Expand Down Expand Up @@ -142,38 +174,11 @@ public string FullName
{
get
{
// This is a recursive method over potentially hostile input. Protection against DoS is offered
// via the [Try]Parse method and TypeNameParserOptions.MaxNodes property at construction time.
// This FullName property getter and related methods assume that this TypeName instance has an
// acceptable node count.
//
// The node count controls the total amount of work performed by this method, including:
// - The max possible stack depth due to the recursive methods calls; and
// - The total number of bytes allocated by this function. For a deeply-nested TypeName
// object, the total allocation across the full object graph will be
// O(FullName.Length * GetNodeCount()).

if (_fullName is null)
{
if (IsConstructedGenericType)
{
_fullName = TypeNameParserHelpers.GetGenericTypeFullName(GetGenericTypeDefinition().FullName.AsSpan(),
#if SYSTEM_PRIVATE_CORELIB
CollectionsMarshal.AsSpan(_genericArguments));
#else
_genericArguments.AsSpan());
#endif
}
else if (IsArray || IsPointer || IsByRef)
{
ValueStringBuilder builder = new(stackalloc char[128]);
builder.Append(GetElementType().FullName);
_fullName = TypeNameParserHelpers.GetRankOrModifierStringRepresentation(_rankOrModifier, ref builder);
}
else
{
Debug.Fail("Pre-allocated full name should have been provided in the ctor");
}
ValueStringBuilder builder = new(stackalloc char[128]);
AppendFullName(ref builder);
_fullName = builder.ToString();
}
else if (_nestedNameLength > 0 && _fullName.Length > _nestedNameLength) // Declaring types
{
Expand All @@ -186,6 +191,58 @@ public string FullName
}
}

private void AppendFullName(ref ValueStringBuilder builder)
{
// This is a recursive method over potentially hostile input. Protection against DoS is offered
// via the [Try]Parse method and TypeNameParserOptions.MaxNodes property at construction time.
// This FullName property getter and related methods assume that this TypeName instance has an
// acceptable node count.
//
// The node count controls the total amount of work performed by this method, including:
// - The max possible stack depth due to the recursive methods calls.

if (_fullName is null)
{
if (IsConstructedGenericType)
{
GetGenericTypeDefinition().AppendFullName(ref builder);
builder.Append('[');
foreach (TypeName genericArg in GetGenericArguments())
{
builder.Append('[');
genericArg.AppendFullName(ref builder);
// Generic arguments need to be always fully qualified.
if (genericArg.AssemblyName is not null)
{
builder.Append(", ");
genericArg.AssemblyName.AppendFullName(ref builder);
}
builder.Append("],");
}
builder[builder.Length - 1] = ']'; // replace ',' with ']'
}
else if (IsArray || IsPointer || IsByRef)
{
GetElementType().AppendFullName(ref builder);
TypeNameParserHelpers.AppendRankOrModifierStringRepresentation(_rankOrModifier, ref builder);
}
else
{
Debug.Fail("Pre-allocated full name should have been provided in the ctor");
}
}
else if (_nestedNameLength > 0 && _fullName.Length > _nestedNameLength) // Declaring types
{
// Stored fullName represents the full name of the nested type.
// Example: Namespace.Declaring+Nested
builder.Append(_fullName.AsSpan(0, _nestedNameLength));
}
else
{
builder.Append(_fullName);
}
}

/// <summary>
/// Returns true if this type represents any kind of array, regardless of the array's
/// rank or its bounds.
Expand Down Expand Up @@ -258,48 +315,54 @@ public string Name
{
get
{
// Lookups to Name and FullName might be recursive. See comments in FullName property getter.

if (_name is null)
{
if (IsConstructedGenericType)
{
_name = GetGenericTypeDefinition().Name;
}
else if (IsPointer || IsByRef || IsArray)
{
ValueStringBuilder builder = new(stackalloc char[64]);
builder.Append(GetElementType().Name);
_name = TypeNameParserHelpers.GetRankOrModifierStringRepresentation(_rankOrModifier, ref builder);
}
else
{
// _fullName can be null only in constructed generic or modified types, which we handled above.
Debug.Assert(_fullName is not null);
ReadOnlySpan<char> name = _fullName.AsSpan();
if (_nestedNameLength > 0)
{
name = name.Slice(0, _nestedNameLength);
}
if (IsNested)
{
// If the type is nested, we know the length of the declaring type's full name.
// Get the characters after that plus one for the '+' separator.
name = name.Slice(_declaringType._nestedNameLength + 1);
}
else if (TypeNameParserHelpers.IndexOfNamespaceDelimiter(name) is int idx && idx >= 0)
{
// If the type is not nested, find the namespace delimiter in the full name and return the substring after it.
name = name.Slice(idx + 1);
}
_name = name.ToString();
}
ValueStringBuilder builder = new(stackalloc char[64]);
AppendName(ref builder);
_name = builder.ToString();
}

return _name;
}
}

private void AppendName(ref ValueStringBuilder builder)
{
// Lookups to Name and FullName might be recursive. See comments in AppendFullName method.

if (IsConstructedGenericType)
{
GetGenericTypeDefinition().AppendName(ref builder);
}
else if (IsPointer || IsByRef || IsArray)
{
GetElementType().AppendName(ref builder);
TypeNameParserHelpers.AppendRankOrModifierStringRepresentation(_rankOrModifier, ref builder);
}
else
{
// _fullName can be null only in constructed generic or modified types, which we handled above.
Debug.Assert(_fullName is not null);
ReadOnlySpan<char> name = _fullName.AsSpan();
if (_nestedNameLength > 0)
{
name = name.Slice(0, _nestedNameLength);
}
if (IsNested)
{
// If the type is nested, we know the length of the declaring type's full name.
// Get the characters after that plus one for the '+' separator.
name = name.Slice(_declaringType._nestedNameLength + 1);
}
else if (TypeNameParserHelpers.IndexOfNamespaceDelimiter(name) is int idx && idx >= 0)
{
// If the type is not nested, find the namespace delimiter in the full name and return the substring after it.
name = name.Slice(idx + 1);
}
builder.Append(name);
}
}

/// <summary>
/// The namespace of this type; e.g., "System".
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,27 +19,6 @@ internal static class TypeNameParserHelpers
// Keep this in sync with GetFullTypeNameLength/NeedsEscaping
private static readonly SearchValues<char> s_endOfFullTypeNameDelimitersSearchValues = SearchValues.Create("[]&*,+\\");
#endif

internal static string GetGenericTypeFullName(ReadOnlySpan<char> fullTypeName, ReadOnlySpan<TypeName> genericArgs)
{
Debug.Assert(genericArgs.Length > 0);

ValueStringBuilder result = new(stackalloc char[128]);
result.Append(fullTypeName);

result.Append('[');
foreach (TypeName genericArg in genericArgs)
{
result.Append('[');
result.Append(genericArg.AssemblyQualifiedName); // see recursion comments in TypeName.FullName
result.Append(']');
result.Append(',');
}
result[result.Length - 1] = ']'; // replace ',' with ']'

return result.ToString();
}

/// <returns>Positive length or negative value for invalid name</returns>
internal static int GetFullTypeNameLength(ReadOnlySpan<char> input, out bool isNestedType)
{
Expand Down Expand Up @@ -186,7 +165,7 @@ static int GetUnescapedOffset(ReadOnlySpan<char> input, int startIndex)
}
}

internal static string GetRankOrModifierStringRepresentation(int rankOrModifier, ref ValueStringBuilder builder)
internal static void AppendRankOrModifierStringRepresentation(int rankOrModifier, ref ValueStringBuilder builder)
{
if (rankOrModifier == ByRef)
{
Expand Down Expand Up @@ -219,8 +198,6 @@ internal static string GetRankOrModifierStringRepresentation(int rankOrModifier,
builder.Append(',', rankOrModifier - 1);
builder.Append(']');
}

return builder.ToString();
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,22 +66,8 @@ public void GetAssemblyNameCandidateReturnsExpectedValue(string input, string ex
public void AppendRankOrModifierStringRepresentationAppendsExpectedString(int input, string expected)
{
ValueStringBuilder builder = new ValueStringBuilder(initialCapacity: 10);
Assert.Equal(expected, TypeNameParserHelpers.GetRankOrModifierStringRepresentation(input, ref builder));
}

[Theory]
[InlineData(typeof(List<int>))]
[InlineData(typeof(int?))]
[InlineData(typeof(List<string>))]
[InlineData(typeof(Dictionary<string, DateTime>))]
[InlineData(typeof(ValueTuple<bool, short, int, DateTime>))]
[InlineData(typeof(ValueTuple<bool, short, int, DateTime, char, ushort, long, sbyte>))]
public void GetGenericTypeFullNameReturnsSameStringAsTypeAPI(Type genericType)
{
TypeName openGenericTypeName = TypeName.Parse(genericType.GetGenericTypeDefinition().FullName.AsSpan());
ReadOnlySpan<TypeName> genericArgNames = genericType.GetGenericArguments().Select(arg => TypeName.Parse(arg.AssemblyQualifiedName.AsSpan())).ToArray();

Assert.Equal(genericType.FullName, TypeNameParserHelpers.GetGenericTypeFullName(openGenericTypeName.FullName.AsSpan(), genericArgNames));
TypeNameParserHelpers.AppendRankOrModifierStringRepresentation(input, ref builder);
Assert.Equal(expected, builder.ToString());
}

[Theory]
Expand Down
Loading