Skip to content

Performance

Martin Zahálka edited this page Feb 12, 2026 · 1 revision

Performance

ZMapper is designed for near-zero overhead mapping. All mapping code is generated at compile time, resulting in performance that matches or beats hand-written mapping code.

Benchmark Results

All benchmarks were run using BenchmarkDotNet and compare ZMapper against:

  • Manual Mapping — hand-written property assignments (baseline)
  • Mapperly — another source-generation mapper
  • AutoMapper — reflection-based mapper (industry standard)

Simple Object Mapping (Single)

Method Mean Ratio Allocated
ZMapper 16.56 ns 0.93x 88 B
Manual Mapping 17.79 ns 1.00x 88 B
Mapperly 17.83 ns 1.00x 88 B
AutoMapper 52.64 ns 2.96x 88 B

ZMapper matches manual mapping speed and is 3x faster than AutoMapper.

Simple Batch Mapping (1,000 objects)

Method Mean Ratio
Manual Loop 18.79 us 1.00x
ZMapper (Span) 19.38 us 1.03x
Mapperly Loop 23.50 us 1.25x
AutoMapper Loop 54.38 us 2.89x

Span-based batch mapping is nearly identical to hand-written loops.

Complex Object Mapping (Nested objects, collections, enums)

Method Mean Ratio
Manual (Order) 168.35 ns 1.00x
ZMapper (Order) 172.59 ns 1.03x
Mapperly (Order) 214.79 ns 1.28x
AutoMapper (Order) 351.79 ns 2.09x

Even with deep object graphs (Order -> OrderItems[] -> OrderStatusInfo), ZMapper stays within 3% of manual mapping.

Complex Batch Mapping (1,000 orders)

Method Mean Ratio
ZMapper (Span) 122.75 us 0.86x
Manual Loop 142.93 us 1.00x
Mapperly Loop 167.91 us 1.18x
AutoMapper Loop 237.94 us 1.67x

ZMapper's Span-based batch mapping is faster than manual mapping for complex objects.

Why Is ZMapper Fast?

1. Compile-Time Code Generation

ZMapper's Roslyn source generator analyzes your mapping configuration at build time and generates optimized C# code. At runtime, there is:

  • No reflection
  • No dictionary lookups
  • No dynamic dispatch
  • No expression tree compilation

2. Direct Property Access

The generated code uses direct property assignments:

// What gets generated (simplified)
destination.UserName = source.Username;
destination.EmailAddress = source.Email;
destination.Active = source.IsActive;

This compiles to the same IL as hand-written code.

3. ReadOnlySpan<T> Batch Operations

Span-based iteration avoids:

  • Heap allocation for the enumerator object
  • Interface dispatch overhead (IEnumerator.MoveNext())
  • Bounds checking (the JIT can optimize Span iteration)
// Generated batch mapping (simplified)
public Person[] MapArray(ReadOnlySpan<PersonDto> source)
{
    var result = new Person[source.Length];
    for (int i = 0; i < source.Length; i++)
    {
        result[i] = Map(source[i]);
    }
    return result;
}

4. AggressiveInlining

Generated extension methods are marked with [MethodImpl(MethodImplOptions.AggressiveInlining)], instructing the JIT compiler to inline the method body at call sites:

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static User ToUser(this UserDto source) { ... }

5. No Boxing

Value types (int, DateTime, enums, etc.) are never boxed. The generated code uses concrete types throughout, keeping value types on the stack.

Running Benchmarks

Run the benchmark suite yourself:

cd tests/ZMapper.Benchmarks
dotnet run -c Release

Filter specific benchmarks:

# Only complex object benchmarks
dotnet run -c Release -- --filter *Complex*

# Only batch/collection benchmarks
dotnet run -c Release -- --filter *Collection*

# Only simple mapping benchmarks
dotnet run -c Release -- --filter *Simple*

Performance Tips

  1. Use MapArray with Span for batch operations — it's the fastest path
  2. Use extension methods (.ToUser()) for single objects — they're JIT-inlined
  3. Register mappings from leaf to root for nested objects
  4. Avoid unnecessary hooks (BeforeMap/AfterMap) when simple ForMember suffices — hooks add a delegate invocation
  5. Use IgnoreNonExisting() instead of many individual .Ignore() calls when most properties don't match

Next Steps

Clone this wiki locally