This post was authored by Morgan Brown, a Software Development Engineer on the .NET Native team. It is the fifth post in a series of five about Runtime Directives. Please see the first posts in this series, Dynamic Features in Static Code, Help! I Hit a MissingMetadataException!, Help! I Didn’t Hit a MissingMetadataException!, and Making Your Library Great, before reading this post.
The previous posts in this series are about getting your app working with .NET Native, but you don’t want just working. You want excellent. We’ll focus on how to tune your runtime directives and other dynamic behavior to make your app or library sing. In particular, we’ll dig into cutting down app size, which in turn improves memory usage, runtime performance, and build time (that’s right, as you optimize, building will get easier!)
Let’s start out with something that sounds a little crazy, but will make a big difference: delete your rd.xml file. (Ok, make a backup first.)
Try doing a quick release build and measure your results – did your resulting binaries get significantly smaller? If they didn’t, you may have an app that really uses all of the code in the app package and your application doesn’t have many optimization opportunities available through Runtime Directives. It’s much more likely you saw a big difference, so this can be a good baseline – this is the minimum size you could possibly get to (and there’s a chance your app doesn’t work).
The default rd.xml file that gets added to your project when you enable .NET Native was chosen for a balance of compatibility with any app and good performance. The directive included in it says to compile all code found in your app package and include extra reflection information for it. Most apps include .NET libraries in the app package and often, only pieces of those libraries are needed. That default rd.xml tells the compiler to skip optimizing all of that unwanted code away and instead spend megabytes of binary size and tens of seconds compiling it.
So now that you’ve deleted your rd.xml, your app may be hitting MissingMetadataExceptions. Before diving for that backup copy, let’s see if we can make your app work by including only the code you need and not large amounts you don’t. At a high level, what that takes is having runtime directives that explain how reflection in your app works to the compiler. Here’s how you get there:
- You probably don’t have to start from scratch. If your app is like most, lots of the dynamic behavior in your app really happens in third party libraries. For some of those, we’ve included rd.xml files in the SDK that automatically get used by the compiler. If you see MissingMetadataExceptions coming out of calls to libraries, check if there’s a newer version of the library that includes rd.xml; if there’s not, tell the author that you want their library to be part of your fast and lightweight .NET Native App and they should check out Making Your Library Great.
If you’ve factored your own code into libraries that work off of reflection, try the same article. The goal here is that the library code should use reflection directives like GenericParameter and Parameter that can automatically pick up all of the individual types you use with a library. That way, your code will work every time, without you either having to write a ton of directives or picking one that includes lots of stuff you don’t need. A good goal is to not use any Namespace or Assembly directives and instead just use Parameter and GenericParameter directives and maybe a few individual types.
See if there are some places you use reflection that you don’t need to. A few good candidates are:
Replace the C# dynamic keyword with strongly typed code using interfaces or generics. It turns out that behind that simple-looking keyword is a ton a reflection. As a bonus, this will make IL versions of your app faster too.
Compiled LINQ expressions are a useful optimization on a runtime with a JIT since they get compiled at runtime. However, on a static runtime like .NET Native, they get interpreted instead of compiled and use lots of reflection too. Instead, think about using handwritten methods that will get compiled up-front. On .NET Native, compiled methods are always faster than interpreted code (and don’t need rd.xml).
Instead of Type.GetType(“MyWellKnownType”), consider typeof(MyWellKnownType). Similarly, when you can, construct delegates directly from methods instead of using MethodInfo.CreateDelegate.
Lots of apps use an old trick to avoid having to come up with strings for INotifyPropertyChanged involving a LINQ Expression with the property. You can now use the CallerMemberName attribute, which will cause the C# compiler to automatically fill in the string without adding any reflection requirements.
So how do you know if you’ve done a good job? Of course, getting your app working with more specific directives is a pretty good sign. Aside from your total binary size, there’s a simple metric that can help you understand how well you’re cutting down on excess reflection metadata. In your Visual Studio project directory, under
obj\(Architecture)\(Debug or Release)\(Name of your project).ilc\intermediate\ILTransformed, there’s a file named
(Name of your project).reflectionlog.csv. That file contains all of the types and members that have been enabled for reflection and which degrees were applied to them. You can use the size of that file as a proxy for how much compiled stuff you’re asking for and you can also skim the contents to get an idea of what you might be pulling in and whether it seems like there are large swaths of things you don’t expect.
If you go through all of that and either couldn’t get your app working without the original rd.xml or it really just didn’t get much smaller despite having library code you probably don’t need, we’d love to hear from you. We’re constantly improving the compiler and data helps us come up with new ways to make apps better automatically. Please feel free to leave comments at the end of this post or email us at firstname.lastname@example.org.