Performance Factors

Before getting down to the nitty-gritty, let’s define the concept of performance in Lambda functions.

Execution time

When it comes to performance, execution time is likely to be the first metric that comes to mind.

User experience aside, optimizing execution time is essential because of Lambda’s pay-per-use pricing model. Getting execution time right is essential.

On Lambda, it’s expressed in milliseconds and billed per 100ms.

Memory Footprint

There are two different considerations with Lambda — memory allocation and memory footprint.

The former massively relies on the use case —whether you’re running some Map/Reduce jobs (big data) or a simple CRUD service, your requirements won’t be the same.

It also depends on which language you use — for a given job, Go and Kotlin won’t require the same amount of memory.

In theory, you will have to pay approximately 24 times more per 100ms if you are allocating 3GB (max) of memory instead of 128MB (min).

There’s the catch — the more you allocate to your function, the stronger the vCPU will be, and the more you will pay. The flip side to this is that your function will also run faster, hence potentially costing less than expected.

Slide from Amazon comparing memory allocation settings for a same computation.

As you can see on the slide above, higher memory allocation doesn’t automatically mean a higher cost.

Finding the optimal setting for a given job is almost an art.

Thanks to Alex Casalboni, there is a solution to this problem:

The memory footprint is the amount of memory that your function will actually use (regardless of the defined allocation). This is really important, particularly if your function is (most likely) sitting in a VPC.

Go: Reference vs. Value

How you use the language is as important as what language you’re using.

Much like C and C++, Go allows you to manipulate pointers instead of values. A pointer is a reference to the memory address of a specific value.

When calling a function, you can either pass by value or by reference.

By value:

func ChangeName(t Tourist) {

t.Name = "Jane Doe"

} tourist := Tourist{Name: "John Doe"}

fmt.Println("Name is ", tourist.Name) // prints "John Doe"

ChangeName(tourist)

fmt.Println("Name is ", tourist.Name) // still prints "John Doe"

When we pass the value of tourist to the function, Go basically copies the value somewhere else in memory, so the name will only be changed within the ChangeName function.

By reference:

func ChangeName(t *Tourist) {

t.Name = "Jane Doe"

} tourist := Tourist{Name: "John Doe"}

fmt.Println("Name is ", tourist.Name) // prints "John Doe"

ChangeName(&tourist)

fmt.Println("Name is ", tourist.Name) // now prints "Jane Doe"

In the snippet above, the value is not copied. Whatever is changed in the ChangeName function will have an impact on the original value.

If you are used to C or C++, this is not new to you.

What does that mean for us?

Well, as you can imagine, copying values all over the place can end up being pretty expensive in terms of memory consumption.

However, it’s not always worth passing the reference as there is a bit more complexity to it — Go’s compiler doesn’t exactly work like C’s.

I try to keep my articles as simple as possible, but if you like technical details, I highly recommend this article about escape analysis with the Go compiler.

There are a couple of rules for this:

If the variable shouldn’t be modified : pass the value

: pass the If the variable is a large Struct (e.g. API Gateway request): pass the pointer

Benchmark

Now, for the interesting part.

The tests were simple — adding a tourist in the database with both functions in the following situations:

With the tests above, we are covering both cold and warm invocations, with and without concurrency.

Tool

I’ve used hey for the load testing.

Specs

Database

As I already mentioned, the database used is PostgreSQL on Amazon RDS.

A new database has been set up specially for the benchmark and is shared across both functions.

The chosen instance is db.t2.micro (1 vCPU and 1Gb of memory).

Results