The .NET runtime is the foundation of the .NET platform and is therefore the source of many improvements and a key component of many new features and enabled scenarios. This is even more true since Microsoft started the .NET Core project. Many of the performance improvements and key changes we made to optimize scenarios (like Docker containers) have come from the runtime.

With each new .NET version, the .NET team chooses which new features and scenarios to enable. We listen to feedback from users, with most of it coming from GitHub issues. We also look at where the industry is headed next and try to predict the new ways that developers will want to use .NET. The features I want to tell you about are a direct outcome of those observations and predictions.

I'm going to tell you about two big .NET 5.0 projects: single file apps, and ARM64. There are many other improvements in .NET 5.0 that there simply isn't room to cover here, like P95 performance improvements, new diagnostic capabilities (like dotnet-monitor), and advances in native interop (like function pointers). If you're mostly interested in performance improvements, please check out the .NET 5.0 performance post at Take a look at the .NET blog ( to learn about the full set of improvements in this release and why you should consider adopting .NET 5.0 for your next project.

Single File Apps

Single file apps significantly expand .NET application deployment options with .NET 5.0. They enable you to create standalone, true xcopy, single-file executables. This capability is appealing for command-line tools, client applications, and Web applications. There's something truly simplifying and productive about launching a single file app from a network share or a USB drive, for example, and having it reliably just run on any computer without requiring installation pre-steps.

Single file apps are supported for all application types (ASP.NET Core, Windows Forms, etc.). There are some differences, depending on the operating system or application type. Those will be covered in the following sections.

All of This Has Happened Before

Many people have correctly observed and noted that .NET Core apps are not as simple as .NET Framework ones. For the longest time, .NET Framework has been part of Windows and .NET Framework executables have been very small single files. That's been really nice. You could put a console or Windows Forms app on a network share and expect it run. This has been possible because .NET Framework is integrated into Windows. The Windows Loader understands .NET executable files (like myapp.exe), and then hands execution off to the .NET Framework to take over.

When Microsoft built .NET Core, we had to start from scratch with many aspects of the platform, including how apps were launched. A driving goal was providing the same application behavior on all operating systems. We also didn't want to require operating system updates to change the behavior (like needing to run Windows Update to get a .NET Core app working). This led us to not replicate the approach we used for .NET Framework, even though we (very) briefly considered it.

We needed to build a native launcher for discovering and loading the runtime for each supported operating system. That's how we ended up with multiple files: at least one for the launcher and another for the app. We're not alone; multiple other platforms have this too. For example, Java and Node.js apps have launchers.

Back to Basics

Over time, we heard more and more feedback that people wanted a single file application solution. Although it's common for language platforms to require launchers, other platforms like C++, Rust, and Go don't require them, and they offer single file as a default or an option you can use.

You wouldn't necessarily think of single file apps as a runtime feature. It's a publish option, right? In actuality, the work to enable single file apps was done almost exclusively in the runtime. There are two primary outcomes we needed to enable: include the runtime within the single file and load managed assemblies from within the single file. In short, we needed to adapt the runtime to being embedded in a single file configuration. It's sort of a new hosting model.

As I said, we use a launcher for .NET Core apps. It's responsible for being a native executable, discovering the runtime, and then loading the runtime and the managed app. The most obvious solution was to statically link the runtime into the launcher. That's what we did, starting with Linux for the .NET 5.0 release. We call the result the “super host.” Native code runtime and library components are linked into the super host. Linux names are listed here (Windows names in brackets):

We focused on Linux for the single file experience with .NET 5.0. Some parts of the experience are only available on Linux, and other parts are also supported on Windows and macOS. These differences in capability will be called out throughout this document. There are critical challenges that we ran into building this feature. The combination of these issues led us to enable the broadest set of experiences with Linux, and then to wait to spend more time improving the other operating systems in upcoming releases. We also have work left to do to improve the Linux experience.

The first problem relates to the structure of the single file. The single file bundler copies managed assemblies to the end of the host (apphost or superhost) to create your app, a bit like adding ice cream to a cone. These assemblies can contain ready-to-run native code. Windows and macOS place extra requirements on executing native code that has been bundled in this way. We haven't done the work yet to satisfy these requirements. Continuing the analogy, these OSes require chocolate chips in the ice cream, but we only had time to get the basic vanilla and chocolate flavors ready. Fortunately, the runtime can work around this issue by copying and remapping assemblies in-memory. The workaround has a performance cost and applies to both self-contained and framework-dependent single file apps.

The second problem relates to diagnostics. We still need to teach the Visual Studio debugger to attach to and debug this executable type. This applies to other tools that use the diagnostics APIs, too. This problem only applies to single file apps that use the superhost, which is only Linux for .NET 5.0. You'll need to use LLDB to debug self-contained single file applications on Linux.

The last problem applies to digital signing on macOS. The macOS signing tool won't sign a file that's bundled (at least in the way we've approached bundling), the macOS app store won't accept .NET single file apps as a result. It also applies to macOS environments that require the “hardened runtime” mode, which is the case with the upcoming Apple Silicon computers. This restriction applies to both self-contained and framework-dependent single file apps.

We've significantly improved single file apps with .NET 5.0, but as you can likely tell, this release is just a stopping point on the .NET single file journey. We'll continue to improve single file apps in the next release, based on your feedback.

Now that we're through the theory, I'd like to show you the new experience. If you've been following .NET Core, you'll know that there are two deployment options: self-contained and framework dependent. Those same two options equally apply to single file apps. That's good. There are no new concepts to learn. Let's take a look.

Self-Contained Single File Apps

Self-contained single file apps include your app and a copy of the .NET runtime in one executable binary. You could launch one of these apps from a DVD or a USB stick and it would work. They don't rely on installing the .NET runtime ahead of time. In fact, self-contained apps (single file or otherwise) won't use a globally installed .NET runtime, even if it's there. Self-contained single file apps have a certain minimum size (by virtue of containing the runtime) and grow as you add dependencies on NuGet libraries. You can use the assembly trimmer to reduce the size of the binary.

Let's double check that we're on the same page. A self-contained single file app includes the following content:

  • Native executable launcher
  • .NET runtime
  • .NET libraries
  • Your app + dependencies (PackageRef and ProjectRef)

What you can expect:

  • The apps will be larger because they're self-contained, so will take longer to download/copy.
  • Startup is fast as it's unaffected by file size.
  • Debugging is limited on Linux. You'll need to use LLDB.
  • The native launcher is native code, so the app will only work in one environment (like Linux x64, Linux ARM64, or Windows x64). You need to publish for each environment you want to support.
  • On Windows and macOS, native runtime binaries are copied beside your (not quite) single file app, by default. For WPF apps, you'll see additional WPF native binaries copied. You can opt to embed native runtime binaries instead, however, they'll be unpacked to a temporary directory on application launch.

Because I focused on Linux for this scenario, I'll demonstrate this experience on Linux and then show which parts work on Windows and macOS.

Let's start with the Linux experience. I'll do this in a Docker container, which may be easier for you to replicate. I'll start by building an app as framework-dependent (the default), then as a self-contained single file app, and then as an assembly-trimmed self-contained single file app. A lot of tool output text has been removed for brevity.

r@thundera ~ % docker pull
r@thundera ~ % docker run --rm -it
root@a255:/# dotnet new console -o app
"Console Application" was created
root@a255:/# cd app
root@a255:/app# dotnet build -c release
root@a255:/app# time ./bin/release/net5.0/app
Hello World!

real    0m0.040s
root@a255:/app# ls -s bin/release/net5.0/
total 232
200 app     8 app.dll
4 app.deps.json   12 app.pdb
4 app.runtimeconfig.json
root@a255:/app# dotnet publish -c release
-r linux-x64 --self-contained true /p:PublishSingleFile=true
root@a255:/app# time ./bin/release/net5.0/
Hello World!

real     0m0.039s
root@a255:/app# ls -s bin/release/net5.0/
total 65848
65836 app     12 app.pdb
root@a255:/app# dotnet publish -c release
-r linux-x64 --self-contained true /p:PublishSingleFile=true /p:PublishTrimmed=true /p:PublishReadyToRun=true
root@a255:/app# time ./bin/release/net5.0/
Hello World!

real    0m0.040s
root@a255:/app# ls -s bin/release/net5.0/
total 27820
27808 app     12 app.pdb
root@a255:/app# exit
r@thundera ~ %

This set of dotnet commands and the extra information shown for size and startup performance demonstrates how to publish single file apps and what you can expect from them. You'll see that apps using the assembly trimmer (via the PublishTrimmed property) are much smaller. You will also see PublishReadyToRun used. It isn't strictly necessary but is available to make applications start faster. It will result in larger binaries. We recommend testing these features to see if they work well for your application and provide a benefit. Assembly trimming is known to break WPF apps, for example, by over-trimming. Check out for more information on assembly trimming.

The following example shows how to set these same properties in a project file.

<Project Sdk="Microsoft.NET.Sdk">
        <!-- Enable single file -->
        <!-- Self-contained or framework-dependent -->
        <!-- The OS and CPU type you are targeting -->
        <!-- Enable assembly trimming ? for self-contained apps -->
        <!-- Enable AOT compilation -->

The “Hello world” console app example demonstrates the baseline experience. Let's take a quick look at an ASP.NET Core Web service. You'll see that it's very similar. This time, I'll show the file sizes using ready-to-run and not.

r@thundera ~ % docker run --rm -it
root@71c:/# dotnet new webapi -o webapi
"ASP.NET Core Web API" was created
root@71c:/# cd webapi/
root@71c:/webapi# dotnet publish -c release
-r linux-x64 --self-contained true /p:PublishSingleFile=true /p:PublishTrimmed=true /p:PublishReadyToRun=true
root@71c:/webapi# ls -s bin/release/net5.0/linux-x64/publish/
total 75508
4 appsettings.Development.json  4 web.config
20 webapi.pdb    4 appsettings.json
75476 webapi
root@71c:/webapi# dotnet publish -c release
-r linux-x64 --self-contained true /p:PublishSingleFile=true /p:PublishTrimmed=true
root@71c:/webapi# ls -s bin/release/net5.0/linux-x64/publish/
total 44056
4 appsettings.Development.json 4 web.config
20 webapi.pdb   4 appsettings.json
44024 webapi
root@71c:/webapi# tmux

Next, I'll also show the app running by launching it and calling it with curl. In order to show the app and call it at the same time, I'll use a two-pane horizontally split tmux (installed via apt-get) session. Again, I'm using Docker.

Figure 1: Calling single file ASP.NET Core Web API with curl
Figure 1: Calling single file ASP.NET Core Web API with curl

The command to launch the app: ./bin/release/net5.0/linux-x64/publish/webapi.

The ASP.NET Core example is very similar to the console app. The big difference is that the size increases due to ready-to-run compilation is more apparent. Again, you should test your application in various configurations to see what's best.

If you publish a self-contained single-file app with containers, you should base it on a runtime-deps image. You don't need to use an aspnet image, because ASP.NET Core is already contained in the single-file app. See image URLs below.

I'll now show you the experience on macOS, which matches the experience on Windows. As stated earlier, we didn't build a superhost for macOS or Windows in .NET 5.0. That means that the native runtime binaries are present beside the (not quite) single file. That's not the desired behavior, but it's what we have for .NET 5.0. We added a feature to embed these files and then unpack them upon application launch: IncludeNativeLibrariesForSelfExtract. That model isn't perfect. For example, the files can get deleted from the temp location (or are never deleted). This feature isn't generally recommended, but it may be the right choice in some cases.

r@thundera ~ % dotnet new console -o app
"Console Application" was created
r@thundera ~ % cd app
r@thundera app % dotnet publish -c release
-r osx-x64 --self-contained true
/p:PublishSingleFile=true /p:PublishTrimmed=true /p:PublishReadyToRun=true
r@thundera app % ls -s bin/release/net5.0/
total 47856
26640 app
24 app.pdb
1792 libSystem.IO.Compression.Native.dylib
144 libSystem.Native.dylib
32 libSystem.Net.Security.Native.dylib
104 libSystem.Security.Cryptography.Native...
304 libSystem.Security.Cryptography.Native...
5232 libclrjit.dylib
13584 libcoreclr.dylib
r@thundera app % ./bin/release/net5.0/osx-x64/publish/app
Hello World!
r@thundera app % dotnet publish -c release
-r osx-x64 --self-contained true /p:PublishSingleFile=true /p:PublishTrimmed=true
/p:PublishReadyToRun=true /p:IncludeNativeLibrariesForSelfExtract=true
r@thundera app % ls -s bin/release/net5.0/osx-x64/publish
total 49264
49240 app   24 app.pdb

Zooming out, self-contained single-file apps are good in the same scenarios that self-contained apps generally are: computers that you don't control and where you can't count on a runtime being installed. We expect this deployment option to show up in a lot of new places, given the greatly improved ease to-use and convenience.

Framework-Dependent Single File Apps

Framework-dependent single file apps include only your app in one executable binary. They rely on a globally installed runtime (of the right version). They're really just a small step-function optimization on framework-dependent apps, as they already exist. For example, they don't use the superhost described earlier, but the regular apphost. If you deploy apps to an environment that's always guaranteed to have the right .NET version installed, framework-dependent single file apps are the way to go. They will be much smaller than self-contained single file apps.

Let's double check that we're on the same page. A framework-dependent single file app includes the following content:

  • Native executable launcher (apphost, not superhost)
  • Your app + dependencies (PackageRef and ProjectRef)

What you can expect:

  • The apps will be smaller, so will be quick to download.
  • Startup is fast, as it's unaffected by size.
  • The native launcher is native code, so the app will only work in one environment (like Linux x64, Linux ARM64, or Windows x64). You'll need to publish for each environment you want to support.
  • Unlike self-contained single file apps, there'll be no additional native runtime binaries copied beside your single file app.

I'll demonstrate this experience on Windows. The experience is the same on Linux and macOS.

C:\Users\rich>dotnet new console -o app
"Console Application" was created
C:\Users\rich>cd app
C:\Users\rich\app>dotnet publish -r win-x64--self-contained false /p:PublishSingleFile=true
C:\Users\rich\app>dir bin\Debug\net5.0\win-x64\publish
Volume in drive C has no label.
Volume Serial Number is 9E31-D4BD

Directory of C:\Users\rich\app\bin\Debug\net5.0\win-x64\publish
148,236 app.exe
9,432 app.pdb
2 File(s)        157,668 bytes
Hello World!

We can do the same thing with an ASP.NET Core application.

C:\Users\rich>dotnet new webapi -o webapi
"ASP.NET Core Web API" was created
C:\Users\rich>cd webapi
C:\Users\rich\webapi>dotnet publish -r win-x64
--self-contained false /p:PublishSingleFile=true
C:\Users\rich\webapi>dir bin\Debug\net5.0\win-x64\publish
Volume in drive C has no label.
Volume Serial Number is 9E31-D4BD

Directory of C:\Users\rich\webapi\bin\Debu...

        162 appsettings.Development.json
        192 appsettings.json
        473 web.config
    267,292 webapi.exe
     19,880 webapi.pdb
     5 File(s)        287,999 bytes

The difference between self-contained and framework-dependent apps becomes apparent. Framework-dependent single file apps are tiny. As stated at the start of this section, they're the clear winner for environments where you can count on the runtime being installed.

A great example of being able to depend on a runtime being available is Docker. If you publish a framework-dependent single-file ASP.NET Core app with containers, you should base it on an ASP.NET image, since you will need ASP.NET Core to be provided by a lower-level image layer. Image location:

Next Steps for Single File Apps

We haven't defined our final plan for .NET 6.0 yet, but we do have some ideas, some of which I've already drawn attention to. The two most obvious focus areas are enabling the superhost model for Windows and macOS in addition to Linux, and enabling first-class debugging for self-contained single file apps. You may have noticed that ASP.NET Core apps have some extra files hanging around. Those should be cleaned up and made optional. That's likely a small work-item.

Assembly trimming is an important capability for making single file apps smaller. We have implemented both conservative and aggressive modes for assembly trimming but haven't yet landed a model where we'd feel confident enabling the aggressive mode by default. This will be a continued area of focus, likely for the next couple of releases.


ARM64 is a very popular family of CPUs, designed by ARM holdings. You have an ARM chip in your phone, and it's looking like ARM chips will become popular in laptops, too. The Surface Pro X, The Samsung Galaxy Book S and the upcoming Apple Silicon-based Mac line all use ARM64 chips. On the .NET team, we're familiar with the various ARM instruction sets and have had support for ARM with .NET Core since the 2.0 version. More recently, we've been improving ARM64 performance to ensure that .NET apps perform well in environments that rely on ARM chips. ARM chips are also popular with IoT. The Raspberry Pi 3 and Raspberry Pi 4 single board computers use ARM64 chips. Wherever ARM chips end up being used, we want .NET apps to be a good option.

Take a look at to learn more about what we've done for ARM64 in this release.

What's Special about ARM Chips?

ARM chips aren't new. They've been used in embedded scenarios for years, along with chip families like MIPS. That's why, for most people, their first awareness of ARM chips is with their phones. ARM's two big advantages are low power and low cost. No one enjoys their phone running out of power. ARM chips help to prolong battery length. Electrical usage is important in plenty of other domains, and that's why we've seen ARM chips show up in laptops more recently.

The downside of ARM chips has been lower performance. Everyone knows that a Raspberry Pi as a desktop computer isn't going to be competitive with an Intel i7 or AMD Ryzen PC. More recently, we've seen ARM chips deliver higher performance. For example, most people think of the latest iPhone or iPad from Apple as high-performance. Even though Apple doesn't use the ARM branding, the “A” in their “A Series” chips could equally apply to Apple as it could to ARM. The future looks bright for ARM technology.

Hardware Intrinsics

CPUs are big blocks of silicon and transistors. The great way to get higher levels of performance is to light up as many of those transistors as possible. Soon after starting the .NET Core project, we created a new set of APIs that enabled calling CPU instructions directly. This enables low-level code to tell the JIT compiler “hey, I know exactly what I want to do; please call instruction A, then B and C, and please don't try to guess that it's something else.” We started out with hardware intrinsics for Intel and AMD processors for the x86 instruction set. That worked out very well, and we saw significant performance improvements on x86 and x86-64 processors.

We started the process of defining hardware intrinsic APIs for ARM processors in the .NET Core 3.0 project. Due to schedule issues, we weren't able to finish the project at that time. Fortunately, ARM intrinsics are included in the .NET 5.0 release. Even better, their usage has been sprinkled throughout the .NET libraries, in the places where hardware intrinsics were already used. As a result, .NET code is now much faster on ARM processors, and the gap with performance on x86-64 processors is now significantly less.

ARM64 Performance Improvements

Let's take a look at some performance improvements. The following are improvements to low-level APIs. You won't necessarily call these directly from your code, but it's likely that some APIs you already use call these APIs as an implementation detail.

You can see the improvements to System.Numerics.BitOperations in Table 1, measured in nanoseconds.

System.Collections.BitArray improvements are listed in Table 2, measured in nanoseconds.

Code-Size Improvements

By virtue of generating better code for ARM64, we found that code size dropped - a lot. This affected size in memory but also the size of ready-to-run code (which affects the size of single file apps, for example). To test that, we compared the ARM64 code produced in .NET Core 3.1 vs. .NET 5.0 for the top 25 NuGet packages (subset shown in the Table 3). On average, we improved the code size of ready-to-run binaries by 16.61%. Table 3 lists NuGet packages with the associated improvement. All the measurements are in bytes (lower is better).

Straight-Out Sprint

You might be wondering how these changes play out in an actual application. If you've been following .NET performance for a while, you'll know that we use the TechEmpower benchmark to measure performance, release after release. We compared the various TechEmpower benchmarks on ARM64, for .NET Core 3.1 vs .NET 5.0. Naturally, there have been other changes in .NET 5.0 that improve the TechEmpower results. The numbers in Table 4 represent all product changes for .NET 5.0, not just those targeted ARM64. That's OK! We'll take whatever improvements are on offer. Higher numbers are better.

Next Steps for .NET and ARM64

To a large degree, we implemented the straightforward and obvious opportunities to improve .NET performance on ARM64. As part of .NET 6.0 and beyond, we'll need users to provide us with reports of ARM64 performance challenges that we can address, and to perform much deeper and more exotic analysis of ARM64 behavior. We will also be looking for new features in the ARM instruction set that we can take advantage of.

We are, at the time of writing, enabling very early .NET 6.0 builds on Apple Silicon, on Desktop Transition Kits that Apple made available to our team. It's exciting for us to see the ARM landscape expand within the Apple ecosystem. We look forward to seeing developers taking advantage of .NET ARM64 improvements on Mac desktops and laptops.

In Closing

.NET has proven to be a truly adaptable platform, in terms of application types, deployment models, and chip architectures. We've made technical choices that enable a uniform experience across application types while offering the best of what an underlying operating system or chip architecture has to offer, with few or any compromises. We'll continue expanding the capabilities of .NET and improving performance so that your applications can run in new places and find new markets.

Just over five years ago, we announced a plan to move to an open source development model on GitHub. Many of the improvements in .NET 5.0 have come from the .NET community. This includes individuals and corporations. Thanks! A sound architecture matters, but the care and technical capability of the .NET community is the source of forward progress for the platform.

You now know more about what we're delivering with .NET 5.0. Did we make good choices? We're always listening on the dotnet/runtime repo on GitHub. Tell us what you think.

Thanks to Kunal Pathak for ARM64 performance information.

Table 1: Hardware intrinsics performance improvements - BitOperations

`BitOperations` methodBenchmark.NET Core 3.1.NET 5Improvements

Table 2: Hardware intrinsics performance improvements - BitArray

`BitArray` methodBenchmark.NET Core 3.1.NET 5Improvements
ctor(bool[])BitArrayBoolArrayCtor(Size: 512)1704.68215.55-87%
CopyTo(Array, int)BitArrayCopyToBoolArray(Size: 4)269.2060.42-78%
CopyTo(Array, int)BitArrayCopyToIntArray(Size: 4)87.8322.24-75%
And(BitArray)BitArrayAnd(Size: 512)212.3365.17-69%
Or(BitArray)BitArrayOr(Size: 512)208.8264.24-69%
Xor(BitArray)BitArrayXor(Size: 512)212.3467.33-68%
`Not()`BitArrayNot(Size: 512)152.5554.47-64%
SetAll(bool)BitArraySetAll(Size: 512)108.4159.71-45%
ctor(BitArray)BitArrayBitArrayCtor(Size: 4)113.3974.63-34%
ctor(byte[])BitArrayByteArrayCtor(Size: 512)395.87356.61-10%

Table 3: ARM64 code-size improvements: Top NuGet packages

`Nuget` packagePackage version.NET Core 3.1.NET 5.0Code size improvement

Table 4: ARM64 Web throughput performance improvements: TechEmpower

TechEmpower Benchmark.NET Core 3.1.NET 5Improvements
JSON RPS484,256542,463+12.02%
Single Query RPS49,66353,392+7.51%
20-Query RPS10,73011,114+3.58%
Fortunes RPS61,16471,528+16.95%
Updates RPS9,15410,217+11.61%
Plaintext RPS6,763,3287,415,041+9.64%
TechEmpower Performance Rating (TPR)484538+11.16%