Runtime: Performance around GetTypes vs GetExportedTypes for Assemblies

Created on 19 Oct 2019  路  5Comments  路  Source: dotnet/runtime

For a project I'm working on, I have a need to perform an all-assembly search for a type by its name (with no namespace nor knowing what assembly specifically). This code, while does cache results, can be hit relatively hard under certain conditions which made me look into the performance of searching the types (both around allocations and raw speed).

Ultimately, the code uses AppDomain.CurrentDomain.GetAssemblies() and then checks every type in each assembly. When getting the types from the assembly, there seems to be a strange performance vs memory trade off where GetExportedTypes is about 5x slower than GetTypes but has ~60% less allocations. Of the two methods, I would have figured it might have been the other way around with GetExportedTypes being faster.

While I did try to look into the source code, I found both methods ultimately leading to external calls to C++ so I couldn't determine why the performance characteristics are like this.

I guess though I'm asking for a bit of understanding behind the implementations of these two methods as to why they perform like this, potentially whether there is a way to bring the performance closer together. Most ideally, it would be great to get the performance of GetTypes with the allocations of GetExportedTypes.

BenchmarkDotNet=v0.11.5, OS=Windows 10.0.18362
Intel Core i7-6700HQ CPU 2.60GHz (Skylake), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=3.0.100
[Host] : .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), 64bit RyuJIT
Core : .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), 64bit RyuJIT

Job=Core Runtime=Core

| Method | findType | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
|------------------- |--------- |---------:|----------:|----------:|--------:|------:|------:|----------:|
| GetTypes | Program | 114.3 us | 0.9008 us | 0.8426 us | 11.3525 | - | - | 35.08 KB |
| GetExportedTypes | Program | 545.8 us | 3.1176 us | 2.9162 us | 3.9063 | - | - | 14.2 KB |
| GetTypes | String | 113.4 us | 0.9702 us | 0.9075 us | 11.3525 | - | - | 35.08 KB |
| GetExportedTypes | String | 542.8 us | 5.0385 us | 3.9337 us | 3.9063 | - | - | 14.2 KB |
| GetTypes | Uri | 118.0 us | 0.6657 us | 0.6227 us | 11.4746 | - | - | 35.51 KB |
| GetExportedTypes | Uri | 551.2 us | 3.8826 us | 3.0313 us | 3.9063 | - | - | 14.38 KB |

Benchmark Code

class Program
{
    static void Main(string[] args)
    {
        BenchmarkRunner.Run<AssemblyGetTypesBenchmark>();
    }
}

[CoreJob, MemoryDiagnoser]
public class AssemblyGetTypesBenchmark
{
    [Benchmark]
    [Arguments(typeof(string))]
    [Arguments(typeof(Uri))]
    [Arguments(typeof(Program))]
    public void GetTypes(Type findType)
    {
        foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
        {
            foreach (var type in assembly.GetTypes())
            {
                if (type == findType)
                {
                    break;
                }
            }
        }
    }

    [Benchmark]
    [Arguments(typeof(string))]
    [Arguments(typeof(Uri))]
    [Arguments(typeof(Program))]
    public void GetExportedTypes(Type findType)
    {
        foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
        {
            foreach (var type in assembly.GetExportedTypes())
            {
                if (type == findType)
                {
                    break;
                }
            }
        }
    }
}

Most helpful comment

It would probably be better time spent trying to figure out how to avoid scanning all assemblies. The usual techniques for optimizing code like this are:

  • Scan less assemblies

    • Skip System.* and Microsoft.* assemblies

  • Do the work at build time instead

    • Work to figure out what assemblies you need to scan at runtime OR

    • Just do the work and cache it in the assembly

  • Start from a different and smaller set of assemblies and walk them transitively

Is it a plugin system? Are you loading new assemblies at runtime?

All 5 comments

GetTypes returns an array of all types in the assembly vs just those that are public. It allocates a larger array because it has many more types to return in the array.

~I'm wondering though that GetExportedTypes doesn't seem to be calling GetTypes internally? If it did, the allocation of GetExportedTypes should be higher than GetTypes right?~

~Is it that there is some other internal GetTypes method they call to process the raw data before creating their result arrays?~

On second thought with this, it totally makes sense now with the performance-vs-memory. Both methods return a full array (like you mentioned) but GetExportedTypes actually has to check every type for being public before returning where I exit early on finding the right type... I shouldn't post issues when I'm tired.

I guess though that still leads me to another question, is there a way to access the types without a full array of all of them being allocated? I figure it might be a long shot with this being fairly low level, but something that is more akin to it being like an enumerator?

It would probably be better time spent trying to figure out how to avoid scanning all assemblies. The usual techniques for optimizing code like this are:

  • Scan less assemblies

    • Skip System.* and Microsoft.* assemblies

  • Do the work at build time instead

    • Work to figure out what assemblies you need to scan at runtime OR

    • Just do the work and cache it in the assembly

  • Start from a different and smaller set of assemblies and walk them transitively

Is it a plugin system? Are you loading new assemblies at runtime?

@Turnerj There is a lower level Metadata API that permits reading the raw Metadata tables. Other than using that, which I don't recommend unless this needs to be done at runtime, @davidfowl's suggestions are what I would tend to recommend.

Thanks for everyone's input! I realise now my question was kinda dumb (as it actually makes sense why one is faster and why one consumes less memory) and yeah, filtering the assemblies is definitely a smarter option.

My use case is also going to sound kinda dumb - I'm working on a serializer for MongoDB which specifically is geared towards not knowing what the type is until the last minute at runtime. Usually you would need to decorate your class with attributes saying all the known types - I don't like that approach when you have dozens and dozens of types which it could be.

I would say 95% of the time, the types it is looking for aren't in System.* (and extremely unlikely to be in Microsoft.*) though occasionally it still might be types like Uri or something that I need to find. I think there are one or two optimisations I think I can do in those cases like search the assembly types like those are located in and then only search ones that aren't System.* or Microsoft.*.

That said, that lower level metadata API does sound interesting enough to check out at least.

Thanks again!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

omajid picture omajid  路  3Comments

jchannon picture jchannon  路  3Comments

matty-hall picture matty-hall  路  3Comments

jzabroski picture jzabroski  路  3Comments

omariom picture omariom  路  3Comments