Skip to content

Convert COMCustomAttribute from MDCS to UCO#126542

Draft
am11 wants to merge 7 commits intodotnet:mainfrom
am11:feature/MDCS-to-UCOA-pattern
Draft

Convert COMCustomAttribute from MDCS to UCO#126542
am11 wants to merge 7 commits intodotnet:mainfrom
am11:feature/MDCS-to-UCOA-pattern

Conversation

@am11
Copy link
Copy Markdown
Member

@am11 am11 commented Apr 4, 2026

Contributes to prio3 #123864

@dotnet-policy-service dotnet-policy-service bot added the community-contribution Indicates that the PR has been added by a community member label Apr 4, 2026
@am11 am11 force-pushed the feature/MDCS-to-UCOA-pattern branch 3 times, most recently from 58cc7e0 to a59dd87 Compare April 4, 2026 20:34
@am11 am11 force-pushed the feature/MDCS-to-UCOA-pattern branch from a59dd87 to 7a3eaba Compare April 4, 2026 22:17
@am11
Copy link
Copy Markdown
Member Author

am11 commented Apr 4, 2026

@jkotas, @AaronRobinsonMSFT, I tried a few approaches to avoid reflection stack, and ended up with this cached stub-gen. Is this the right path forward? The AI agent I was working analyzed and found that we can potentially piggyback on this approach for funceval (the last MDCS usage).

Copy link
Copy Markdown
Member

@AaronRobinsonMSFT AaronRobinsonMSFT left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A bunch of style nits.

U1ARRAYREF -> BOOLARRAYREF
IntPtr -> void*

I'm still trying to understand this approach. I know it is following the newer Reflection approach and it looks very similar to what Jan has attempted to explain to me a few times. I just don't have the intuition yet. I'd wager it is closer to correct than not though.

@jkotas
Copy link
Copy Markdown
Member

jkotas commented Apr 5, 2026

it is following the newer Reflection approach

It is duplicating the newer reflection approach. I do not think we want to have a different strategy for dynamic invoke thunks for each reflection invoke-like path, each with different bugs and perf characteristics. I think we want to have one way to do this and have all reflection invoke-like paths to call into it. Avoiding reflection stack completely is a non-goal. We want to avoid code duplication. It is fine to refactor the reflection stack to make it possible. Also, it is fine to reduce sharing of the reflection stack with Mono if it makes the refactoring easier.

@jkotas
Copy link
Copy Markdown
Member

jkotas commented Apr 5, 2026

The custom attribute instantiation reuses the reflection stack already.

setMethod.InvokePropertySetter(attribute, BindingFlags.Default, null, value, null);
is a good example. This line could have called the public MethodInfo.Invoke method, but it would come with an extra overhead (passing the arguments as object[], etc). We have internal InvokePropertySetter method to avoid MethodInfo.Invoke overhead, but the heavy lifting that deals with actual argument passing is reused from MethodInfo.Invoke. This is the kind of approach I have in mind for this.

@jkotas
Copy link
Copy Markdown
Member

jkotas commented Apr 5, 2026

It may work better to get rid of CallDescrWorker use in RuntimeMethodHandle.InvokeMethod first. I do not have a strong opinion about the order.

Also, both RuntimeMethodHandle.InvokeMethod and this one will need extra scrutiny to avoid perf regressions (both startup and throughput).

@am11
Copy link
Copy Markdown
Member Author

am11 commented Apr 5, 2026

It may work better to get rid of CallDescrWorker use in RuntimeMethodHandle.InvokeMethod first. I do not have a strong opinion about the order.

In previous conversation, you mentioned reflection stack is heavy and InvokeMethod uses CDW (because it affects app startup perf and eats up JIT budget). That's why I opted for the IL stub caching approach. In last commit, I have switch to MethodBaseInvoker and removed stub caching.

@am11
Copy link
Copy Markdown
Member Author

am11 commented Apr 5, 2026

@EgorBot -osx_arm64 -linux_x64 -windows_x64

using System;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

BenchmarkSwitcher.FromAssembly(typeof(CustomAttributeCtorBench).Assembly).Run(args);

[MemoryDiagnoser]
public class CustomAttributeCtorBench
{
    [Benchmark]
    public int SingleArgCtor()
    {
        var a = (SingleArgAttribute)Attribute.GetCustomAttribute(
            typeof(SingleArgTarget),
            typeof(SingleArgAttribute),
            inherit: false)!;

        return a.A;
    }

    [Benchmark]
    public int MultiArgCtor()
    {
        var a = (MultiArgAttribute)Attribute.GetCustomAttribute(
            typeof(MultiArgTarget),
            typeof(MultiArgAttribute),
            inherit: false)!;

        return a.Sum;
    }

    [SingleArg(42)]
    private sealed class SingleArgTarget { }

    [MultiArg(1, 2, 3, 4, 5, 6, 7, 8)]
    private sealed class MultiArgTarget { }

    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
    private sealed class SingleArgAttribute : Attribute
    {
        public readonly int A;
        public SingleArgAttribute(int a) => A = a;
    }

    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
    private sealed class MultiArgAttribute : Attribute
    {
        public readonly int Sum;
        public MultiArgAttribute(int a, int b, int c, int d, int e, int f, int g, int h)
            => Sum = a + b + c + d + e + f + g + h;
    }
}

@am11
Copy link
Copy Markdown
Member Author

am11 commented Apr 5, 2026

@EgorBot -osx_arm64 -linux_x64 -windows_x64

using System;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

BenchmarkSwitcher.FromAssembly(typeof(CustomAttributeCtorBench).Assembly).Run(args);

[MemoryDiagnoser]
public class CustomAttributeCtorBench
{
    [Benchmark]
    public object ZeroArgCtor()
    {
        return Attribute.GetCustomAttribute(
            typeof(ZeroArgTarget),
            typeof(ZeroArgAttribute),
            inherit: false)!;
    }

    [Benchmark]
    public int SingleArgCtor()
    {
        var a = (SingleArgAttribute)Attribute.GetCustomAttribute(
            typeof(SingleArgTarget),
            typeof(SingleArgAttribute),
            inherit: false)!;

        return a.A;
    }

    [Benchmark]
    public object FiveArgCtor()
    {
        return Attribute.GetCustomAttribute(
            typeof(FiveArgTarget),
            typeof(FiveArgAttribute),
            inherit: false)!;
    }

    [ZeroArg]
    private sealed class ZeroArgTarget { }

    [SingleArg(42)]
    private sealed class SingleArgTarget { }

    [FiveArg(123, 3.14, "hello", FiveArgAttribute.Kind.Alpha, typeof(string))]
    private sealed class FiveArgTarget { }

    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
    private sealed class ZeroArgAttribute : Attribute
    {
        public ZeroArgAttribute() { }
    }

    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
    private sealed class SingleArgAttribute : Attribute
    {
        public readonly int A;
        public SingleArgAttribute(int a) => A = a;
    }

    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
    private sealed class FiveArgAttribute : Attribute
    {
        public enum Kind { Alpha, Beta, Gamma }

        public readonly int I;
        public readonly double D;
        public readonly string S;
        public readonly Kind K;
        public readonly Type T;

        public FiveArgAttribute(int i, double d, string s, Kind k, Type t)
        {
            I = i;
            D = d;
            S = s;
            K = k;
            T = t;
        }
    }
}

@am11
Copy link
Copy Markdown
Member Author

am11 commented Apr 5, 2026

@EgorBot -osx_arm64 -linux_x64 -windows_x64

using System;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

BenchmarkSwitcher.FromAssembly(typeof(CustomAttributeCtorBench).Assembly).Run(args);

[MemoryDiagnoser]
public class CustomAttributeCtorBench
{
    [Benchmark]
    public object ZeroArgCtor()
    {
        return Attribute.GetCustomAttribute(
            typeof(ZeroArgTarget),
            typeof(ZeroArgAttribute),
            inherit: false)!;
    }

    [Benchmark]
    public int SingleArgCtor()
    {
        var a = (SingleArgAttribute)Attribute.GetCustomAttribute(
            typeof(SingleArgTarget),
            typeof(SingleArgAttribute),
            inherit: false)!;

        return a.A;
    }

    [Benchmark]
    public object FiveArgCtor()
    {
        return Attribute.GetCustomAttribute(
            typeof(FiveArgTarget),
            typeof(FiveArgAttribute),
            inherit: false)!;
    }

    [ZeroArg]
    private sealed class ZeroArgTarget { }

    [SingleArg(42)]
    private sealed class SingleArgTarget { }

    [FiveArg(123, 3.14, "hello", FiveArgAttribute.Kind.Alpha, typeof(string))]
    private sealed class FiveArgTarget { }

    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
    private sealed class ZeroArgAttribute : Attribute
    {
        public ZeroArgAttribute() { }
    }

    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
    private sealed class SingleArgAttribute : Attribute
    {
        public readonly int A;
        public SingleArgAttribute(int a) => A = a;
    }

    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
    private sealed class FiveArgAttribute : Attribute
    {
        public enum Kind { Alpha, Beta, Gamma }

        public readonly int I;
        public readonly double D;
        public readonly string S;
        public readonly Kind K;
        public readonly Type T;

        public FiveArgAttribute(int i, double d, string s, Kind k, Type t)
        {
            I = i;
            D = d;
            S = s;
            K = k;
            T = t;
        }
    }
}

@am11
Copy link
Copy Markdown
Member Author

am11 commented Apr 5, 2026

With the original (native stub-gen +caching), f3e9b82, there is no regression EgorBot/Benchmarks#92 (although I haven't measured timing of startup/first run). With regular reflection invoke, that involves CallDescrWorker, 3e6efe5, it is significantly slower: EgorBot/Benchmarks#89. The new reflection stack, which bypasses CallDescrWorker d7f8b33, is moderately slower: EgorBot/Benchmarks#90.

@jkoritzinsky
Copy link
Copy Markdown
Member

I had a thought of another approach:

Instead of dynamically figuring out the shape of the constructor and constructing the correct arguments for the constructor dynamically, what if we were to emit a UCO IL stub to construct the attribute and generate the IL stub based on the specific CustomAttributeBlob? Then we could have a known signature, avoid going through the reflection stack entirely, and ensure that we don't introduce another usage of CallDescrWorker infra that we'd need to remove.

@jkotas
Copy link
Copy Markdown
Member

jkotas commented Apr 6, 2026

what if we were to emit a UCO IL stub to construct the attribute and generate the IL stub

We run into a startup perf problem when we tried to implement MethodInfo.Invoke using this strategy. MethodInfo.Invoke has a lot of cases where given method is invoked once or a very few times. Emitting method-specific IL stubs by default is too expensive during startup. The plan for MethodInfo.Invoke has been to R2R pre-compile the stubs for like 100 most common signatures, and JIT for less common signatures or when the given method is executed a lot. #115345 has WIP implementation that we were not able to land in time.

I think this should use the same tiered strategy as MethodInfo.Invoke, ideally by sharing the code so that we do not have to maintain and tune implementation of multiple of these system.

@jkotas
Copy link
Copy Markdown
Member

jkotas commented Apr 7, 2026

3e6efe5, it is significantly slower: EgorBot/Benchmarks#89.

I think this delta is closest to what it should look like. Have you tried to profile it to see where the time is spent and opportunities to optimize it? For example, the casts like ctorObject is not System.IRuntimeMethodInfo methodInfo should be avoidable.

@am11
Copy link
Copy Markdown
Member Author

am11 commented Apr 7, 2026

I've tried converting InvokeMethod to UCO path using dynamic/cached IL stub approach: main...am11:runtime:feature/MDCS-to-UCOA-pattern3. It is passing reflection and runtime tests on osx-arm64.

@jkotas
Copy link
Copy Markdown
Member

jkotas commented Apr 7, 2026

I've tried converting InvokeMethod to UCO path using dynamic/cached IL stub approach: main...am11:runtime:feature/MDCS-to-UCOA-pattern3

This is duplicating what managed reflection side does using DynamicMethods. It would introduce a lot of JITing in common scenarios during startup. It won't pass the perf gates - #126542 (comment) .

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-System.Reflection community-contribution Indicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants