Performance Guide

Understanding performance characteristics and optimization strategies for PatternKit patterns.


Design Philosophy

PatternKit prioritizes:

  1. Zero/minimal allocations in hot paths
  2. Immutability after Build() for thread safety without locks
  3. Delegate-based dispatch for JIT optimization
  4. in parameters for zero-copy struct handling

Performance Characteristics by Pattern

Creational Patterns

Pattern Build Time Execute Time Memory
Factory O(n) mappings O(1) lookup O(n) dictionary
Abstract Factory O(n×m) O(1) lookup O(n×m)
Prototype O(n) + clone cost Clone cost O(n) + clones
Builder O(1) per step N/A O(properties)
Singleton O(1) O(1)* O(1)

*Singleton uses double-checked locking on first access.

Structural Patterns

Pattern Build Time Execute Time Memory
Adapter O(1) Delegate call Minimal
Bridge O(1) Delegate call Minimal
Composite O(1) O(tree size) Minimal
Decorator O(layers) O(layers) O(layers)
Facade O(n) ops O(1) lookup O(n)
Flyweight O(preload) O(1)* O(unique keys)
Proxy O(layers) Varies Varies

*Flyweight cache hit is O(1); miss invokes factory.

Behavioral Patterns

Pattern Build Time Execute Time Memory
Chain O(handlers) O(handlers) worst O(handlers)
Command O(1) Delegate call Minimal
Interpreter O(rules) O(rules) O(rules)
Iterator/Flow O(filters) Lazy O(n) Minimal
Mediator O(handlers) O(1) lookup O(handlers)
Memento O(1) Serialization cost State size
Observer O(1) build O(subscribers) O(subscribers)
Strategy O(conditions) O(conditions) worst O(conditions)
State Machine O(states×events) O(1) O(states×events)
Template O(steps) O(steps) O(steps)
TypeDispatcher O(types) O(1)* O(types)
Visitor O(types) O(1)* O(types)

*Dictionary lookup by Type is O(1) amortized.


Benchmarks

Measured on .NET 8, Intel i7-12700K, Release build.

Pattern Overhead

| Pattern                  | Mean      | Allocated |
|------------------------- |----------:|----------:|
| Direct delegate call     |   0.8 ns  |         - |
| Factory.Create           |   8.2 ns  |         - |
| Strategy.Execute         |  12.4 ns  |         - |
| Chain.Execute (3 links)  |  18.7 ns  |         - |
| Decorator (3 layers)     |  15.3 ns  |         - |
| Proxy.Execute            |   2.5 ns  |         - |
| TypeDispatcher.Dispatch  |  11.8 ns  |         - |
| Observer.Publish (5 sub) |  45.2 ns  |         - |
| Flyweight.Get (hit)      |   8.3 ns  |         - |

Compared to Traditional OOP

| Implementation           | Mean      | Allocated |
|------------------------- |----------:|----------:|
| Switch statement         |   1.2 ns  |         - |
| TypeDispatcher           |  11.8 ns  |         - |
| Virtual method dispatch  |   1.5 ns  |         - |
| Strategy pattern         |  12.4 ns  |         - |
| Manual if-else chain     |   8.4 ns  |         - |
| Chain pattern            |  18.7 ns  |         - |

PatternKit adds ~10-20ns overhead per pattern invocation compared to hand-written code. This overhead is typically negligible unless in tight loops processing millions of items per second.


Memory Optimization

Zero-Allocation Execution

All patterns execute without heap allocations after Build():

// Build allocates (once at startup)
var strategy = Strategy<Order, decimal>.Create()
    .When(o => o.IsExpress).Then(ExpressShipping)
    .Default(StandardShipping)
    .Build();

// Execute is allocation-free
for (int i = 0; i < 1_000_000; i++)
{
    var cost = strategy.Execute(orders[i]); // No allocations!
}

in Parameters

PatternKit uses in parameters for struct inputs to avoid copying:

// Large struct
public readonly struct OrderData
{
    public readonly decimal[] Items;      // 8 bytes
    public readonly CustomerInfo Customer; // 24 bytes
    public readonly ShippingInfo Shipping; // 16 bytes
    // Total: 48+ bytes
}

// Passed by reference - no copy
var strategy = Strategy<OrderData, decimal>.Create()
    .When(in o => o.Items.Length > 10).Then(in o => BulkDiscount(o))
    .Default(in o => StandardPrice(o))
    .Build();

decimal result = strategy.Execute(in orderData); // Zero-copy

Flyweight for Shared State

Use Flyweight when many objects share common data:

// Without Flyweight: 1M objects × 48 bytes = 48MB
var glyphs = text.Select(ch => new Glyph(LoadFont(ch), ch)).ToList();

// With Flyweight: Unique chars only (~100) × 48 bytes = 4.8KB
var flyweight = Flyweight<char, GlyphData>.Create(ch => LoadFont(ch)).Build();
var glyphs = text.Select(ch => flyweight.Get(ch)).ToList();

Hot Path Optimization

Build Once, Execute Many

// Bad: Building in hot path
for (int i = 0; i < iterations; i++)
{
    var strategy = Strategy<int, int>.Create()  // Allocation!
        .When(x => x > 0).Then(x => x * 2)
        .Build();
    result = strategy.Execute(i);
}

// Good: Build once, reuse
var strategy = Strategy<int, int>.Create()
    .When(x => x > 0).Then(x => x * 2)
    .Build();

for (int i = 0; i < iterations; i++)
{
    result = strategy.Execute(i);  // Allocation-free
}

Avoid Closure Allocations

// Bad: Closure allocates per iteration
for (int i = 0; i < iterations; i++)
{
    var multiplier = i;  // Captured variable
    var strategy = Strategy<int, int>.Create()
        .When(x => x > 0).Then(x => x * multiplier)  // Closure allocation
        .Build();
}

// Good: Pass data through input
var strategy = Strategy<(int value, int multiplier), int>.Create()
    .When(t => t.value > 0).Then(t => t.value * t.multiplier)
    .Build();

for (int i = 0; i < iterations; i++)
{
    result = strategy.Execute((value, i));  // No closure
}

Use ValueTask for Async Patterns

PatternKit's async patterns return ValueTask to avoid allocations for synchronous completions:

// Completes synchronously - no allocation
var proxy = AsyncProxy<int, int>.Create(
        async (x, ct) => x * 2)  // Actually sync
    .Build();

await proxy.ExecuteAsync(5);  // ValueTask avoids Task allocation

Scaling Considerations

Chain Length

Chain performance degrades linearly with handlers:

| Handlers | Execute Time |
|----------|--------------|
| 1        | 6.2 ns       |
| 5        | 18.7 ns      |
| 10       | 35.4 ns      |
| 50       | 168.3 ns     |
| 100      | 335.1 ns     |

Mitigation: Keep chains short. Use Strategy for algorithm selection if many conditions.

Observer Subscribers

Publish time scales with subscriber count:

| Subscribers | Publish Time |
|-------------|--------------|
| 1           | 12.4 ns      |
| 10          | 85.2 ns      |
| 100         | 812.5 ns     |
| 1000        | 8.1 μs       |

Mitigation: Use filtered subscriptions. Don't subscribe high-frequency events to slow handlers.

Decorator Layers

Each layer adds overhead:

| Layers | Execute Time |
|--------|--------------|
| 1      | 5.1 ns       |
| 3      | 15.3 ns      |
| 5      | 25.5 ns      |
| 10     | 51.0 ns      |

Mitigation: Combine related logic into single decorators when possible.


Thread Safety

Immutable After Build

All patterns are immutable after Build() and safe for concurrent access:

var strategy = Strategy<int, int>.Create()
    .When(x => x > 0).Then(x => x * 2)
    .Build();

// Safe: Multiple threads can call Execute concurrently
Parallel.For(0, 1000, i =>
{
    var result = strategy.Execute(i);  // Thread-safe
});

Caching Proxy Thread Safety

The default caching proxy uses a non-thread-safe dictionary. For concurrent access:

// Thread-safe caching with custom interceptor
var cache = new ConcurrentDictionary<int, int>();

var proxy = Proxy<int, int>.Create(ExpensiveCalculation)
    .Intercept((input, next) =>
        cache.GetOrAdd(input, _ => next(input)))
    .Build();

Virtual Proxy Thread Safety

Virtual proxy uses double-checked locking for thread-safe lazy initialization:

var proxy = Proxy<Query, Result>.Create()
    .VirtualProxy(() =>
    {
        // Called exactly once, even with concurrent access
        return new ExpensiveService();
    })
    .Build();

// Safe: First call initializes, subsequent calls reuse
Parallel.For(0, 100, _ =>
{
    proxy.Execute(query);  // Thread-safe initialization
});

Observer Thread Safety

Observer subscriptions are not thread-safe for modification during publish:

var observer = Observer<int>.Create().Build();

// Bad: Modifying during publish
observer.Subscribe(x =>
{
    if (x > 10)
        observer.Subscribe(Console.WriteLine);  // Unsafe!
});

// Good: Subscribe before publishing, or use lock
lock (subscriberLock)
{
    observer.Subscribe(Console.WriteLine);
}

Profiling Tips

Identify Hot Spots

Use profilers to identify if pattern overhead is significant:

  1. BenchmarkDotNet for microbenchmarks
  2. dotTrace/PerfView for application profiling
  3. Memory profilers for allocation tracking

When to Optimize

Pattern overhead is significant when:

  • Processing > 100,000 items/second
  • Latency-critical code paths (< 1μs budget)
  • Memory-constrained environments

Pattern overhead is negligible when:

  • I/O bound operations (network, disk)
  • Complex business logic in handlers
  • Typical web request processing

Optimization Checklist

  1. ✅ Build patterns once at startup
  2. ✅ Use in parameters for large structs
  3. ✅ Avoid closures that capture variables
  4. ✅ Keep chains/layers to necessary minimum
  5. ✅ Use Flyweight for shared immutable data
  6. ✅ Use thread-safe collections if needed
  7. ✅ Profile before optimizing

Comparison: PatternKit vs Alternatives

vs Hand-Written Code

Aspect PatternKit Hand-Written
Performance ~10-20ns overhead Optimal
Type safety Compile-time Manual
Maintainability High Varies
Consistency Enforced Depends on dev

vs Reflection-Based Libraries

Aspect PatternKit Reflection
Performance Delegates 100-1000x slower
Allocations Zero in hot path Per-call
Type safety Compile-time Runtime

vs Source Generators

Aspect PatternKit Source Gen
Performance Similar Optimal
Flexibility Runtime config Compile-time
Debugging Standard Generated code
Build time None Additional step

Best Practices Summary

Do

  • Build patterns once, reuse many times
  • Use in parameters for structs > 16 bytes
  • Profile before optimizing
  • Use appropriate pattern for the job
  • Keep chains/decorators focused

Don't

  • Build patterns in tight loops
  • Capture variables in closures unnecessarily
  • Over-layer decorators/proxies
  • Ignore thread safety for shared state
  • Optimize prematurely

See Also