Achieving compile-time performance with Reflection in C#

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
hello everybody i'm nick and this is going to take a look at reflections performance in csharpen.net and show you how you can actually improve it significantly to the point where it's comparable to something that would be compile time now this wouldn't always be the case and it heavily depends on what you're doing and most of the time you're going to use reflection cold paths where you would just do some initial wiring up when your application starts and then you're going to forget about it but if you need to use it in a hot puff and sometimes you have to i know i have when i need to access something that a library wouldn't expose to me but i really need it for something like logging or metrics collection then i would need to optimize what reflection call that being said you also need to ensure that this thing doesn't break because when you use reflection to access things that you're not supposed to access it's up to the discretion of the library owner to change those things and then not consider breaking changes so you have to be very very careful when you update the library that you're using reflection on to make sure you didn't break something but besides that and besides all the safety nets you can put around reflection i think it's a very powerful tool that you have to use with great responsibility and it's something i personally use a lot and i'm going to show you how you can optimize it in three ways in this video if you like a lot of content and you want to see more make sure you subscribe in the certification bell to get that later when i upload a new video so what do i have here well i have a main console application which is basically our benchmark runner because i'm gonna show you a few benchmarks here as i'm showing you the techniques to optimize reflections usage and then i have an external library here which will act as the thing that we're trying to access more specifically we're going to try to get the value of this very private property and i also have a public property here and the reason why i do that is to provide a perspective on what a simple get compile time would perform like so i have this as like my base for all the other benchmarks and then i'm going to build around it to compare it with it so the first thing you would do when you use reflection is the traditional reflection usage especially in properties where you just do something like traditional reflection and then you would get the property info or actually let's just initialize the class in the same way where we initialize it here um initialize the class and what we're going to get just to understand what we're doing here is the very uh private property as you can see we cannot actually use it it won't compile it won't let me it's um private so in order to get access to that i'm going to get the property info from that type so i'm going to do um some class dot get type dot get property to the property info then i'm gonna provide the name which in this case is very private property i cannot use name off because it is private and i also need to give some flags here for the binding it is an instance and it is non-public so that should return the property info back to me and then i can get the value of that so i can say uh propertyinfo.getvalue and provide the instance of the class i want to get the value from and i know that this is not actually null and then return the value to string because i think it's object we're going to cast it and that's it now just to show you how this um works i did not mean to run that so let me just quickly stop this i'm going to comment this out stick a break point here we're going to say reflection usage traditional reflection of our value equals that and if i change the debug and i debug this and this is gonna be the same for every other usage i go about we create an instance so you can see the value is default by default and then you get the property info as you can see we have it here and then we get the value as you can see it's default and then we return it and the value is indeed default so that's how every other way i'm going to show you in this video will actually play out and now what i want to do is i'm going to run a benchmark against this which is the traditional reflection usage to get to that value i'm going to go back to my benchmarks i'm going to create a new entry here and i'm going to say traditional reflection here we go i'm going to change that to release and i'm going to execute this and this will run all the benchmarks hundreds of thousands of times until we get consistent results if you don't know what benchmark.net is i actually do have a video on it you're gonna click on the top right corner of the screen right now to watch that end results are back and as you can see the simple get which we just have compile time usage of the property is returning on six nanoseconds on average while the traditional reflection is quite a few times slower at 75 nanoseconds so significantly slower however if we take a look at how we implemented the traditional way you see that we get the property info every single time now the property info is quite the heavy operation to get and we don't have to get it every time you can actually cash it into maybe a field so what you can do to optimize the traditional way if i duplicate this is i'm going to say optimized here is create a private static read-only property info and then put that there and only get it on startup once and i'm going to say cast property equal as that and to get the type i'm going to say type of this and i don't get type anymore and here we go we can use that and now what i'm going to do is i'm going to add this in the benchmarks the optimize traditional version where we basically cached the property in for in this specific case and this is applicable to any other thing method infos anything you're using can actually benefit of this idea that you don't actually need to evaluate something every single time it's universally used and i'm going to execute the benchmark again and see what we get now that we cached that heavy property info and results are back and as you can see we trimmed off quite a bit from that original traditional reflection we trimmed off 25 27 nanoseconds not half enough but almost half so we did quite a lot of work here and that's an easy way to optimize the traditional reflection a bit but there's actually two more ways we're going to go way way further with this you see you can actually create in c sharp something called delegates and it's very much the same as something like this for example you have a function that accepts um a very public class and returns a string and let's say del here and then that has an implementation this thing can actually be defined here for example um can actually be generated in code and that can be used to speed up this very operation i'm going to show you how you can do that it will make more sense as i'm talking you through that so in a very similar way we are going to pre-cache something but in this scenario it is the delegate that will accept our instance of the very public class and return a string which is going to be the property value and to do that i'm going to say private read-only stat is it static we don't know i think started anyway function that accepts a very public class and returns yeah i think it's the other way around here we go and returns a string due to the nature of this i'm also going to have to cache it i'm going to say get property delegate here cast this and say delegate dot create delegate and we're going to create the thing that we want to actually return and we're going to have to specify the type of the delegate so again we're going to copy that the syntax is a bit bloaty in my opinion but it is what it is so first we specify the delegate type and let me just new line of this so you can see it better and then we're going to specify the method info that we want to invoke to get that thing back in our scenario it's the getter of the property so in the cash property that we have here it's the get get method then we're going to have to say true because it's a non-public get method so this will essentially do what we need in compile time but also using reflection but it will perform as a compile-time delete and to show you how this works let me just quickly copy that so i'm going to say delegate reflection or maybe just compile delegate is also a decent name and we have that and then the only thing we're going to say is return get property delegate provide the sum class and that's it and just to show you how this would work actually before i run the benchmarks just because you might be curious is i'm going to say reflection usage dot compile delegate here or value equals this and i'm going to change the debug and deduct it and i'm going to step into that and you can see we have a new class the value is default and then we're going to invoke that thing that we created and the value is different so we got that but let's see how that thing performs compared to the traditional or even the optimized version of reflection you can understand that this might not be applicable everywhere but it might be in your use case so let's take a look at how it performs i will need to add it as well in here so let me just duplicate this and say compile delegate here and this is still very much reflection it's just that we effectively mask it around the compile-time delegate to get the performance that this has so let's run this and take a look in a minute so results are back and as you can see look at this the simple get the compile time version six nanoseconds the compiled delegate still basically using reflection but around the compiled delegate seven nanoseconds we achieved almost almost compile time performance with that approach again might not be applicable everywhere and it does have its limitations and we're gonna take a look at those now but this is pretty damn good for this specific use case and i've actually used this in production capacity because i i just have to and perform so so well so i hope you haven't seen this before because it always wows um people when i saw that for the first time it's just such an interesting feature now it has its limitations and the limitations that i i see is the following there's many times where you don't actually have direct compile-time access to the type so you cannot use this if this was for example an object because you wouldn't know what the type would be but then even if you could do it like compile time here you still wouldn't be able to use it it just doesn't doesn't resolve it doesn't work with a specific approach and there is an approach that works let me just show you exactly the problem because i might be confusing you so if i duplicate this and i call this very internal class so an internal class which is what we're gonna mark this by the way um can't even be uh referred to do they just mess up the name in turn null so this is now an internal class and if i go to the reflection usage and i just duplicate this to show you what i mean comment this out for now if i change to a very internal class as you can see just doesn't doesn't do it and if i did something like private static read only type and i said a very important class type and i got it from the type um dot get type method where i just say um i think first is the fully qualified name which is this dot very entire class coma and then the the name of the library namespace which is this so if i do this then there is no way for me to properly cast the type here in this delegate to provide the same basically the same functionality and that's a problem so in order to get around that and i'm actually gonna keep this class type here um we are going to dive into uncharted territory if you may and we're going to emit il code now it's the least safe way you can go about this but if you need this to be compile time but also use a runtime type that you can't access in compile time you have to do it this way i'm going to simplify it however for us and let me just quickly delete this and name this admitted il version i'm going to say that i'm going to add a new get package called sigil and sigil will make this way way easier to implement we effectively are going to be building a very similar compile time delegate but this time we are going to be allowed to use a runtime type to implement that so it's going to look like this we're going to have a private static read-only emit of a function of an object that returns a string and i'm doing that explicitly to show you that you can actually do it without needing to have the access of the type in compile time so emit a function that um receives an object and it has a string and we're going to call that get property emitter and we're going to say emit that basically dot new dynamic method and we're basically writing a method as we're going and that method will be called get internal property value so like we're gonna call it but okay let's let's name it that um i'm just expanding so you can see it then this method doesn't have an argument so load argument 0 then cost class because we want to cast it in the same way that we casting it here but we couldn't before so now we can because we can actually get the type in compile time we're going to use this type sorry in in run time we're going to use this type here and then we're going to call the method that we want which is the same method it's the cache property.getgetmethod true and it's definitely not null and then we just wanna return so it's a fluent api around il emission if you don't know about io code i actually made a video on that uh writing hello world with iel so you can check that as well if you're interested so this thing will emit the code that we need it does mean that we still need to create a delegate and we're going to do that now and it's actually very simple all we need to do is say again private static read only and it's the same type actually it's the same type as the thing we created so it's this one i'm gonna say get property and meet the delegate equals and it's basically the git property emitter dot create delegate and this will basically do the same thing as we did before but we can now use a type that we get access to in runtime and can be internal it can be it can be anything and you can use it dangerous works so yeah use of your own discretion so now we are going to use that type to instantiate it so i'm going to say internal class equals activator i'm going to just imitate how this would be instantiated in runtime and but you don't have access to that so we have the internal class and then we're gonna return using the get property emitted um delegate and we're gonna provide the entire class here and this will work i'm going to show you how it will work by going in the program commenting that out i know this might look like voodoo um and you probably won't have to do it this way but there is a path forward and it's very fast let me show you reflection usage dot emitted il version var value and again this is accessing the internal class that you don't have access to in compile time it's just a runtime thing that you can grab and use so step into that um the initializer through an exception oh it found the wrong oh that's because we're using the the cache property from the the public class while we're using the the private class so let me just quickly copy that and show you uh the new version here actually and take cast internal property and then this should be this yep and then this goes [Music] here so let's do this again here we go entire clasp again we just instantiated this class you can see that it's the entire class that i have there that i don't have access to in compile time and then if i do this and step over without any issue i got the value and now that i've proven i can actually do this let's run the benchmarks and see how this new way this exotic way of doing it would perform let me go back and remove this and add it into the benchmarks and now with that you can basically do anything you can access all those things that you couldn't do in compile time and still get as you're going to see compile level comparable performance with reflection at basically the expense of some safety that you can actually uni-test around and guard around actually wait a second this wouldn't give you a realistic view because we're creating an instance using the activator which is reflection every single time so what i'm going to end up doing again is caching it so we can only focus on the get bit which might be unfair to all the other versions that create a new class every time so i'm going to extract that as well for the final examples so i'm going to say private read-only object this and we're going to say new activator based on the type so this goes here here we go and then we are going to use that so pre-initialized and then for everything else we're going to do the same so private static read only a very public class equals new very public class and then i can just remove that and i'm going to reuse it basically on everything so here here and here and here okay yeah and i think that's it everything now is covered so if i go back to my benchmarks all of them are here let's run this again and this should be more fair towards what we're testing so results are back and as you can see with all the removals and all the improvements we have the simple get returning in 2.7 nanoseconds within margin over the very very tiny measurements those ones um traditional is always the slowest optimized is almost half that then compiled delegate 3.4 and m80 dial version 4.2 this is all within margin of error basically these three are technically the same i mean these two are definitely the same this is always going to be a bit faster but very very tiny bit so as you can see three different ways to optimize with one being very easy one being a bit more complicated and more situational and one being a lot more complicated and maybe harder to understand but both of them are very very performant to the point where they perform the exact same as compile time so make what you want out of this i'm just here to show you that this thing exists and you can use it and it performs pretty damn fast that's all i had for you for this video thank you very much for watching special thanks to my patreons for making the videos possible if you want to support me as well you're going to find the link description down below leave a like if you like this video subscribe for more quick like listen ring the bell as well and i'll see you in the next video keep coding
Info
Channel: Nick Chapsas
Views: 14,502
Rating: 4.9255319 out of 5
Keywords: Elfocrash, elfo, coding, .netcore, dot net, core, C#, how to code, tutorial, development, software engineering, microsoft, microsoft mvp, .net core, nick chapsas, chapsas, clean code, reflection in c#, reflection in .NET, optimizing reflection, fast reflection, Optimizing Reflection in C# to achieve compile-time performance, how to make reflection fast, reflection in F#, dotnet, .net
Id: er9nD-usM1A
Channel Id: undefined
Length: 21min 34sec (1294 seconds)
Published: Fri Jun 11 2021
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.