Skip to content

v1.0.4 — parallel merge (~1.54×) + robustness pass

Choose a tag to compare

@ysemenenko ysemenenko released this 27 Jun 06:33

ExternalSorting.Core v1.0.4

Parallel merge — the headline

The multi-pass merge used to be fully single-threaded; only chunk creation
ran in parallel, so Amdahl's law capped the end-to-end speedup at ~1.20×
no matter how many cores were free. Within a pass the batches are independent
(each reads its own group of chunk files and writes one merged output), so
they now merge concurrently.

Measured on a Ryzen 9800X3D (same box, same config — only the merge
implementation differs, so the delta is the code, not the hardware):

Parallelism Serial merge Parallel merge Speedup
1 23.72 ms 23.70 ms 1.00×
2 21.12 ms 17.93 ms 1.32×
4 19.82 ms 15.75 ms 1.50×
8 20.20 ms 15.41 ms 1.54×
16 20.90 ms 15.73 ms 1.51×

End-to-end scaling 1.20× → 1.54×, sweet spot moved P=4 → P=4–8,
~−24% wall-clock at P=8. The serial path (P=1) is unchanged and output is
byte-identical. Peak merge RAM now scales with DegreeOfParallelism.

Robustness pass

  • Option validation in the constructor: non-null serializer/comparer,
    MergeWayCount >= 2 (a 1-way merge never converged → was an infinite loop),
    positive memory/buffer, DegreeOfParallelism >= 1, non-empty temp dir,
    positive EstimatedItemSize; Sort() null-guards its streams.
  • Leak-safe streams: input readers use leaveOpen: true and are disposed;
    merge readers and replacement-selection writers are released in finally on
    fault; a reader-thread fault in the parallel pipeline cancels and drains the
    workers before propagating (no worker writes into a temp dir being deleted),
    and a worker fault is no longer masked by the reader's induced cancellation.
  • Int64 count headers everywhere (chunk files + final output) so sorts
    beyond int.MaxValue items don't corrupt counts.
  • Real metrics: SortMetrics.BytesRead / BytesWritten are now populated
    (cheap Position delta for seekable streams, counting-stream fallback
    otherwise — no hot-path cost in the common case).
  • Docs: stream-ownership + unstable-sort semantics on IExternalSorter,
    binary record format on RecordSerializer.

Tests

Suite 51 → 71: option/null validation, fault cleanup across all three
chunk strategies, a cross-platform proof that the parallel fault path drains
blocked workers, Int64 header, truncated-input EOF handling, leaveOpen,
metrics, and a deep multi-pass parallel-merge byte-identical check.

Compatibility

The on-disk chunk format and the final output header are now Int64 (8-byte)
counts. If you previously persisted sorter output and parse the count header
yourself, read it as Int64.