You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Hello stranger! I ran across your reddit post for this project, to be completely honest I don't have a lot of use for desktop applications these days, but I've identified a potential solution to your reflection problem and thought I'd share (if you want). This might be considered a micro-optimization, so feel free to tell me to go to hell.
Summary
Reflection is expensive. In an application such as this, efficient interaction between the framework and the users code is critical for widespread adoption. I will spend the following wall of text explaining how we can accomplish this.
Solution
Source Generators! tl;dr source generators are a roslyn analyzer feature that when implemented, generates code based on code the user enters. We can then use the generated code to interface with user code instead of relying on reflection.
The following rough examples are in the context of registering a component and invoking the methods of a component.
And imagine that your IpcContainer looks more like this:
publicclassIpcContainer{privatereadonlyDictionary<string,IIpcComponent>_instanceCache=[];publicIpcContainerRegister<T>(stringinstanceName,Ttarget)whereT:notnull,IIpcComponent{//we could even get rid of the instanceName param and use typeof(T).GetName() instead//for an even better user experience!if(!_instanceCache.TryAdd(instanceName,target)){thrownewInvalidOperationException("This instance name already exists in the container");}returnthis;}publicIIpcComponentGetInstance(stringinstanceName){if(!_instanceCache.TryGetValue(instanceName,outvarinstance)){thrownewArgumentException($"Instance '{instanceName}' not found.");}returninstance;}}
Now the changes above are already a huge performance boost in regards to registering a new component - since all were doing is managing a dictionary. So how do we make this work with the rest of the framework, you might ask? This is where source generation comes in. Imagine a simple component:
Using a simple source generator we can very easily generate the following, triggered by the attribute:
// <auto-generated/>usingSystem;
#nullable enable
publicpartialclassTestComponent:IIpcComponent{publicobject?Invoke(stringtarget,object?[]?args=null){switch(target){casenameof(Add):{//ArgsBuilder is a simple helper class to cast the positional argsArgsBuildera=new(args);returnAdd(a.MoveNext<Int32>(),a.MoveNext<Int32>());}default:thrownewException($"Target {target} not found in available methods");}}}
With the generated partial class, the TestComponent class is now of type IIpcComponent and can be registered with the IpcContainer, leading us to the ability to invoke methods dynamically without the cost of reflection.
Note
I'm not including the actual source generation code here because it would bloat this proposal even more. Suffice to say the above code is 100% generated lol
Benchmarks
These benchmarks probably aren't super accurate since I threw them together on short notice, but the numbers are interesting to consider:
These metrics are not surprising at all, since the V2 version of the IpcContainer is basically just a wrapper over a dictionary - but it shows what kind of penalty we're paying with the original implementation.
Component Invocation
Method
Count
Mean
Error
StdDev
Ratio
Gen0
Allocated
Alloc Ratio
Reflection
10
794.8 ns
6.47 ns
6.05 ns
1.00
0.1984
2.03 KB
1.00
SourceGen
10
288.9 ns
1.99 ns
1.77 ns
0.36
0.1683
1.72 KB
0.85
Reflection
100
7,382.9 ns
68.46 ns
60.69 ns
1.00
1.9836
20.31 KB
1.00
SourceGen
100
2,893.0 ns
13.37 ns
11.85 ns
0.39
1.6823
17.19 KB
0.85
Reflection
1000
78,986.5 ns
592.94 ns
554.63 ns
1.00
21.9727
225 KB
1.00
SourceGen
1000
35,216.4 ns
502.42 ns
469.96 ns
0.45
18.9209
193.75 KB
0.86
Reflection
10000
1,156,346.8 ns
11,557.47 ns
10,810.86 ns
1.00
228.5156
2334.38 KB
1.00
SourceGen
10000
457,363.7 ns
8,492.86 ns
7,944.23 ns
0.40
197.7539
2021.88 KB
0.87
These numbers are a little more interesting, this is calculated by getting a component from the container and invoking it with _container.Invoke("Add", [10, 5]); and compared to a slimmed down version of the original. As the metrics show, we can save a significant amount of time on the invocation by using source generators instead of reflection.
Cons
So we've talked about all the pros for source generators, what are some of the cons?
Increased code complexity: Source generators make the underlying code much more complicated to maintain and understand - sometimes it feels like magic (not the good kind!). Any experienced dev can look at GetType().GetMethods() and know that reflection is being used to get a types methods; but magic attributes that make a type suddenly have an interface and a method that gets other methods (???)
This would be a breaking change: Since the added requirement that classes that have the IpcExposeAttribute or have methods that use IpcExposeAttribute must be partial, this would be a breaking change and require a major version bump :(
Not all reflection can be (or should be) replaced with source generators. Obvious hot paths such as registration/invocation, but I haven't looked hard enough to determine if anywhere else should be.
Its a lot of work to perform this kind of migration, so you must ask yourself "is it worth it?"
Conclusion
Thinking back I'd probably use an abstract class instead of an interface for IIpcComponent - it kind of feels more natural, and frees us up to pass in cool things during the component lifecycle if we wanted in the future.
I don't really have much else to add tbh, I couldn't sleep last night so this is what I did instead lol. Anyways, this was a fun thought experiment since I've never actually created a source generator before today.
Hello stranger! I ran across your reddit post for this project, to be completely honest I don't have a lot of use for desktop applications these days, but I've identified a potential solution to your reflection problem and thought I'd share (if you want). This might be considered a micro-optimization, so feel free to tell me to go to hell.
Summary
Reflection is expensive. In an application such as this, efficient interaction between the framework and the users code is critical for widespread adoption. I will spend the following wall of text explaining how we can accomplish this.
Solution
Source Generators! tl;dr source generators are a roslyn analyzer feature that when implemented, generates code based on code the user enters. We can then use the generated code to interface with user code instead of relying on reflection.
The following rough examples are in the context of registering a component and invoking the methods of a component.
Imagine a new interface:
And imagine that your
IpcContainerlooks more like this:Now the changes above are already a huge performance boost in regards to registering a new component - since all were doing is managing a dictionary. So how do we make this work with the rest of the framework, you might ask? This is where source generation comes in. Imagine a simple component:
Using a simple source generator we can very easily generate the following, triggered by the attribute:
With the generated partial class, the
TestComponentclass is now of typeIIpcComponentand can be registered with theIpcContainer, leading us to the ability to invoke methods dynamically without the cost of reflection.Note
I'm not including the actual source generation code here because it would bloat this proposal even more. Suffice to say the above code is 100% generated lol
Benchmarks
These benchmarks probably aren't super accurate since I threw them together on short notice, but the numbers are interesting to consider:
Component Registration
BenchmarkDotNet v0.14.0, Windows 11 (10.0.22621.1702/22H2/2022Update/SunValley2)
12th Gen Intel Core i5-12600K, 1 CPU, 16 logical and 10 physical cores
.NET SDK 8.0.400
[Host] : .NET 8.0.8 (8.0.824.36612), X64 RyuJIT AVX2
DefaultJob : .NET 8.0.8 (8.0.824.36612), X64 RyuJIT AVX2
These metrics are not surprising at all, since the V2 version of the
IpcContaineris basically just a wrapper over a dictionary - but it shows what kind of penalty we're paying with the original implementation.Component Invocation
These numbers are a little more interesting, this is calculated by getting a component from the container and invoking it with
_container.Invoke("Add", [10, 5]);and compared to a slimmed down version of the original. As the metrics show, we can save a significant amount of time on the invocation by using source generators instead of reflection.Cons
So we've talked about all the pros for source generators, what are some of the cons?
GetType().GetMethods()and know that reflection is being used to get a types methods; but magic attributes that make a type suddenly have an interface and a method that gets other methods (???)IpcExposeAttributeor have methods that useIpcExposeAttributemust bepartial, this would be a breaking change and require a major version bump :(Conclusion
Thinking back I'd probably use an abstract class instead of an interface for
IIpcComponent- it kind of feels more natural, and frees us up to pass in cool things during the component lifecycle if we wanted in the future.I don't really have much else to add tbh, I couldn't sleep last night so this is what I did instead lol. Anyways, this was a fun thought experiment since I've never actually created a source generator before today.
If I don't hear from you again - good luck!