Move the cache for the marshaler methods to be stored on the RuntimeType instead of in a ConditionalWeakTable#126621
Move the cache for the marshaler methods to be stored on the RuntimeType instead of in a ConditionalWeakTable#126621jkoritzinsky wants to merge 3 commits intodotnet:mainfrom
Conversation
…ype instead of in a ConditionalWeakTable Should fix dotnet#126608
|
@MihuBot benchmark Interop.StructureToPtr |
|
Tagging subscribers to this area: @dotnet/area-system-reflection |
There was a problem hiding this comment.
Pull request overview
This PR moves the cache for Marshal.LayoutTypeMarshalerMethods from a ConditionalWeakTable<Type, …> to the per-RuntimeType generic cache infrastructure to address the reported interop performance regressions (issue #126608).
Changes:
- Adds
LayoutTypeMarshalerMethodsstorage toRuntimeType.CompositeCacheEntryso it can live in the existingRuntimeTypegeneric cache. - Refactors
LayoutTypeMarshalerMethodsto implementRuntimeType.IGenericCacheEntry<T>and retrieves it viaRuntimeType.GetOrCreateCacheEntry<T>(). - Switches from delegate creation (
CreateDelegate) to caching raw function pointers (MethodHandle.GetFunctionPointer) for the marshaler methods.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
src/coreclr/System.Private.CoreLib/src/System/RuntimeType.GenericCache.cs |
Adds a new composite-cache field to hold the marshaler-methods cache on RuntimeType. |
src/coreclr/System.Private.CoreLib/src/System/Runtime/InteropServices/Marshal.CoreCLR.cs |
Reworks LayoutTypeMarshalerMethods to use RuntimeType cache entries and cached function pointers instead of a ConditionalWeakTable. |
src/coreclr/System.Private.CoreLib/src/System/Runtime/InteropServices/Marshal.CoreCLR.cs
Outdated
Show resolved
Hide resolved
Interop.StructureToPtr
|
|
@MihuBot benchmark Interop.StructureToPtr |
Interop.StructureToPtr
|
| [RequiresDynamicCode("Marshalling code for the object might not be available.")] | ||
| internal static LayoutTypeMarshalerMethods GetMarshalMethodsForType(Type t) | ||
| public static LayoutTypeMarshalerMethods Create(RuntimeType type) | ||
| { | ||
| return s_marshalerCache.GetOrAdd(t, CreateMarshalMethods); | ||
| if (!HasLayout(new QCallTypeHandle(ref type), out _, out int size)) | ||
| throw new ArgumentException(SR.Argument_MustHaveLayoutOrBeBlittable, nameof(type)); | ||
|
|
||
| static LayoutTypeMarshalerMethods CreateMarshalMethods(Type type) | ||
| { | ||
| Type instantiatedMarshaler = typeof(BoxedLayoutTypeMarshaler<>).MakeGenericType([type]); | ||
| return new LayoutTypeMarshalerMethods(instantiatedMarshaler); | ||
| } | ||
| Type instantiatedMarshaler = typeof(BoxedLayoutTypeMarshaler<>).MakeGenericType([type]); | ||
| return new LayoutTypeMarshalerMethods(instantiatedMarshaler, size); | ||
| } |
There was a problem hiding this comment.
LayoutTypeMarshalerMethods.Create now throws ArgumentException(..., nameof(type)) when HasLayout is false. This exception can bubble out of public APIs like StructureToPtr / DestroyStructure, changing the observable ParamName from the prior values (structure / structuretype). Consider catching and rethrowing with the original parameter name at the public entrypoints (or in GetMarshalMethodsForType) so API behavior stays consistent, while keeping the per-type cache creation.
| public static unsafe void StructureToPtr(object structure, IntPtr ptr, bool fDeleteOld) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(ptr); | ||
| ArgumentNullException.ThrowIfNull(structure); | ||
|
|
||
| RuntimeType type = (RuntimeType)structure.GetType(); | ||
|
|
||
| if (type.IsGenericType) | ||
| throw new ArgumentException(SR.Argument_NeedNonGenericObject, nameof(structure)); | ||
|
|
||
| if (!HasLayout(new QCallTypeHandle(ref type), out bool isBlittable, out int size)) | ||
| throw new ArgumentException(SR.Argument_MustHaveLayoutOrBeBlittable, nameof(structure)); | ||
|
|
||
| if (isBlittable) | ||
| { | ||
| SpanHelpers.Memmove(ref *(byte*)ptr, ref structure.GetRawData(), (nuint)size); | ||
| return; | ||
| } | ||
|
|
||
| LayoutTypeMarshalerMethods methods = LayoutTypeMarshalerMethods.GetMarshalMethodsForType(type); | ||
|
|
||
| if (fDeleteOld) | ||
| { | ||
| methods.Free(structure, (byte*)ptr, size, ref Unsafe.NullRef<CleanupWorkListElement?>()); | ||
| methods.Free((byte*)ptr); | ||
| } | ||
|
|
||
| methods.ConvertToUnmanaged(structure, (byte*)ptr, size, ref Unsafe.NullRef<CleanupWorkListElement?>()); | ||
| methods.ConvertToUnmanaged(structure, (byte*)ptr, ref Unsafe.NullRef<CleanupWorkListElement?>()); | ||
| } |
There was a problem hiding this comment.
The previous fast-path for blittable layout types (calling HasLayout(..., out isBlittable, out size) and doing a direct SpanHelpers.Memmove) was removed. The new path always routes through LayoutTypeMarshalerMethods / BoxedLayoutTypeMarshaler, which (for blittable structs/classes) performs extra delegate dispatch and also zeros the destination buffer (NativeMemory.Clear) before copying. This is a measurable perf/behavior change for the blittable case; if the intent of this PR is to address perf regressions, consider reintroducing a blittable fast-path (potentially using cached isBlittable + nativeSize in the RuntimeType cache entry) so blittable types remain a simple memmove without clearing.
| public static unsafe void DestroyStructure(IntPtr ptr, Type structuretype) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(ptr); | ||
| ArgumentNullException.ThrowIfNull(structuretype); | ||
|
|
||
| if (structuretype is not RuntimeType rt) | ||
| throw new ArgumentException(SR.Argument_MustBeRuntimeType, nameof(structuretype)); | ||
|
|
||
| if (rt.IsGenericType) | ||
| throw new ArgumentException(SR.Argument_NeedNonGenericType, nameof(structuretype)); | ||
|
|
||
| if (!HasLayout(new QCallTypeHandle(ref rt), out bool isBlittable, out int size)) | ||
| throw new ArgumentException(SR.Argument_MustHaveLayoutOrBeBlittable, nameof(structuretype)); | ||
| LayoutTypeMarshalerMethods methods = LayoutTypeMarshalerMethods.GetMarshalMethodsForType(rt); | ||
|
|
||
| if (!isBlittable) | ||
| { | ||
| LayoutTypeMarshalerMethods methods = LayoutTypeMarshalerMethods.GetMarshalMethodsForType(structuretype); | ||
|
|
||
| methods.Free(null, (byte*)ptr, size, ref Unsafe.NullRef<CleanupWorkListElement?>()); | ||
| } | ||
| methods.Free((byte*)ptr); | ||
| } |
There was a problem hiding this comment.
DestroyStructure previously did nothing for blittable types; it now always calls methods.Free((byte*)ptr). For blittable structs, StructureMarshaler<T>.Free clears the native memory (NativeMemory.Clear), which is a behavioral change and adds work on a hot path. Consider preserving the old semantics by skipping Free entirely for blittable types (again, this could be driven off cached isBlittable in the per-RuntimeType cache entry).
| internal static unsafe void LayoutTypeConvertToUnmanaged(object obj, byte* pNative, ref CleanupWorkListElement? pCleanupWorkList) | ||
| { | ||
| RuntimeType type = (RuntimeType)obj.GetType(); | ||
| bool hasLayout = Marshal.HasLayout(new QCallTypeHandle(ref type), out bool isBlittable, out int size); | ||
| Debug.Assert(hasLayout); | ||
|
|
||
| if (isBlittable) | ||
| { | ||
| SpanHelpers.Memmove(ref *pNative, ref obj.GetRawData(), (nuint)size); | ||
| return; | ||
| } | ||
|
|
||
| Marshal.LayoutTypeMarshalerMethods methods = Marshal.LayoutTypeMarshalerMethods.GetMarshalMethodsForType(type); | ||
|
|
||
| methods.ConvertToUnmanaged(obj, pNative, size, ref pCleanupWorkList); | ||
| methods.ConvertToUnmanaged(obj, pNative, ref pCleanupWorkList); | ||
| } |
There was a problem hiding this comment.
The previous blittable fast-path was removed here as well. LayoutTypeConvertToUnmanaged now always goes through Marshal.LayoutTypeMarshalerMethods, which for blittable types introduces extra indirection and (via StructureMarshaler / LayoutClassMarshaler) may clear the native buffer before copying. If these helpers are used on interop hot paths, consider restoring a blittable memmove fast-path using cached layout info to avoid the extra work.
| internal static LayoutTypeMarshalerMethods GetMarshalMethodsForType(RuntimeType t) | ||
| { | ||
| return t.GetOrCreateCacheEntry<LayoutTypeMarshalerMethods>(); | ||
| } |
There was a problem hiding this comment.
LayoutTypeMarshalerMethods.GetMarshalMethodsForType(RuntimeType) is no longer annotated with [RequiresDynamicCode], even though it can trigger MakeGenericType / delegate creation via the cache entry’s Create method. Previously the "get" helper carried the annotation, which helps flow the requirement to internal callers (e.g., StubHelpers.LayoutTypeConvertTo*). Consider restoring [RequiresDynamicCode] on GetMarshalMethodsForType (or otherwise ensuring the requirement is correctly surfaced) so trimming/AOT analysis remains accurate.
Fixes #126608
Move the marshalling info to be cached on the type itself instead of in a ConditionalWeakTable.
Move some of the validation (in particular the "has layout" validation) to be done only at "create" time so we don't have to rerun it for every time someone calls the Marshal methods with the same (valid) type.
Use the blittable fallbacks on
StructureMarshaler<T>andLayoutClassMarshaler<T>instead of inlining the calls. The cost may go up slightly (delegate call to a method that callsSpanHelpers.Memmoveinstead of direct call toSpanHelpers.Memmove) but still much faster than the existing implementation that does the "is blittable" checks each time.