From 1254af8bbba136c5b0244ea93d93541f7c5b870c Mon Sep 17 00:00:00 2001 From: Ari Vuollet Date: Sun, 13 Aug 2023 14:18:57 +0300 Subject: [PATCH] Optimize UnboxValue performance, safety and memory usage - Avoids unnecessary boxing of the converted values by storing them in unmanaged memory. - Wrap ToNative-method in a delegate and cache it - Fixes returning address to unpinned memory by pinning POD-types for a short period of time. --- .../Engine/Engine/NativeInterop.Unmanaged.cs | 1 - Source/Engine/Engine/NativeInterop.cs | 63 ++++++++++++++----- 2 files changed, 49 insertions(+), 15 deletions(-) diff --git a/Source/Engine/Engine/NativeInterop.Unmanaged.cs b/Source/Engine/Engine/NativeInterop.Unmanaged.cs index d2a8ca55e..af739b55e 100644 --- a/Source/Engine/Engine/NativeInterop.Unmanaged.cs +++ b/Source/Engine/Engine/NativeInterop.Unmanaged.cs @@ -650,7 +650,6 @@ namespace FlaxEngine.Interop if (!type.IsValueType) return ManagedHandle.ToIntPtr(handle); - // HACK: Get the address of a non-pinned value return ValueTypeUnboxer.GetPointer(value, type); } diff --git a/Source/Engine/Engine/NativeInterop.cs b/Source/Engine/Engine/NativeInterop.cs index da372ac90..de2afec04 100644 --- a/Source/Engine/Engine/NativeInterop.cs +++ b/Source/Engine/Engine/NativeInterop.cs @@ -17,6 +17,7 @@ using FlaxEngine.Assertions; using System.Collections.Concurrent; using System.IO; using System.Text; +using System.Threading; namespace FlaxEngine.Interop { @@ -1009,44 +1010,78 @@ namespace FlaxEngine.Interop internal static class ValueTypeUnboxer { - private delegate IntPtr UnboxerDelegate(object value); + private static GCHandle[] pinnedBoxedValues = new GCHandle[256]; + private static uint pinnedBoxedValuesPointer = 0; + private static (IntPtr ptr, int size)[] pinnedAllocations = new (IntPtr ptr, int size)[256]; + private static uint pinnedAllocationsPointer = 0; + + private delegate TInternal ToNativeDelegate(T value); + private delegate IntPtr UnboxerDelegate(object value, object converter); - private static ConcurrentDictionary unboxers = new ConcurrentDictionary(1, 3); + private static ConcurrentDictionary unboxers = new (1, 3); private static MethodInfo unboxerMethod = typeof(ValueTypeUnboxer).GetMethod(nameof(ValueTypeUnboxer.UnboxPointer), BindingFlags.Static | BindingFlags.NonPublic); private static MethodInfo unboxerToNativeMethod = typeof(ValueTypeUnboxer).GetMethod(nameof(ValueTypeUnboxer.UnboxPointerWithConverter), BindingFlags.Static | BindingFlags.NonPublic); internal static IntPtr GetPointer(object value, Type type) { - if (!unboxers.TryGetValue(type, out var deleg)) + if (!unboxers.TryGetValue(type, out var tuple)) { // Non-POD structures use internal layout (eg. SpriteHandleManaged in C++ with SpriteHandleMarshaller.SpriteHandleInternal in C#) so convert C# data into C++ data var attr = type.GetCustomAttribute(); var toNativeMethod = attr?.NativeType.GetMethod("ToNative", BindingFlags.Static | BindingFlags.NonPublic); if (toNativeMethod != null) { - deleg = unboxerToNativeMethod.MakeGenericMethod(toNativeMethod.ReturnType).CreateDelegate(); + tuple.deleg = unboxerToNativeMethod.MakeGenericMethod(type, toNativeMethod.ReturnType).CreateDelegate(); + tuple.toNativeDeleg = toNativeMethod.CreateDelegate(typeof(ToNativeDelegate<,>).MakeGenericType(type, toNativeMethod.ReturnType)); } else { - deleg = unboxerMethod.MakeGenericMethod(type).CreateDelegate(); + tuple.deleg = unboxerMethod.MakeGenericMethod(type).CreateDelegate(); } - deleg = unboxers.GetOrAdd(type, deleg); + tuple = unboxers.GetOrAdd(type, tuple); } - return deleg(value); + return tuple.deleg(value, tuple.toNativeDeleg); } - private static IntPtr UnboxPointer(object value) where T : struct + private static void PinValue(object value) { + // Prevent garbage collector from relocating the boxed value by pinning it temporarily. + // The pointer should remain valid quite long time but will be eventually unpinned. + uint index = Interlocked.Increment(ref pinnedBoxedValuesPointer) % (uint)pinnedBoxedValues.Length; + ref GCHandle handle = ref pinnedBoxedValues[index]; + if (handle.IsAllocated) + handle.Free(); + handle = GCHandle.Alloc(value, GCHandleType.Pinned); + } + + private static IntPtr PinValue(T value) where T : struct + { + // Store the converted value in unmanaged memory so it will not be relocated by the garbage collector. + int size = Unsafe.SizeOf(); + uint index = Interlocked.Increment(ref pinnedAllocationsPointer) % (uint)pinnedAllocations.Length; + ref (IntPtr ptr, int size) alloc = ref pinnedAllocations[index]; + if (alloc.size < size) + { + if (alloc.ptr != IntPtr.Zero) + NativeFree(alloc.ptr.ToPointer()); + alloc.ptr = new IntPtr(NativeAlloc(size)); + alloc.size = size; + } + + Unsafe.Write(alloc.ptr.ToPointer(), value); + return alloc.ptr; + } + + private static IntPtr UnboxPointer(object value, object converter) where T : struct + { + PinValue(value); return new IntPtr(Unsafe.AsPointer(ref Unsafe.Unbox(value))); } - private static IntPtr UnboxPointerWithConverter(object value) where T : struct + private static IntPtr UnboxPointerWithConverter(object value, object converter) where T : struct where TInternal : struct { - var type = value.GetType(); - var attr = type.GetCustomAttribute(); - var toNative = attr.NativeType.GetMethod("ToNative", BindingFlags.Static | BindingFlags.NonPublic); - value = toNative.Invoke(null, new[] { value }); - return new IntPtr(Unsafe.AsPointer(ref Unsafe.Unbox(value))); + ToNativeDelegate toNative = Unsafe.As>(converter); + return PinValue(toNative(Unsafe.Unbox(value))); } }