I've been playing with the .NET's native AOT (Ahead of Time Compilation)(github.com/dotnet/runtime…) support to get a better understanding of the implications for .NET libraries and applications that want to take advantage of it. #dotnet
The promise of AOT is that you trade off some compile time performance and dynamism for a system that can optimize for reduced output size, startup speed and improved steady state throughput.
.NET has had lots of different versions of AOT over the years (ngen, crossgen, ready to run). Those versions of AOT always run with a JIT fallbacks so binaries carry both the native compiled code *and* the IL as a fallback that the JIT can use to further optimize.
These modes are all hybrid and have various tradeoffs as a result. The Native AOT in dotnet/runtimelab is true AOT in a sense that there is no dynamic code fallback and is meant to support running in environments where there can't be any dynamic code generation
Why? Small output binaries, improved startup time, ability to run in restricted environments (No JIT), better code generation in cases where the entire program can be analyzed. This is great for immutable deployment environments like containers.
Why would you care about startup time? Command line tools are a big scenario for this. They aren't long running applications so you want them to start extremely quickly without having to warm up. Serverless functions is another place where cold start matters a lot.
Client applications that want to startup quickly (notepad, vscode, etc). You'd love it if these applications avoided any repeated work on every application start, especially if it doesn't change.
This also matters for Blazor applications that want to compile directly to WASM. The underlying runtime is different but the principles of Native AOT are the same. If you want to save on size, then it's a problem carrying both the native code and managed code.
So what does it mean for your code? Well we want to make sure your code doesn't break. It would be ideal if we could take any piece of managed code, compile it to native and hit it "work". You may not see the size benefits but it won't break.
Then to get the size benefits we need to start describing the "dynamic behavior" of your application "statically". This is why people say that reflection is AOT unfriendly. It's really about the linking process that tries to remove unused code to give you the size benefits.
If we look at the other existing AOT solutions for .NET, they have about a 3x size penalty. You're trading off binary size for startup performance. FYI, .NET itself is natively compiled, if it wasn't it would be super slow to JIT all of it on every application startup.
So to avoid this 3x size bloat, we want to
- Get rid of the IL. There's no JIT so no need for the IL to be there.
- Remove metadata for things we don't reflect on.
- Remove unused code. To do this with confidence, we need a way to describe reflection to the linker.
- Warn if the linker can't figure things out
- Optimize the crap out of the remaining code
...
- Profit!
So what things are off the table? Well things that end up needing new code (IL) at runtime. For example, loading new assemblies or generating new code using reflection emit. If new IL instructions are added at runtime, there's nothing to execute it.
This means APIs like
- Assembly.Load
- Array.CreateInstance,
- Type.MakeGenericType.
Are *potentially* unsafe. Making new types at runtime that the AOT compiler didn't see at compile time is problematic.
These APIs are pretty low level but they are used in some key subsystems in .NET namely:
- Serializers
- Dependency injection containers
- RPC systems
- ORMs
- AutoMapper
etc
The other pattern that is anti-AOT are plugin systems. Systems that load assemblies to discovery new runtime behavior are inherently incompatible with this model. This is one of the things .NET is really good at that systems like golang are not good at.
In golang, you'll see lots of out of process extensibility via gRPC instead of loading dynamically linked libraries in process. This obviously is quite a bit more painful than the typical plugin models .NET developers are used to but it's one of the tradeoffs that need to be made
Remember that thing I said about new code not working? That's only partially true. It could in theory be interpreted at runtime and we have various subsystems that ship with an interpreter.
Expression tree compilation for example has a built in interpreter so it still "works". The problem is that its often slower than just using reflection, especially since Native AOT has "faster reflection".
Right now, we have a multi-stage plan for .NET that includes:
- Making .NET itself linker friendly.
- Making .NET apps/libraries linker friendly.
- Making .NET apps/libraries single file friendly.
- Making .NET apps/libraries AOT friendly.
Currently a lot of this work is being driven by Blazor but the overall effort will accrue towards making the .NET ecosystem better in more environments.
If you care about this stuff, I recommend following @MStrehovsky. I've learnt a lot from him in this space and you will too if you follow.
• • •
Missing some Tweet in this thread? You can try to
force a refresh
After spending the last 5 years deep in networking code, I can say one of the most fundamental missing pieces is the ability to know why a connection closed (root causing the problem).
I wish all the protocols from here on out would also have a "reason for close" field for additional debugging information. The cumulative time that has been lost trying to debug what part of the stack caused the connection to drop (OS, proxy, libraries) probably adds up to years.
This gets even more complicated by these "invisible layers" introduced by virtualization. Cloud networking comes to mind... don't forget the layers built on top of that in orchestrators like kubernetes.
How is it different from SignalR you ask? Well internally it's built on the same underlying tech but the big difference is that there's no client requirement or protocol requirement, BYOWL (bring your own websocket library).
The mainline scenarios are also focused on severless so we can handle your long running websocket connections and trigger HTTP calls to any backend. It can be azure functions or any addressable HTTP endpoint!
The transition to asynchronous code changes a lot of the basics we're used to. There's usually much more concurrency in these systems and that becomes more challenging to diagnose when things go wrong. #dotnet#aspnetcore
The threads are usually switching between lots of concurrent operations as various IO operations complete. Here's an example of visualizing lots of concurrent operations happening in the parallel tasks window. The right is parallel stacks. Async stacks vs OS thread stacks.
Essentially the async logical stacks are linked lists of objects stored on the heap. There may not be any code executing if a request is waiting on IO for example.
4 months I got to spend time with friends and family. We went on cruises, and had lots of beach days. The kids learned how to swim at the beach. They got to live with grandparents for 3 of those 4 months. Got to spend my birthday here for the first time in over 10 years.
Got to experience what it would be like to work remotely from Barbados. Started a relationship between Microsoft and the UWI (University of the West Indies). Joined a WhatsApp group of coders of Barbados and am doing what I can to make them love .NET 🤣.
.NET APIs you didn't know existed. StringBuilder.GetChunks docs.microsoft.com/en-us/dotnet/a…. You can use this API to get access to the internal buffer of the StringBuilder instead of turning it into a String. #dotnet
StringSplitOptions.TrimEntries docs.microsoft.com/en-us/dotnet/a…. Ever split a string and didn't want to trim each entry? Me too, now you can use this enum to do it!
There are some common issues I see when people use ConcurrentDictionary:
- Dealing with the fact that GetOrAdd executes the creation callback more than once. This means if you create something expensive, it'll happen more than once. (this is documented) #dotnet
- If you create a disposable object in your factory and it gets called multiple times, the created objects will never be disposed.
- Making sure asynchronous factories and are called once. (making all callers wait on the same value).
Here's a contrived example of the resource leak. The code runs a couple of concurrent operations and uses GetOrAdd to cache a FileStream. This will call File.OpenRead potentially multiple times which will result in opening a handle to the resource and not disposing it.