Performance
CHDSharp is designed for both batch verification throughput (parallel) and low-latency random access (single-threaded, cached). This page documents the knobs and what to expect.
Typical throughput
Measured on a modern desktop with the bundled corpus and typical game images (single-threaded I/O, Release build):
| Scenario | Throughput | Notes |
|---|---|---|
CheckFile(deepCheck: true) |
~200–400 MB/s | 8 parallel workers, bounded memory |
CheckFile(deepCheck: false) |
> 1 GB/s | Header-only |
ChdFile.Read() sequential |
~150–300 MB/s | Single-threaded, last-hunk cache |
ChdFile.ReadHunk() random |
~50–150 MB/s | Per-hunk re-decompression |
Actual numbers depend on the mix of codecs (LZMA is the slowest, raw/zstd the fastest), hunk size, and storage speed.
Tuning parallelism
Chd.TaskCount controls the worker count for CheckFile(deepCheck: true) (default 8, valid range 1–64). It is a process-global setting — set it before calling:
Chd.TaskCount = Environment.ProcessorCount; // or a value you benchmarked
var result = Chd.CheckFile(stream, "game.chd", deepCheck: true);
Memory is bounded even with many workers:
- pooled buffers (
ArrayPool) for compressed input, decompressed output, and caches; - a semaphore capping in-flight repeat-block cache to 512 MiB (
blocksToKeep = (512 MiB) / hunkbytes).
Caching model
Repeat-block cache (verification)
ChdBlockRead detects hunks referenced multiple times (deduplicated SELF entries) and:
- computes a usage weight per block (LZMA/Huffman/CD-LZMA blocks are the most expensive to re-decompress, so they get the highest priority),
- keeps the top-N blocks’ decompressed copies within the 512 MiB budget (
KeepMostRepeatedBlocks), - serves repeated hunks from the cache instead of re-decompressing.
Last-hunk cache (random access)
ChdFile.Read() caches the most recently read hunk, so byte ranges that stay within one hunk cost a single decompression.
Multi-hunk LRU cache (libchdr #36)
ChdFile.ReadHunk() retains decompressed hunks in a configurable LRU cache so random reads that revisit hunks avoid re-decompression. Default size is 1 (equivalent to the single-hunk slot); set CacheSize or call ConfigureCache(n) to keep the last n distinct hunks (n <= 1 disables it). Memory is capped at CacheSize * HunkBytes.
chd.ConfigureCache(16); // keep 16 decompressed hunks
chd.ReadHunk(100, buf); // decompressed
chd.ReadHunk(100, buf); // served from cache
Use a larger CacheSize when a workload performs random/scattered reads that repeatedly touch the same subset of hunks.
Precache (whole file in RAM)
ChdFile.Precache() reads the entire compressed file into memory once. Every subsequent hunk read is served from RAM — no stream seeks, no disk I/O. Ideal for:
- random-access workloads over slow/remote storage,
- many small reads across a large image,
- repeated passes over the same file.
var err = chd.Precache();
if (err != ChdError.Chderrnone) { /* >2 GiB file → out of memory; IO failure */ }
// from here on, ReadHunk/Read serve from RAM
Precache() is idempotent, restores the stream position, and returns Chderroutofmemory for files larger than 2 GiB or when allocation fails.
Reusable codec state
ChdCodecState keeps per-codec scratch alive across hunks:
- LZMA dictionary window (sized to the hunk),
- zstd decompressor instance,
- FLAC decoder + audio buffers,
- Huffman lookup tables (1 MiB each for AVHuff contexts).
This avoids reallocating the most expensive buffers on every hunk — the main reason sequential Read stays in the hundreds of MB/s.
Memory profile
| Operation | Peak memory |
|---|---|
CheckFile(deepCheck: true) |
~workers × hunkbytes × 3 (pooled) + ≤ 512 MiB repeat cache |
ReadHunk / Read |
~2–3 × hunkbytes + codec state |
Precache |
file size + codec state |
ReadAllBytes |
image size (fails with Chderroutofmemory > 2 GiB) |
Practical tips
- Batch-verify in parallel with
CheckFile, notReadAllBytesper file. - Extract with
ExtractToDirectory(streams hunk-by-hunk) rather thanReadAllBytesfor large images. - Random access patterns benefit from
Precachewhen the underlying stream is slow; on local NVMe the difference is usually small. - Hunk size matters: larger hunks amortize per-hunk overhead but waste space on small reads. This is a property of the CHD file itself (set by
chdman), not the library. - For sequential streaming, prefer
EnumerateHunks()(single buffer, no per-call allocation) over repeatedReadcalls.