Skip to content

Make ExceptWith linear on InsertionOrderSet, OrderedSet and ContiguousSet [patch] - #144

Open
matt-edmondson wants to merge 1 commit into
mainfrom
fix/108-except-with-linear
Open

matt-edmondson wants to merge 1 commit into
mainfrom
fix/108-except-with-linear

Conversation

@matt-edmondson

@matt-edmondson matt-edmondson commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #108

What was wrong

ExceptWith removed elements one at a time from list- or array-backed storage, and each removal shifted the tail. Excepting m elements from an n-element set therefore cost O(n·m). IntersectWith and SymmetricExceptWith had already been changed to compact in one pass, which closed the other rows of the issue's table, but ExceptWith was still per-element on all three types.

Change

Each ExceptWith builds one lookup from other, using the set's own comparer, and then compacts the storage in a single pass:

Type Lookup Compaction
InsertionOrderSet<T> HashSet<T> items.RemoveAll, which also drops each removed item from the uniqueness index
ContiguousSet<T> HashSet<T> the existing RemoveWhere, which clears vacated slots and bumps version only when something was removed
OrderedSet<T> ToComparerSet, which reuses other when it is an OrderedSet with the same comparer items.RemoveAll

The surviving elements keep their order. ExceptWith(self) keeps its early Clear(). An ExceptWith that removes nothing still leaves the version unchanged, so it doesn't invalidate a running enumeration.

Tests

  • One large-set timing test per type, following the existing *_RunsInLinearTime pattern. Each also checks that the survivors come out in order.
  • I checked each test against the old code by reverting the library change:
Test Without the fix With the fix
InsertionOrderSetTests.ExceptWith_LargeSetRemovingEvens_… (200k) fails, ~7 s passes
ContiguousSetTests.ExceptWith_LargeSetRemovingEvens_… (100k) fails, ~25 s passes
OrderedSetTests.ExceptWith_LargeEvens_… (800k) fails, ~20 s passes, ~0.1 s

OrderedSet needs the larger size because a List.RemoveAt shift is a fast memmove: at 400k the old code still finished in 3.8 s.

  • Full Containers.Test suite: 519 passed, 0 failed.
  • Adds a SetExceptWithBenchmarks class (10k and 80k elements, HashSet<T> as the baseline), which the acceptance criteria asked for. I built it but didn't run it.

🤖 Generated with Claude Code

https://claude.ai/code/session_017vfJW6AfToAg6F8bcZwwZ5

…sSet [patch]

ExceptWith removed one element at a time, and each removal shifted the
list or array tail, so excepting m elements from an n-element set was
O(n·m): seconds at 80k elements. It now builds one lookup from `other`
with the set's own comparer and compacts the storage in a single pass,
as IntersectWith and SymmetricExceptWith already do. Survivors keep
their order, and ExceptWith(self) still empties the set.

Adds a large-set timing test per type and an ExceptWith benchmark.

Fixes #108

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017vfJW6AfToAg6F8bcZwwZ5
@sonarqubecloud

sonarqubecloud Bot commented Nov 4, 2026

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants