diff --git a/src/Tests/Aardvark.Base.FSharp.Benchmarks/Aardvark.Base.FSharp.Benchmarks.fsproj b/src/Tests/Aardvark.Base.FSharp.Benchmarks/Aardvark.Base.FSharp.Benchmarks.fsproj
index 4066b6ef..b85a2555 100644
--- a/src/Tests/Aardvark.Base.FSharp.Benchmarks/Aardvark.Base.FSharp.Benchmarks.fsproj
+++ b/src/Tests/Aardvark.Base.FSharp.Benchmarks/Aardvark.Base.FSharp.Benchmarks.fsproj
@@ -27,6 +27,7 @@
+
diff --git a/src/Tests/Aardvark.Base.FSharp.Benchmarks/DynamicDispatchBench.fs b/src/Tests/Aardvark.Base.FSharp.Benchmarks/DynamicDispatchBench.fs
new file mode 100644
index 00000000..9b7fb6f3
--- /dev/null
+++ b/src/Tests/Aardvark.Base.FSharp.Benchmarks/DynamicDispatchBench.fs
@@ -0,0 +1,181 @@
+namespace Aardvark.Base.FSharp.Benchmarks
+
+open System
+open System.Runtime.CompilerServices
+open Aardvark.Base
+open BenchmarkDotNet.Attributes
+
+// Benchmarks for dynamic dispatch pattern (e.g. creating a PixImage from a given System.Type)
+//
+// Takeaways about dynamic invocation and delegates:
+// - MakeGenericMethod() is expensive
+// - Creating a delegate is also pretty expensive
+// - Dynamic invocation (MethodInfo.Invoke()) is expensive
+// - Calling a delegate is less expensive than dynamic invocation
+//
+// Takeaways about thread-safe caching:
+// - ConcurrentDictionary seems to incur less overhead than ThreadLocal or a Dictionary with a simple lock for some reason
+// - ConcurrentDictionary.TryGetValue() and manual insert is faster than ConcurrentDictionary.GetOrAdd()
+// - try-finally pattern with Monitor.Enter() and Monitor.Exit() is faster than lock()
+//
+// BenchmarkDotNet v0.13.12, Windows 10 (10.0.19045.4529/22H2/2022Update)
+// AMD Ryzen 5 5600X, 1 CPU, 12 logical and 6 physical cores
+// .NET SDK 8.0.300
+// [Host] : .NET 8.0.5 (8.0.524.21615), X64 RyuJIT AVX2 DEBUG
+
+// Job=ShortRun Toolchain=InProcessEmitToolchain IterationCount=3
+// LaunchCount=1 WarmupCount=3
+
+// | Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio |
+// |------------------------------------------ |-----------:|-----------:|----------:|-------:|--------:|-------:|----------:|------------:|
+// | Direct | 3.864 ns | 0.2973 ns | 0.0163 ns | 1.00 | 0.00 | 0.0014 | 24 B | 1.00 |
+// | Dispatch (uncached) | 245.382 ns | 4.7115 ns | 0.2583 ns | 63.51 | 0.23 | 0.0153 | 256 B | 10.67 |
+// | Dispatch (cached) | 43.992 ns | 2.6162 ns | 0.1434 ns | 11.39 | 0.08 | 0.0033 | 56 B | 2.33 |
+// | Dispatch (uncached delegate) | 550.627 ns | 7.2393 ns | 0.3968 ns | 142.51 | 0.70 | 0.0172 | 288 B | 12.00 |
+// | Dispatch (cached delegate) | 16.621 ns | 1.2594 ns | 0.0690 ns | 4.30 | 0.02 | 0.0014 | 24 B | 1.00 |
+// | Dispatch (cached thread-local delegate) | 29.383 ns | 1.0146 ns | 0.0556 ns | 7.60 | 0.02 | 0.0052 | 88 B | 3.67 |
+// | Dispatch (cached locked delegate) | 25.263 ns | 3.4076 ns | 0.1868 ns | 6.54 | 0.07 | 0.0052 | 88 B | 3.67 |
+// | Dispatch (predefined delegate) | 11.809 ns | 52.8603 ns | 2.8975 ns | 3.06 | 0.76 | 0.0014 | 24 B | 1.00 |
+
+
+type IArray =
+ abstract member Data: Array
+
+type MyArray<'T>(data: 'T[]) =
+ member x.Data = data
+ interface IArray with
+ member x.Data = data :> Array
+
+module MyArray =
+ open System.Threading
+ open System.Reflection
+ open System.Collections.Concurrent
+ open System.Collections.Generic
+
+ []
+ type Dispatcher() =
+ static member Create<'T>(data: Array) : IArray = MyArray<'T>(unbox<'T[]> data)
+
+ type CreateDelegate = delegate of Array -> IArray
+
+ let private createMethod = typeof.GetMethod(nameof Dispatcher.Create, BindingFlags.Public ||| BindingFlags.Static)
+ let private createMethods = ConcurrentDictionary()
+ let private createDelegates = ConcurrentDictionary()
+ let private createDelegatesThreadLocal = new ThreadLocal>((fun _ -> Dictionary()), false)
+ let private createDelegatesLocked = Dictionary()
+
+ let private makeDelegate t =
+ let mi = createMethod.MakeGenericMethod [| t |]
+ unbox <| Delegate.CreateDelegate(typeof, null, mi)
+
+ let private createFloatDelegate =
+ makeDelegate typeof
+
+ []
+ let create (data: 'T[]) : MyArray<'T> =
+ MyArray<'T>(data)
+
+ let createUntypedUncached (data: Array) : IArray =
+ let mi = createMethod.MakeGenericMethod(data.GetType().GetElementType())
+ mi.Invoke(null, [| data |]) |> unbox
+
+ let createUntypedCached (data: Array) : IArray =
+ let t = data.GetType().GetElementType()
+
+ let mi =
+ match createMethods.TryGetValue t with
+ | (true, mi) -> mi
+ | _ ->
+ Thread.Sleep 2000
+ let mi = createMethod.MakeGenericMethod t
+ createMethods.[t] <- mi
+ mi
+
+ mi.Invoke(null, [| data |]) |> unbox
+
+ let createUntypedUncachedDelegate (data: Array) : IArray =
+ let del = makeDelegate <| data.GetType().GetElementType()
+ del.Invoke(data)
+
+ let createUntypedCachedDelegate (data: Array) : IArray =
+ let t = data.GetType().GetElementType()
+
+ let del =
+ match createDelegates.TryGetValue t with
+ | (true, del) -> del
+ | _ ->
+ let del = makeDelegate t
+ createDelegates.[t] <- del
+ del
+
+ del.Invoke(data)
+
+ let createUntypedCachedThreadLocalDelegate (data: Array) : IArray =
+ let del = createDelegatesThreadLocal.Value.GetCreate(data.GetType().GetElementType(), makeDelegate)
+ del.Invoke(data)
+
+ let createUntypedCachedLockedDelegate (data: Array) : IArray =
+ let t = data.GetType().GetElementType()
+
+ let del =
+ let mutable taken = false
+ try
+ Monitor.Enter(createDelegatesLocked, &taken)
+
+ match createDelegatesLocked.TryGetValue t with
+ | (true, del) -> del
+ | _ ->
+ let del = makeDelegate t
+ createDelegatesLocked.[t] <- del
+ del
+ finally
+ if taken then Monitor.Exit createDelegatesLocked
+
+ del.Invoke(data)
+
+ let createUntypedPredefinedDelegate (data: Array) : IArray =
+ createFloatDelegate.Invoke(data)
+
+[]
+type Dispatch() =
+
+ []
+ val mutable Data : float[]
+
+ []
+ member x.Setup() =
+ let rnd = RandomSystem 0
+ let arr = MyArray.create (Array.init 512 (ignore >> rnd.UniformDouble))
+ x.Data <- arr.Data
+
+ []
+ member x.Direct() : IArray =
+ MyArray.create x.Data
+
+ []
+ member x.DispatchUncached() : IArray =
+ MyArray.createUntypedUncached x.Data
+
+ []
+ member x.DispatchCached() : IArray =
+ MyArray.createUntypedCached x.Data
+
+ []
+ member x.DispatchUncachedDelegate() : IArray =
+ MyArray.createUntypedUncachedDelegate x.Data
+
+ []
+ member x.DispatchCachedDelegate() : IArray =
+ MyArray.createUntypedCachedDelegate x.Data
+
+ []
+ member x.DispatchCachedThreadLocalDelegate() : IArray =
+ MyArray.createUntypedCachedThreadLocalDelegate x.Data
+
+ []
+ member x.DispatchCachedLockedDelegate() : IArray =
+ MyArray.createUntypedCachedLockedDelegate x.Data
+
+ []
+ member x.DispatchPredefinedDelegate() : IArray =
+ MyArray.createUntypedPredefinedDelegate x.Data
\ No newline at end of file