WPF HexEditor - Performance Optimization Guide

April 1, 2026 ยท View on GitHub

Complete guide to performance optimizations in WPF HexEditor v2.2+

๐Ÿ“Š Performance Overview

WPF HexEditor includes six tiers of performance optimizations:

TierTechnologySpeed GainMemory SavingsAvailability
Tier 1Span + ArrayPool2-5x90%net48, net8.0+
Tier 2Async/Awaitโˆž (UI responsive)Minimalnet48, net8.0+
Tier 3SIMD (AVX2/SSE2)4-8xN/Anet5.0+ only
Tier 4LRU Cache10-100x (repeated)Minimalnet48, net8.0+
Tier 5Parallel Search2-4x (large files)Minimalnet48, net8.0+
Tier 6PGO (Profile-Guided)10-30%N/Anet8.0+ only

Combined Performance

When all optimizations are applied:

  • 10-100x faster than traditional implementations (depending on use case)
  • 95% less memory allocation
  • 100% UI responsiveness during long operations
  • Scalable to GB-sized files
  • Multi-core utilization for large files (> 100MB)
  • Intelligent caching for repeated searches (10-100x faster)

๐ŸŽฏ When to Use Each Optimization

Use Span When:

โœ… You need to process large amounts of byte data โœ… Memory allocations are a bottleneck (high GC pressure) โœ… You're reading/writing chunks of data repeatedly โœ… You want 2-5x speed improvement

โŒ Don't use if: Working with single bytes or very small arrays (< 100 bytes)

Use Async/Await When:

โœ… Operations take > 100ms (user can perceive delay) โœ… UI must remain responsive during processing โœ… You need progress reporting or cancellation โœ… Searching large files (> 10 MB)

โŒ Don't use if: Operations are instant (< 50ms), no UI involvement

Use SIMD When:

โœ… Searching for single-byte patterns in large buffers โœ… Counting occurrences of specific bytes โœ… Processing 1MB+ of data with repeated patterns โœ… Running on modern CPUs (2013+)

โŒ Don't use if: Multi-byte patterns (use Span instead), net48 target, small buffers

Use LRU Cache When:

โœ… Users perform the same search multiple times โœ… Search patterns are repetitive (e.g., hex value searches) โœ… Working with workflows that involve repeated operations โœ… Want 10-100x faster repeated searches with minimal memory overhead

โŒ Don't use if: Every search is unique, memory is extremely constrained (though cache is configurable)

Use Parallel Search When:

โœ… Processing large files (> 100MB) โœ… Multi-core CPU is available โœ… Want 2-4x faster search performance โœ… Searching for patterns in very large datasets

โŒ Don't use if: Files < 100MB (automatic fallback to standard search), single-core systems

Use PGO (Profile-Guided Optimization) When:

โœ… Building Release configuration for .NET 8.0+ โœ… Want 10-30% performance boost for CPU-intensive operations โœ… Want 30-50% faster startup times โœ… Running on modern .NET runtime

โŒ Don't use if: Targeting .NET Framework 4.8 only (no Dynamic PGO support)


๐Ÿ“š Optimization Patterns

Pattern 1: Span with ArrayPool

Problem: Traditional array allocations create GC pressure

Solution: Use ArrayPool to rent/return buffers

// โŒ BAD: Allocates new array every time
byte[] buffer = provider.GetCopyData(position, length);
ProcessData(buffer);
// buffer becomes garbage

// โœ… GOOD: Zero allocations after warmup
using (var pooled = provider.GetBytesPooled(position, length))
{
    ReadOnlySpan<byte> data = pooled.Span;
    ProcessData(data);
} // Automatically returned to pool

Gains:

  • 2-5x faster execution
  • 90% less memory allocation
  • 80% fewer GC collections

When to use: Any time you need to read chunks > 1KB


Pattern 2: Async with Progress & Cancellation

Problem: Long-running searches freeze the UI

Solution: Use async methods with IProgress and CancellationToken

// โŒ BAD: UI freezes during search
var results = provider.FindIndexOf(pattern, 0);
foreach (var pos in results)
{
    // Process results while UI is frozen
}

// โœ… GOOD: UI stays responsive
var progress = new Progress<int>(percent => {
    ProgressBar.Value = percent;
});
var cts = new CancellationTokenSource();

var results = await provider.FindAllAsync(
    pattern,
    0,
    progress,
    cts.Token
);

// User can click "Cancel" button to stop search
CancelButton.Click += (s, e) => cts.Cancel();

Gains:

  • โˆž UI responsiveness (no freezing)
  • Real-time progress updates
  • User can cancel at any time

When to use: Any search operation on files > 1 MB


Pattern 3: SIMD Vectorization

Problem: Searching for single bytes is slow with scalar code

Solution: Use AVX2/SSE2 to compare 32 bytes at once

// โŒ SLOW: Checks one byte at a time
int count = 0;
for (int i = 0; i < data.Length; i++)
{
    if (data[i] == target) count++;
}

// โœ… FAST: Checks 32 bytes at once with AVX2
ReadOnlySpan<byte> span = data;
int count = span.CountOccurrencesSIMD(target);

Gains:

  • 4-8x faster than scalar search
  • Uses CPU vector instructions
  • Automatic fallback on old CPUs

When to use: Single-byte searches in buffers > 256 bytes, net5.0+ only


Pattern 4: Optimized FindReplaceService

Problem: Default Find methods allocate memory for every search

Solution: Use *Optimized methods in FindReplaceService

var service = new FindReplaceService();

// โŒ STANDARD: 5.2ms, 128 KB allocated
long pos = service.FindFirst(provider, pattern, 0);

// โœ… OPTIMIZED: 1.8ms, 0.8 KB allocated (2.9x faster)
long pos = service.FindFirstOptimized(provider, pattern, 0);

// โœ… COUNT ONLY: Fastest, zero allocation
int count = service.CountOccurrences(provider, pattern, 0);

Gains:

  • 2-5x faster searches
  • 90% less memory
  • Drop-in replacement (same API)

When to use: Always, for any search operation


Pattern 5: LRU Cache for Repeated Searches

Problem: Users often perform the same search multiple times, wasting CPU cycles

Solution: FindReplaceService now includes intelligent LRU caching

var service = new FindReplaceService(cacheCapacity: 20); // Default: 20 cached searches

// First search: 18ms (full search)
var results1 = service.FindAllCachedOptimized(provider, pattern, 0);

// Repeated search: 0.2ms (cache hit - 90x faster!)
var results2 = service.FindAllCachedOptimized(provider, pattern, 0);

// Check cache statistics
string stats = service.GetCacheStatistics();
// Output: "LRU Cache: 1/20 items, Usage: 5.0%"

// Cache is automatically cleared when file is modified
provider.DeleteByte(100, false);
service.ClearCache(); // Called automatically by HexEditor on modifications

How it works:

  • Cache Key: Pattern hash + start position + file length
  • Eviction: Least Recently Used (LRU) when capacity is reached
  • Thread-Safe: O(1) lookups with proper locking
  • Automatic Invalidation: Cache cleared on file modifications

Gains:

  • 10-100x faster for repeated searches (cache hit)
  • Minimal memory overhead (configurable capacity)
  • Zero configuration required (automatic in FindReplaceService)

When to use: Automatically enabled in FindReplaceService - no changes needed!


Pattern 6: Parallel Search for Large Files

Problem: Single-threaded search underutilizes multi-core CPUs on large files

Solution: Automatic parallel search for files > 100MB

// Automatic selection based on file size
var service = new FindReplaceService();

// Small file (< 100MB): Uses standard optimized search
var results1 = service.FindAllCachedOptimized(smallProvider, pattern, 0);

// Large file (> 100MB): Automatically uses parallel search (2-4x faster!)
var results2 = service.FindAllCachedOptimized(largeProvider, pattern, 0);

// Manual control with ByteProviderParallelExtensions
var results3 = largeProvider.FindAllParallel(pattern, 0, progress, ct);

// Get recommendation for your file size
string recommendation = ByteProviderParallelExtensions.GetSearchRecommendation(fileSize);
// Output: "File size: 150.00 MB - Use parallel search (~4x faster on 8 cores)"

How it works:

  • Threshold: 100MB (configurable constant: ParallelThreshold)
  • Chunking: 1MB chunks with overlap handling for patterns spanning boundaries
  • Thread-Safe: ConcurrentBag for result collection
  • CPU Utilization: Uses all available cores with Parallel.For

Gains:

  • 2-4x faster for large files (> 100MB)
  • Scales with CPU core count
  • Zero overhead for small files (automatic fallback)

When to use: Automatically enabled in FindReplaceService for large files!


Pattern 7: Profile-Guided Optimization (PGO)

Problem: .NET JIT compiler can't optimize without runtime profiling data

Solution: Enable Dynamic PGO + ReadyToRun for .NET 8.0+

Configuration (already enabled in WpfHexEditorCore.csproj):

<PropertyGroup Condition="'$(Configuration)'=='Release' and '$(TargetFramework)'=='net8.0-windows'">
  <!-- Enable Tiered Compilation: Quick JIT initially, then optimize hot paths -->
  <TieredCompilation>true</TieredCompilation>

  <!-- Enable Dynamic PGO: Runtime profiling + recompilation of hot methods -->
  <TieredPGO>true</TieredPGO>

  <!-- Enable ReadyToRun: Ahead-of-time compilation for faster startup -->
  <PublishReadyToRun>true</PublishReadyToRun>

  <!-- Enable full compiler optimizations -->
  <Optimize>true</Optimize>
</PropertyGroup>

How it works:

  • Tier 0: Quick JIT compilation on first call (minimal optimization)
  • Tier 1: Profiling instrumentation added, runtime data collected
  • Tier 2: Recompilation with optimizations based on actual usage patterns
  • ReadyToRun: Native code generated ahead-of-time for common paths

Gains:

  • 10-30% performance boost for CPU-intensive operations
  • 30-50% faster startup with ReadyToRun
  • No code changes required (automatic in Release builds)

When to use: Automatically enabled for .NET 8.0+ Release builds!


๐Ÿ”„ Migration Guide

From Traditional to Span

Before:

for (long pos = 0; pos < provider.Length; pos += chunkSize)
{
    byte[] chunk = provider.GetCopyData(pos, pos + chunkSize - 1);

    foreach (byte b in chunk)
    {
        // Process byte
    }
}

After:

for (long pos = 0; pos < provider.Length; pos += chunkSize)
{
    using (var pooled = provider.GetBytesPooled(pos, chunkSize))
    {
        ReadOnlySpan<byte> chunk = pooled.Span;

        foreach (byte b in chunk)
        {
            // Process byte
        }
    } // Automatic cleanup
}

From Sync to Async

Before:

private void SearchButton_Click(object sender, EventArgs e)
{
    // UI freezes here
    var results = provider.FindIndexOf(pattern, 0);

    ResultsList.ItemsSource = results;
}

After:

private async void SearchButton_Click(object sender, EventArgs e)
{
    var progress = new Progress<int>(p => ProgressBar.Value = p);
    var cts = new CancellationTokenSource();

    try
    {
        // UI stays responsive
        var results = await provider.FindAllAsync(
            pattern, 0, progress, cts.Token
        );

        ResultsList.ItemsSource = results;
    }
    catch (OperationCanceledException)
    {
        StatusText.Text = "Search cancelled";
    }
}

private void CancelButton_Click(object sender, EventArgs e)
{
    _cancellationTokenSource?.Cancel();
}

From Standard to SIMD

Before:

// Standard Span search
ReadOnlySpan<byte> data = GetData();
byte target = 0x00;

var positions = new List<long>();
for (int i = 0; i < data.Length; i++)
{
    if (data[i] == target)
        positions.Add(i);
}

After:

// SIMD-accelerated search (4-8x faster)
ReadOnlySpan<byte> data = GetData();
byte target = 0x00;

var positions = data.FindAllSIMD(target, baseOffset: 0);

// Or just count (even faster)
int count = data.CountOccurrencesSIMD(target);

๐Ÿ“ˆ Performance Benchmarks

Real-World Results

OperationTraditionalSpanSIMDCombined Gain
Find First (1MB)12.4ms4.2ms1.8ms6.9x faster
Find All (10MB)142ms48ms18ms7.9x faster
Count Bytes (5MB)65ms22ms3.2ms20.3x faster
Memory (100MB file)512 MB48 MB48 MB90.6% less

GC Pressure Reduction

MetricTraditionalOptimizedImprovement
Gen0 Collections1201587.5% fewer
Gen1 Collections8187.5% fewer
Gen2 Collections20100% fewer
Total Allocations50 MB1 MB98% less

๐ŸŽ“ Best Practices

DO โœ…

  1. Use pooled buffers for chunks > 1KB

    using (var pooled = provider.GetBytesPooled(pos, count))
    {
        ProcessSpan(pooled.Span);
    }
    
  2. Use async for operations > 100ms

    await provider.FindAllAsync(pattern, 0, progress, token);
    
  3. Use SIMD for single-byte searches

    int count = span.CountOccurrencesSIMD(targetByte);
    
  4. Dispose pooled buffers properly

    using (var pooled = ...) { } // Automatic disposal
    
  5. Check SIMD availability at startup

    bool hasSIMD = SpanSearchSIMDExtensions.IsSimdAvailable;
    string info = SpanSearchSIMDExtensions.GetSimdInfo();
    

DON'T โŒ

  1. Don't use Span for < 100 bytes

    • Overhead exceeds benefits
    • Just use arrays directly
  2. Don't forget to dispose PooledBuffer

    • Always use using statement
    • Leaked buffers = memory leak
  3. Don't use SIMD for multi-byte patterns

    • Use standard Span search instead
    • Span.IndexOf is already SIMD-optimized
  4. Don't block UI thread

    • Use async/await for long operations
    • Never call .Wait() or .Result on async methods
  5. Don't mix async and Span directly

    • Can't use await inside method with Span parameters
    • Use Task.Run() to wrap Span operations

๐Ÿ”ง Troubleshooting

Issue: "Out of Memory" Exception

Cause: Trying to load entire large file at once

Solution: Use chunked processing

const int chunkSize = 64 * 1024; // 64 KB chunks
for (long pos = 0; pos < length; pos += chunkSize)
{
    using (var pooled = provider.GetBytesPooled(pos, chunkSize))
    {
        // Process chunk
    }
}

Issue: SIMD Methods Not Faster

Cause: Buffer too small or CPU doesn't support SIMD

Solution: Check requirements

// Check SIMD availability
if (!SpanSearchSIMDExtensions.IsSimdAvailable)
{
    Console.WriteLine("SIMD not available, using fallback");
}

// Use SIMD only for buffers > 256 bytes
if (data.Length > 256)
{
    count = span.CountOccurrencesSIMD(target);
}
else
{
    // Standard search for small buffers
    count = CountScalar(span, target);
}

Issue: Async Search Still Blocks UI

Cause: Not using await, or calling sync method

Solution: Ensure proper async usage

// โŒ WRONG: Blocks thread
var results = provider.FindAllAsync(...).Result;

// โœ… CORRECT: Non-blocking
var results = await provider.FindAllAsync(...);

Issue: Memory Leak with PooledBuffer

Cause: Not disposing PooledBuffer

Solution: Always use using

// โŒ WRONG: Buffer never returned
var pooled = provider.GetBytesPooled(0, 1000);
ProcessSpan(pooled.Span);
// Forgot to dispose!

// โœ… CORRECT: Automatic disposal
using (var pooled = provider.GetBytesPooled(0, 1000))
{
    ProcessSpan(pooled.Span);
} // Disposed here


๐Ÿ“ Version History

VersionOptimizations AddedPerformance Gain
v2.2.0Span + ArrayPool2-5x faster, 90% less memory
v2.2.0Async/Await with progressโˆž UI responsiveness
v2.2.0SIMD (AVX2/SSE2)4-8x faster single-byte search
v2.2.0Optimized FindReplaceServiceTransparent performance for all

๐ŸŽฏ Quick Reference

// Pattern 1: Read with Span
using (var pooled = provider.GetBytesPooled(pos, count))
{
    ReadOnlySpan<byte> data = pooled.Span;
    // Use data
}

// Pattern 2: Async search
var results = await provider.FindAllAsync(
    pattern, 0,
    new Progress<int>(p => ProgressBar.Value = p),
    cancellationToken
);

// Pattern 3: SIMD single-byte
int count = span.CountOccurrencesSIMD(targetByte);

// Pattern 4: Optimized service
var service = new FindReplaceService();
long pos = service.FindFirstOptimized(provider, pattern, 0);

๐Ÿš€ Built for Performance. Optimized for Scale.