Skip to content

Commit

Permalink
[perf] Cache Runtime.IsUserType results
Browse files Browse the repository at this point in the history
This is computed **twice** _per instance_, when the instance is
* created: `NSObject.CreateManagedRef`
* released: `NSObject.ReleaseManagedRef`

However its (boolean) value remains identical for all instances of the same type.

This shows up as consuming a lot of time in the logs attached to dotnet#15145

Basically two things are cached
* the selector for `xamarinSetGCHandle:flags:`, which makes it 15% faster;
	 * some platforms, but not macOS, have an optimization for the `Selector.GetHandle` API
* the result is cached into a `Dictionrary<IntPtr,bool>`

Note that the optimizations are not specific to CoreCLR nor macOS (so they are not fixes for the CoreCLR performance regression of the above mentioned issue).
OTOH it will also help performance for legacy and other net6 (mono) platforms.

```
BenchmarkDotNet=v0.12.1, OS=macOS 12.3.1 (21E258) [Darwin 21.4.0]
Apple M1, 1 CPU, 8 logical and 8 physical cores
.NET Core SDK=        6.0.100 [/usr/local/share/dotnet/sdk]
  [Host] : .NET Core 6.0 (CoreCLR 6.0.522.21309, CoreFX 6.0.522.21309), Arm64 RyuJIT

Job=InProcess  Toolchain=InProcessEmitToolchain  IterationCount=3
LaunchCount=1  WarmupCount=3
```

|           Method | Length |           Mean |        Error |      StdDev | Ratio |
|----------------- |------- |---------------:|-------------:|------------:|------:|
|         Original |     16 |     7,729.8 ns |    212.61 ns |    11.65 ns |  1.00 |
|   CachedSelector |     16 |     6,552.6 ns |    202.70 ns |    11.11 ns |  0.85 |
| CachedIsUserType |     16 |       162.0 ns |     14.86 ns |     0.81 ns |  0.02 |
|                  |        |                |              |             |       |
|         Original |    256 |   123,183.0 ns |  4,724.95 ns |   258.99 ns |  1.00 |
|   CachedSelector |    256 |   104,570.3 ns |  2,029.20 ns |   111.23 ns |  0.85 |
| CachedIsUserType |    256 |     2,489.5 ns |    390.86 ns |    21.42 ns |  0.02 |
|                  |        |                |              |             |       |
|         Original |   4096 | 1,970,381.7 ns | 66,393.09 ns | 3,639.23 ns |  1.00 |
|   CachedSelector |   4096 | 1,676,773.0 ns | 12,149.92 ns |   665.98 ns |  0.85 |
| CachedIsUserType |   4096 |    39,933.3 ns |  7,426.74 ns |   407.08 ns |  0.02 |

[Benchmark source code](https://gist.github.com/spouliot/42fd43e94c5a9ce90164f0f6f9b35018)
  • Loading branch information
spouliot committed May 29, 2022
1 parent 6676f81 commit b369d61
Showing 1 changed file with 19 additions and 2 deletions.
21 changes: 19 additions & 2 deletions src/ObjCRuntime/Runtime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public partial class Runtime {
static List <object> delegates;
static List <Assembly> assemblies;
static Dictionary <IntPtr, GCHandle> object_map;
static Dictionary <IntPtr, bool> usertype_cache;
static object lock_obj;
static IntPtr NSObjectClass;
static bool initialized;
Expand Down Expand Up @@ -272,6 +273,7 @@ unsafe static void Initialize (InitializationOptions* options)
Runtime.options = options;
delegates = new List<object> ();
object_map = new Dictionary <IntPtr, GCHandle> (IntPtrEqualityComparer);
usertype_cache = new Dictionary <IntPtr, bool> (IntPtrEqualityComparer);
intptr_ctor_cache = new Dictionary<Type, ConstructorInfo> (TypeEqualityComparer);
intptr_bool_ctor_cache = new Dictionary<Type, ConstructorInfo> (TypeEqualityComparer);
lock_obj = new object ();
Expand Down Expand Up @@ -1735,11 +1737,23 @@ internal static IntPtr GetProtocolForType (Type type)
throw new ArgumentException ($"'{type.FullName}' is an unknown protocol");
}

[BindingImpl (BindingImplOptions.Optimizable)]
internal static bool IsUserType (IntPtr self)
{
var cls = Class.object_getClass (self);
if (!usertype_cache.TryGetValue (cls, out var result)) {
result = SlowIsUserType (cls);
usertype_cache.Add (cls, result);
}
return result;
}

#if __MACOS__
static IntPtr selSetGCHandle = Selector.GetHandle ("xamarinSetGCHandle:flags:");
#endif

[BindingImpl (BindingImplOptions.Optimizable)]
static bool SlowIsUserType (IntPtr cls)
{
unsafe {
if (options->RegistrationMap is not null && options->RegistrationMap->map_count > 0) {
var map = options->RegistrationMap->map;
Expand All @@ -1752,8 +1766,11 @@ internal static bool IsUserType (IntPtr self)
return false;
}
}

#if __MACOS__
return Class.class_getInstanceMethod (cls, selSetGCHandle) != IntPtr.Zero;
#else
return Class.class_getInstanceMethod (cls, Selector.GetHandle ("xamarinSetGCHandle:flags:")) != IntPtr.Zero;
#endif
}

static unsafe int FindUserTypeIndex (MTClassMap* map, int lo, int hi, IntPtr cls)
Expand Down

0 comments on commit b369d61

Please # to comment.