Performance tricks I learned from contributing to the Azure .NET SDK

Introduction

Focus on performance optimization in .NET Code and not architecture.

Esoteric

Being called out for premature optimizations.

At Scale implementation details matter

“Scale for an application can mean the number of users that will concurrently connect to the application at any given time, the amount of input to process or the number of times data needs to be processed.

For us, as engineers, it means we have to know what to ignore and knowing what to pay close attention to.” David Fowler

  • Avoid excessive allocations to reduce the GC overhead
  • Avoid unnecessary copying of memory

Avoid excessive allocations to reduce the GC overhead

Think at least twice before using LINQ or unnecessary enumeration on the hot path

Avoid LINQ on the hot path.
Avoid LINQ on the hot path.
Avoid LINQ on the hot path.
Avoid LINQ on the hot path.

Benchmarking Time!

We can only know the before and after when we measure it.

~20-40%
~20-40%

LINQ to collection-based operations

  • Use Array.Empty<T> to represent empty arrays
  • Use Enumerable.Empty<T> to represent empty enumerables
  • Use CSharp12 collection expressions
  • Prevent collections from growing
  • Use concrete collection types
  • Leverage pattern matching or Enumerable.TryGetNonEnumeratedCount
  • Wait with instantiating collections until really needed
  • There be dragons
  • Keep yourself up to date with latest .NET performance improvements
Avoid LINQ on the hot path.
Avoid LINQ on the hot path.

Benchmarking Time!

We can only know the before and after when we measure it.

~5-64%
~23-61%

+56%
~23-61%

Avoid excessive allocations to reduce the GC overhead

Be aware of closure allocations

Remove closure allocations.
Remove closure allocations.
Remove closure allocations.
Remove closure allocations.
Remove closure allocations.
Remove closure allocations.
Remove closure allocations.
Remove closure allocations.
Remove closure allocations.
Remove closure allocations.

Benchmarking Time!

We can only know the before and after when we measure it.

~71%
~Gone!

How to detect those allocations?

~74-78%
~Gone!

go.particular.net/ndc-porto-2023-pipeline

Avoid excessive allocations to reduce the GC overhead

Pool and re-use buffers (and larger objects)

Pool and re-use buffers.
Pool and re-use buffers.

Benchmarking Time!

We can only know the before and after when we measure it.

+226%
~Gone!

Avoid excessive allocations to reduce the GC overhead

For smaller local buffers, consider using the stack

Small local buffers on stack.

Benchmarking Time!

We can only know the before and after when we measure it.

~45%
~Gone!

Avoid excessive allocations to reduce the GC overhead

Parameter overloads and boxing

Task.WhenAny
Avoid parameter overloads and boxing.
Task.WhenAny
Avoid parameter overloads and boxing.
CancellationTokenSource
Avoid parameter overloads and boxing.
CancellationTokenSource
Avoid parameter overloads and boxing.
  • Avoid excessive allocations to reduce the GC overhead
    • Think at least twice before using LINQ or unnecessary enumeration on the hot path
    • Be aware of closure allocations
    • Pool and re-use buffers
    • For smaller local buffers, consider using the stack
    • Be aware of parameter overloads
    • Where possible and feasible use value types but pay attention to unnecessary boxing
    • Move allocations away from the hot-path where possible

Avoid unnecessary copying of memory

Avoid unnecessary copying of memory

Watch out for immutable/readonly data that is copied

Immutable/readonly data should not be copied.
Immutable/readonly data should not be copied.

Avoid unnecessary copying of memory

  • Look for Stream and Byte-Array usages that are copied or manipulated without using Span or Memory
  • Replace existing data manipulation methods with newer Span or Memory based variants
Avoid unnecessary copying of memory.
Avoid unnecessary copying of memory.

Benchmarking Time!

We can only know the before and after when we measure it.

~38-47%
~Gone!

  • Look for Stream and Byte-Array usages that are copied or manipulated without using Span or Memory
  • Replace existing data manipulation methods with newer Span or Memory based variants
  • Watch out for immutable/readonly data that is copied
  • Avoid excessive allocations to reduce the GC overhead
    • Be aware of closure allocations
    • Think at least twice before using LINQ
      or unnecessary enumeration on the hot path
      • Use Array.Empty<T> to represent empty arrays
      • Use Enumerable.Empty<T> to represent empty enumerables
      • Use CSharp12 collection expressions
      • Prevent collections from growing
      • Use concrete collection types
      • Leverage pattern matching or Enumerable.TryGetNonEnumeratedCount
      • Wait with instantiating collections until really needed
      • There be dragons
      • Keep yourself up to date with latest .NET performance improvements
    • Pool and re-use buffers
    • For smaller local buffers, consider using the stack
    • Be aware of parameter overloads
    • Where possible and feasible use value types but pay attention to unnecessary boxing
    • Move allocations away from the hot-path where possible
  • Avoid unnecessary copying of memory
    • Watch out for immutable/readonly data that is copied
    • Look for Stream and Byte-Array usages that are copied or manipulated without using Span or Memory
    • Replace existing data manipulation methods with newer Span or Memory based variants

In case you are up for a challenge

The original ComputeHash method had a .
Did you spot it?

static void ComputeHash(byte[] data, uint seed1, uint seed2,
	out uint hash1, out uint hash2)	{

   uint a, b, c;

   a = b = c = (uint)(0xdeadbeef + data.Length + seed1);
   c += seed2;

   int index = 0, size = data.Length;
   while (size > 12) {
      a += BitConverter.ToUInt32(data, index);
      b += BitConverter.ToUInt32(data, index + 4);
      c += BitConverter.ToUInt32(data, index + 8);

      // rest omitted
   }
At Scale implementation details matter

Tweak expensive I/O operations first.
Pay close attention to the context of the code.
Apply the principles where they matter.
Everywhere else, favor readability.

github.com/danielmarbach/PerformanceTricksAzureSDK

Happy coding!