Skip to content

Azure Functions Performance Exploration

Notifications You must be signed in to change notification settings

pettijohn/func-cs

Repository files navigation

Azure Functions Performance Exploration

Or, How to run .NET 7 Native AOT in an Azure Function (and is it a good idea?)

TL;DR: For C#, .NET 6 In-Process appears to be the best choice - performance is the best, and it is a a fully-featured, supported out-of-box solution. Native AOT may have cold start benefits, and may be better in certain workloads, but you'll have to roll your own HTTP listener, router, etc.

This repository explores the performance of Azure Functions and their various hosting models, In-Process .NET6, Isolated .NET7, and Custom using .NET 7.0 Native AOT.

Microsoft describes .NET 7.0 Native AOT: "The benefit of native AOT is most significant for workloads with a high number of deployed instances, such as cloud infrastructure and hyper-scale services." They also say: "native AOT promises to help build faster, lighter apps, while explaining what it is for those not familiar, noting that its main advantages affect: Startup time, Memory usage, Access to restricted platforms (where no just-in time (JIT) compilation is allowed), Smaller size on disk." Also: "Native AOT is best suited for environments where startup time matters the most." Compelling, especially for Functions-as-a-Service!

But! Azure Functions only support in-process for long term support versions of .NET (6.0); you can only run NET 7.0 in isolated processes. Both support ReadyToRun (a form of AOT compilation), and it looks like they both use reflection to identify Azure Functions (relatively slow at startup to discover them all). There is no out-of-the-box way to run .NET 7 Native AOT as Azure Functions. This repository explores that, spinning up an HTTP Listener in C#, and compares performance of the three approaches.

The shared-logic project targets net6.0 and net7.0, and uses source generation for JSON serialization in net7.0 only. The ClassThatDoesSomeWork creates a (small) object, serializes it to JSON, encrypts it with AES, converts to Base64 and returns the string. It's meant to represent a typical cloud function workload, such as validating a JWT & serializing data to JSON. Since we're interested in Azure Function performance, especially cold & warm start, there is no IO to e.g. a database, only the pure compute workload of the Function.

Results

Average of 10,000 iterations each, includes cold & warm. Functions called in round robin (one call to each, then repeat) in case the CPU throttles as the test drags on. Cold start table is fifty invocation of each method (restart the func process each time). I repeated these tests and, while the numbers varied from run to run, the pattern held true: inproc was fastest, native was slightly slower, and isolated was a bit slower yet.

I assume that In-Process wins because, despite Native AOT's performance benefits, In-Process can just pass an object pointer for each function call as opposed to the overhead of out-of-process HTTP over a pipe. I don't know why, in my expensive scenario, which runs the same computation in a loop, in-proc does so well. Native AOT may be a better fit for cases where the compute cost is considerably higher than the cost of out-of-process HTTP. I also didn't measure memory consumption, so Native AOT may be less expensive in some cases as well.

Result set 1 - Typical CRUD API compute

The Function executes ClassThatDoesSomeWork once.

Method Average response (ms) 10k runs StdDev
func-cs-inproc 2.12 3.48
func-cs-isolated 3.52 6.05
func-cs-nativeaot 2.77 2.90
Cold start Avg of 50 runs (ms)
func-cs-inproc 280.39
func-cs-isolated 544.15
func-cs-nativeaot 277.66

Result Set 2 - Computationally Expensive

The Function executes ClassThatDoesSomeWork ten thousand times times and returns the last result.

Method Average response (ms) 5k runs StdDev
func-cs-inproc 90.80 8.22
func-cs-isolated 324.00 46.29
func-cs-nativeaot 343.27 47.27

Caveats: These tests were run with the local functions tools on a Windows laptop under WSL2. Who knows what noise there is in the data or how this would behave on Azure's servers. If your function is most often called cold, you may wish to further explore the potential cold start benefits of Native AOT.

Helpful commands

# Build & run two frameworks 
dotnet build && dotnet run -f net6.0 && dotnet run -f net7.0

# Publish Native AOT
dotnet publish -c Release -r linux-x64

# Start Azure Function 
func start 
func start --port 7071
func start --port 7071 --dotnet-cli-params -- "--configuration Release"

References

About

Azure Functions Performance Exploration

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published