Baseline Performance for AWS Lambda .NET using Top-Level Statements
Tags: aws, dotnet, serverless
Published: 2022-10-12 18:03:35

.NET 6 introduced top-level statements, which simplify the entry point of the application code. Unlike the previous style of Lambda definitions, this project creates an executable instead of an assembly. That means we have to provide our own Lambda host implementation. Fortunately, AWS already provides one in the [Amazon.Lambda.RuntimeSupport package](https://www.nuget.org/packages/Amazon.Lambda.RuntimeSupport/).

## Minimal Top-Level Lambda Function

The [MinimalTopLevel project](https://github.com/LambdaSharp/LambdaSharp.Benchmark/tree/main/Projects/MinimalTopLevel) defined a Lambda function that takes a stream and returns an empty response. It has no business logic and only includes required libraries. There is also no deserialization of a payload. This is the Lambda function using top-level statements with the least amount of overhead.

```csharp
using Amazon.Lambda.Core;
using Amazon.Lambda.RuntimeSupport;
using Amazon.Lambda.Serialization.SystemTextJson;

await LambdaBootstrapBuilder.Create(Handler, new DefaultLambdaJsonSerializer())
    .Build()
    .RunAsync();

Task Handler(Stream request, ILambdaContext context)
    => Task.CompletedTask;
```

## Benchmark Data for .NET 6 on x86-64

Again, we see that the duration of the INIT phase is not impacted until we exceed the 3,008 MB threshold, which also drives up cost.

However, compared to the [Minimal baseline project](https://dev.to/lambdasharp/baseline-performance-for-net-on-aws-lambda-32al), this Lambda function has 20% to 100% longer cold start durations.

| Memory Size | Init    | Cold Used | Total Cold Start | Total Warm Used (100) | Cost (µ$) |
| ----------- | ------- | --------- | ---------------- | --------------------- | --------- |
| 128MB       | 235.420 | 1,484.898 | 1,720.318        | 367.174               | 24.05849  |
| 256MB       | 236.617 | 744.375   | 980.992          | 151.328               | 23.93210  |
| 512MB       | 236.002 | 353.587   | 589.589          | 120.821               | 24.15341  |
| 1024MB      | 238.403 | 163.425   | 401.828          | 115.392               | 24.84696  |
| 1769MB      | 234.304 | 96.894    | 331.198          | 115.843               | 26.32520  |
| 5120MB      | 216.870 | 92.632    | 309.502          | 117.367               | 37.69996  |

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o7ob92avxrik1488atuv.png)

[Fullsize Image](https://raw.githubusercontent.com/LambdaSharp/LambdaSharp.Benchmark/main/Docs/MinimalTopLevel-Net6-x64-NoTC-NoR2R-NoPreJIT-ANY%20(Cold%20Start).png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/twba3faf5e35u2lho0bh.png)

[Fullsize Image](https://raw.githubusercontent.com/LambdaSharp/LambdaSharp.Benchmark/main/Docs/MinimalTopLevel-Net6-x64-NoTC-NoR2R-NoPreJIT-ANY%20(Lifetime%20Cost).png)

## Minimum Cold Start Duration

Fortunately, the situation improves quite a bit once we look at the optimal configuration for minimum cold start duration. Using top-level statements shift some of the overhead from the INIT phase to the first INVOKE phase, but otherwise the total duration is very close to what was measured for the [Minimal baseline project](https://dev.to/lambdasharp/baseline-performance-for-net-on-aws-lambda-32al).

| Architecture | Memory Size | Tiered | Ready2Run | PreJIT | Init    | Cold Used | Total Cold Start |
| ------------ | ----------- | ------ | --------- | ------ | ------- | --------- | ---------------- |
| arm64        | 5120MB      | yes    | yes       | no     | 178.743 | 70.947    | 249.690          |
| x86\_64      | 5120MB      | yes    | no        | no     | 190.382 | 67.500    | 257.882          |
| x86\_64      | 1769MB      | yes    | yes       | no     | 189.152 | 58.625    | 247.777          |
| x86\_64      | 5120MB      | yes    | yes       | no     | 176.368 | 57.039    | 233.407          |

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rywk8cylzbmue6rxrcde.png)

[Fullsize Image](https://raw.githubusercontent.com/LambdaSharp/LambdaSharp.Benchmark/main/Docs/MinimalTopLevel-Net6-ANY-ANY-NoR2R-ANY-ANY%20(Minimal%20Cold%20Start).png)

## Minimum Execution Cost

Again, the ARM64 architecture is the most cost-effective approach. _ReadyToRun_, _Tiered Compilation_, and the _PreJIT_ settings all contribute to reduce cost a bit further for the minimal top-level project. That said, the minimum execution cost is ~8.5% higher when using top-level statements. This increased cost is most likely due to the higher memory configuration, which is required to compensate for the increased overhead of the INIT and first INVOKE phases.

| Architecture | Memory Size | Tiered | Ready2Run | PreJIT | Init    | Cold Used | Total Warm Used (100) | Cost (µ$) |
| ------------ | ---------- | ------ | --------- | ------ | ------- | --------- | --------------------- | --------- |
| arm64        | 1024MB     | no     | yes       | no     | 234.381 | 168.534   | 122.581               | 24.08155  |
| arm64        | 1024MB     | no     | yes       | yes    | 252.593 | 167.558   | 124.272               | 24.09109  |
| arm64        | 1024MB     | yes    | yes       | no     | 202.952 | 124.406   | 152.314               | 23.88962  |
| arm64        | 1024MB     | yes    | yes       | yes    | 218.905 | 118.831   | 152.727               | 23.82079  |

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r6zgk9z0e76vhl2s5ijj.png)

[Fullsize Image](https://raw.githubusercontent.com/LambdaSharp/LambdaSharp.Benchmark/main/Docs/MinimalTopLevel-Net6-ANY-ANY-ANY-ANY-ANY%20(Minimal%20Lifetime%20Cost).png)

## What's Next

It's been fun diving into the fundamentals of AWS Lambda for .NET functions. The post is summarizing my findings and thoughts on future projects that might be interesting to explore.