Skip to content

Speed up IsSubset for a long and a short list - #6793

Open
fingolfin wants to merge 2 commits into
masterfrom
mh-claude/issubset-galloping
Open

fingolfin wants to merge 2 commits into
masterfrom
mh-claude/issubset-galloping

Conversation

@fingolfin

Copy link
Copy Markdown
Member

The kernel function behind IsSubsetSet, and so behind IsSubset for two internal lists, merged the two sets, scanning the first one up to the last element of the second. It now finds each element of the second set in the first by galloping and binary search, with O(len2 * log(len1 / len2)) comparisons; equal sets still cost one comparison per element. Sorted lists that are not plain, such as ranges, are no longer copied: for [-2^50 .. 2^50] the copy ran out of memory.

Timings with builds from the default configure options.

Setup:

gap> L1:= List( [ 1 .. 10^5 ], i -> 2*i );;
gap> L2:= [ 199996, 200000 ];;
gap> L3:= ShallowCopy( L1 );;

Before:

gap> for i in [ 1 .. 10^4 ] do IsSubset( L1, L2 ); od; time;
990
gap> IsSubset( [ 1 .. 10^7 ], [ 5 ] ); time;
true
121
gap> for i in [ 1 .. 100 ] do IsSubset( L1, L3 ); od; time;
7

After:

gap> for i in [ 1 .. 10^4 ] do IsSubset( L1, L2 ); od; time;
1
gap> IsSubset( [ 1 .. 10^7 ], [ 5 ] ); time;
true
0
gap> for i in [ 1 .. 100 ] do IsSubset( L1, L3 ); od; time;
8

Fixes #771

Assisted-by: Generative AI

The kernel function behind `IsSubsetSet`, and so behind `IsSubset` for
two internal lists, merged the two sets, scanning the first one up to
the last element of the second. It now finds each element of the second
set in the first by galloping and binary search, with
O(len2 * log(len1 / len2)) comparisons; equal sets still cost one
comparison per element.

Ranges were copied into plain lists first; for `[-2^50 .. 2^50]` this
ran out of memory. Membership in a range is now decided arithmetically,
and whether a range lies in another one in constant time.

Timings with builds from the default configure options.

Setup:

    gap> L1:= List( [ 1 .. 10^5 ], i -> 2*i );;
    gap> L2:= [ 199996, 200000 ];;
    gap> L3:= ShallowCopy( L1 );;
    gap> R:= [ 2, 4 .. 2*10^5 ];;

Before:

    gap> for i in [ 1 .. 10^4 ] do IsSubset( L1, L2 ); od; time;
    990
    gap> IsSubset( [ 1 .. 10^7 ], [ 5 ] ); time;
    true
    120
    gap> for i in [ 1 .. 100 ] do IsSubset( R, L3 ); od; time;
    99
    gap> for i in [ 1 .. 1000 ] do IsSubset( L1, L3 ); od; time;
    66

After:

    gap> for i in [ 1 .. 10^4 ] do IsSubset( L1, L2 ); od; time;
    2
    gap> IsSubset( [ 1 .. 10^7 ], [ 5 ] ); time;
    true
    0
    gap> for i in [ 1 .. 100 ] do IsSubset( R, L3 ); od; time;
    27
    gap> for i in [ 1 .. 1000 ] do IsSubset( L1, L3 ); od; time;
    62

Fixes #771

Assisted-by: Generative AI
@fingolfin
fingolfin requested a review from Stefan-Kohl October 26, 2026 10:51
@fingolfin fingolfin added kind: enhancement Label for issues suggesting enhancements; and for pull requests implementing enhancements topic: performance bugs or enhancements related to performance (improvements or regressions) topic: library release notes: use title For PRs: the title of this PR is suitable for direct use in the release notes labels Oct 26, 2026
@codecov

codecov Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 79.77%. Comparing base (a778edf) to head (1036989).
⚠️ Report is 2 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #6793      +/-   ##
==========================================
+ Coverage   79.72%   79.77%   +0.05%     
==========================================
  Files         684      684              
  Lines      295592   295632      +40     
  Branches     8660     8672      +12     
==========================================
+ Hits       235650   235842     +192     
+ Misses      58126    57975     -151     
+ Partials     1816     1815       -1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@james-d-mitchell james-d-mitchell left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me, subject to the included comments.

Comment thread src/set.c Outdated
Comment on lines +251 to +252
** that is not smaller: probe i1, i1+1, i1+3, i1+7, ... until one is, then
** search between the last two probes. This needs O(len2 * log(len1/len2))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
** that is not smaller: probe i1, i1+1, i1+3, i1+7, ... until one is, then
** search between the last two probes. This needs O(len2 * log(len1/len2))
** that is not smaller: probe i1, i1+1, i1+3, i1+7, ... until <set1> at that position is not smaller than the given element of <set2>, then
** perform a binary search between the last two probed entries of <set1>. This needs O(len2 * log(len1/len2))

I had some trouble parsing this at first, some minor edits (that mess up the line length, sorry!)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might also be good to mention "galloping" here for the (like me) uninitiated.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reworded and named the technique (galloping) in the header.

Comment thread src/set.c
** comparisons, and one per entry for equal sets.
*/
static BOOL IsSubsetPlainSet(Obj set1, Obj set2)
{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add some assertions here that set1 and set2 are indeed plain sets.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added GAP_ASSERT(IsPlainSet(...)) for both arguments.

Comment thread src/set.c Outdated
if (!LT(e1, e2))
return FALSE;

// invariant: set1[lo] < e2, and set1[hi] >= e2 or hi = len1 + 1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this comment really accurate? When lo, step, and hi are defined in the next 3 lines, this "invariant" doesn't necessary hold. Wouldn't it be more accurate to say "Find values of lo and hi such that ..."?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, it only holds from the binary search on; moved it there.

@Stefan-Kohl Stefan-Kohl left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very good. Thanks!

The comment in IsSubsetPlainSet stated an invariant before it held and
did not name the technique; both arguments are now asserted to be plain
sets.

Ranges with ends beyond 2^28 cannot be formed on 32-bit, so the tests
with 2^50 run on 64-bit only; smaller ranges test the same code on both.

Assisted-by: Generative AI

This branch has not been deployed

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

Labels

kind: enhancement Label for issues suggesting enhancements; and for pull requests implementing enhancements release notes: use title For PRs: the title of this PR is suitable for direct use in the release notes topic: library topic: performance bugs or enhancements related to performance (improvements or regressions)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cannot IsSubset be faster by orders of magnitude?

3 participants