Performance Guide
Understanding performance characteristics and optimization strategies for PatternKit patterns.
Design Philosophy
PatternKit prioritizes:
- Zero/minimal allocations in hot paths
- Immutability after Build() for thread safety without locks
- Delegate-based dispatch for JIT optimization
inparameters 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:
- BenchmarkDotNet for microbenchmarks
- dotTrace/PerfView for application profiling
- 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
- ✅ Build patterns once at startup
- ✅ Use
inparameters for large structs - ✅ Avoid closures that capture variables
- ✅ Keep chains/layers to necessary minimum
- ✅ Use Flyweight for shared immutable data
- ✅ Use thread-safe collections if needed
- ✅ 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
inparameters 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