Skip to content

Optimize ImmutableHashSet<T>.SetEquals to avoid unnecessary allocations - #126309

Merged
eiriktsarpalis merged 1 commit into
dotnet:mainfrom
aw0lid:fix-immutablehashset-setequals-allocs
Apr 30, 2026
Merged

Optimize ImmutableHashSet<T>.SetEquals to avoid unnecessary allocations#126309
eiriktsarpalis merged 1 commit into
dotnet:mainfrom
aw0lid:fix-immutablehashset-setequals-allocs

Conversation

@aw0lid

@aw0lidaw0lid commented Mar 30, 2026

Copy link
Copy Markdown
Contributor

Fixes#90986, Part of #127279

Summary

ImmutableHashSet<T>.SetEquals always creates a new intermediate HashSet<T> for the other collection, leading to avoidable allocations and GC pressure, especially for large datasets

Optimization Logic

  • O(1) Pre-Scan: Immediately returns false if other is an ICollection with a smaller Count, avoiding any overhead.
  • Fast-Path Pattern Matching: Detects ImmutableHashSet<T> and HashSet<T> to bypass intermediate allocations.
  • Comparer Guard: Validates EqualityComparer compatibility before triggering fast paths to ensure logical consistency.
  • Short-Circuit Validation: Re-validates Count within specialized paths for an immediate exit before $O(n)$ enumeration.
  • Reverse-Lookup Strategy: An architectural shift where the ImmutableHashSet (The Source) iterates and queries the other collection if was Hashset. This leverages the O(1) lookup of the HashSet instead of the O(log N) lookup of the immutable tree.
  • Zero-Allocation Execution: Direct iteration over compatible collections, eliminating the costly new HashSet<T>(other) fallback.
  • Deferred fallback: Reserves the expensive allocation solely for general IEnumerable types.
Click to expand Benchmark Source Code
usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Order;usingBenchmarkDotNet.Running;usingSystem;usingSystem.Collections.Generic;usingSystem.Collections.Immutable;usingSystem.Linq;namespaceImmutableHashSetBenchmarks{[MemoryDiagnoser][Orderer(SummaryOrderPolicy.FastestToSlowest)][RankColumn]publicclassImmutableHashSetSetEqualsBenchmark_Int{privateImmutableHashSet<int>_sourceSet=null!;privateImmutableHashSet<int>_immutableHashSetEqual=null!;privateHashSet<int>_bclHashSetEqual=null!;privateList<int>_listEqual=null!;privateIEnumerable<int>_linqSelectEqual=null!;privateint[]_arrayEqual=null!;privateList<int>_listLastDiff=null!;privateList<int>_listSmaller=null!;privateImmutableHashSet<int>_immutableLarger=null!;privateint[]_smallerArray=null!;privateHashSet<int>_smallerHashSetDiffComparer=null!;// Worst case: same count, last element differentprivateImmutableHashSet<int>_immutableHashSetLastDiff=null!;privateHashSet<int>_bclHashSetLastDiff=null!;privateList<int>_listWithDuplicates=null!;privateList<int>_listWithDuplicatesMatch=null!;// Different comparers (fallback path)privateHashSet<int>_bclHashSetDiffComparer=null!;// Count mismatch early exitprivateImmutableHashSet<int>_immutableHashSetSmaller=null!;privateHashSet<int>_bclHashSetSmaller=null!;// Lazy enumerable for worst caseprivateIEnumerable<int>_lazyEnumerableLastDiff=null!;[Params(100000)]publicintSize{get;set;}[GlobalSetup]publicvoidSetup(){varelements=Enumerable.Range(0,Size).ToList();varelementsWithLastDiff=Enumerable.Range(0,Size-1).Concat(new[]{Size+1000}).ToList();varsmallerElements=Enumerable.Range(0,Size/2).ToList();varduplicates=Enumerable.Repeat(1,Size).ToList();varsmallerList=newList<int>();for(inti=0;i<Size-1;i++)smallerList.Add(i);_sourceSet=ImmutableHashSet.CreateRange(elements);_immutableHashSetEqual=ImmutableHashSet.CreateRange(elements);_bclHashSetEqual=newHashSet<int>(elements);_listEqual=elements;_linqSelectEqual=elements.Select(x =>x);// Lazy LINQ enumerable_arrayEqual=elements.ToArray();_immutableHashSetLastDiff=ImmutableHashSet.CreateRange(elementsWithLastDiff);_bclHashSetLastDiff=newHashSet<int>(elementsWithLastDiff);_listLastDiff=elementsWithLastDiff;_bclHashSetDiffComparer=newHashSet<int>(elements,newReverseComparer<int>());_immutableHashSetSmaller=ImmutableHashSet.CreateRange(smallerElements);_bclHashSetSmaller=newHashSet<int>(smallerElements);_lazyEnumerableLastDiff=elementsWithLastDiff.Select(x =>x);_immutableLarger=ImmutableHashSet.CreateRange(elements.Concat(new[]{-1}));_listWithDuplicates=duplicates;_listWithDuplicatesMatch=elements.Concat(elements).ToList();// Matches source but with duplicates_listSmaller=smallerList;_smallerArray=Enumerable.Range(0,Size-1).ToArray();_smallerHashSetDiffComparer=newHashSet<int>(_listSmaller,newReverseComparer<int>());}
#region Fast Path: Same Type and Comparer (Optimized)
[Benchmark(Description="ImmutableHashSet (Match - Same Comparer)")]publicboolCase_ImmutableHashSet_Match()=>_sourceSet.SetEquals(_immutableHashSetEqual);[Benchmark(Description="BCL HashSet (Match - Same Comparer)")]publicboolCase_BclHashSet_Match()=>_sourceSet.SetEquals(_bclHashSetEqual);[Benchmark(Description="ImmutableHashSet (Mismatch - Same Count)")]publicboolCase_ImmutableHashSet_LastDiff()=>_sourceSet.SetEquals(_immutableHashSetLastDiff);[Benchmark(Description="Case 04: BCL HashSet (Mismatch - Same Count)")]publicboolCase_BclHashSet_LastDiff()=>_sourceSet.SetEquals(_bclHashSetLastDiff);
#endregion
#region Early Exit: Count Mismatch
[Benchmark(Description="ImmutableHashSet (Smaller Count)")]publicboolCase_ImmutableHashSet_SmallerCount()=>_sourceSet.SetEquals(_immutableHashSetSmaller);[Benchmark(Description="BCL HashSet (Smaller Count)")]publicboolCase_BclHashSet_SmallerCount()=>_sourceSet.SetEquals(_bclHashSetSmaller);[Benchmark(Description="Array (Smaller Count)")]publicboolCase_SmallerCollection_EarlyExit(){return_sourceSet.SetEquals(_smallerArray);}
#endregion
#region Fallback Path: Different Comparer
[Benchmark(Description="HashSet (Different Comparer)")]publicboolCase_HashSet_DifferentComparer()=>_sourceSet.SetEquals(_bclHashSetDiffComparer);[Benchmark(Description="HashSet (Smaller Count - Different Comparer)")]publicboolCase_HashSet_SmallerCount_DiffComparer()=>_sourceSet.SetEquals(_smallerHashSetDiffComparer);
#endregion
#region Fallback Path: Non-Set Collections (IEnumerable/ICollection)
[Benchmark(Description="List (Match - Fallback)")]publicboolCase_List_Match()=>_sourceSet.SetEquals(_listEqual);[Benchmark(Description="LINQ (Mismatch - Lazy IEnumerable)")]publicboolCase_LazyEnumerable_LastDiff()=>_sourceSet.SetEquals(_lazyEnumerableLastDiff);[Benchmark(Description="LINQ (Match - Lazy IEnumerable)")]publicboolCase_LazyEnumerable_Match()=>_sourceSet.SetEquals(_linqSelectEqual);[Benchmark(Description="List (Last Diff - Fallback)")]publicboolCase_List_LastDiff()=>_sourceSet.SetEquals(_listLastDiff);[Benchmark(Description="Array (Match - Fallback)")]publicboolCase_Array_Match()=>_sourceSet.SetEquals(_arrayEqual);[Benchmark(Description="ImmutableHashSet (Larger Count)")]publicboolCase_LargerCount()=>_sourceSet.SetEquals(_immutableLarger);
#endregion
#region Handling Duplicates (Fallback Path)
[Benchmark(Description="List with Duplicates (Mismatch)")]publicboolCase_List_Duplicates_Mismatch()=>_sourceSet.SetEquals(_listWithDuplicates);[Benchmark(Description="List with Duplicates (Match)")]publicboolCase_List_Duplicates_Match()=>_sourceSet.SetEquals(_listWithDuplicatesMatch);
#endregion
}publicclassReverseComparer<T>:IEqualityComparer<T>whereT:IComparable<T>{publicboolEquals(T?x,T?y){if(xisnull&&yisnull)returntrue;if(xisnull||yisnull)returnfalse;returnx.CompareTo(y)==0;}publicintGetHashCode(T?obj){returnobj?.GetHashCode()??0;}}publicclassProgram{publicstaticvoidMain(string[]args){BenchmarkRunner.Run<ImmutableHashSetSetEqualsBenchmark_Int>();}}}
Click to expand Benchmark Results

Benchmark Results (Before Optimization)

MethodSizeMeanErrorStdDevRankGen0Gen1Gen2Allocated
'BCL HashSet (Smaller Count)'100000313.8 us6.01 us6.43 us115.625015.625015.6250818.33 KB
'Array (Smaller Count)'100000647.9 us11.20 us11.50 us226.367226.367226.36721697.7 KB
'List with Duplicates (Mismatch)'100000954.1 us18.77 us41.60 us331.250031.250031.25001697.77 KB
' HashSet (Smaller Count - Different Comparer)'1000001,449.3 us28.65 us74.46 us441.015641.015641.01561697.8 KB
' ImmutableHashSet (Smaller Count)'1000004,733.2 us74.18 us69.39 us523.437523.437523.4375818.58 KB
' BCL HashSet (Match - Same Comparer)'1000007,084.0 us65.02 us57.64 us654.687554.687554.68751697.9 KB
'Array (Match - Fallback)'1000007,821.7 us30.71 us27.23 us746.875046.875046.87501697.86 KB
'List (Match - Fallback)'1000008,428.4 us30.82 us28.83 us846.875046.875046.87501697.9 KB
'BCL HashSet (Mismatch - Same Count)'1000008,636.3 us52.37 us46.42 us846.875046.875046.87501697.86 KB
'List (Last Diff - Fallback)'1000009,172.5 us35.85 us33.54 us946.875046.875046.87501697.9 KB
'List with Duplicates (Match)'1000009,310.2 us128.11 us119.83 us9109.3750109.3750109.37503521.42 KB
' ImmutableHashSet (Larger Count)'1000009,477.3 us141.55 us125.48 us946.875046.875046.87501697.89 KB
' HashSet (Different Comparer)'1000009,839.2 us99.14 us87.88 us946.875046.875046.87501697.79 KB
'LINQ (Mismatch - Lazy IEnumerable)'10000011,274.4 us63.77 us56.53 us10296.8750156.2500156.25004717.23 KB
'LINQ (Match - Lazy IEnumerable)'10000011,341.5 us69.37 us61.49 us10296.8750156.2500156.25004717.23 KB
'ImmutableHashSet (Mismatch - Same Count)'10000017,015.5 us170.03 us150.73 us1131.250031.250031.25001697.88 KB
'ImmutableHashSet (Match - Same Comparer)'10000017,410.2 us334.48 us312.87 us1131.250031.250031.25001697.87 KB

Benchmark Results (After Optimization)

MethodSizeMeanErrorStdDevRankGen0Gen1Gen2Allocated
'ImmutableHashSet (Smaller Count)'1000002.300 ns0.0478 ns0.0447 ns1----
'ImmutableHashSet (Larger Count)'1000002.328 ns0.0650 ns0.0576 ns1----
'BCL HashSet (Smaller Count)'1000002.595 ns0.0524 ns0.0491 ns2----
'HashSet (Smaller Count - Different Comparer)'1000002.644 ns0.0464 ns0.0411 ns2----
'Array (Smaller Count)'1000002.711 ns0.0568 ns0.0504 ns2----
'List with Duplicates (Mismatch)'100000794,876.698 ns15,781.0452 ns35,941.4284 ns331.250031.250031.25001738498 B
'List (Last Diff - Fallback)'1000004,722,211.915 ns55,323.2393 ns51,749.3924 ns454.687554.687554.68751738698 B
'List (Match - Fallback)'1000004,778,905.952 ns33,894.4095 ns28,303.3670 ns454.687554.687554.68751738688 B
'List with Duplicates (Match)'1000005,517,422.167 ns110,159.9473 ns171,505.7803 ns593.750093.750093.75003605853 B
'BCL HashSet (Match - Same Comparer)'1000005,576,721.937 ns45,754.5403 ns38,207.1134 ns5----
'Case 04: BCL HashSet (Mismatch - Same Count)'1000005,640,651.163 ns64,526.5199 ns60,358.1468 ns5----
'LINQ (Mismatch - Lazy IEnumerable)'1000006,406,188.227 ns132,260.6999 ns379,480.7689 ns6281.2500140.6250140.62504830429 B
'LINQ (Match - Lazy IEnumerable)'1000006,784,385.648 ns135,159.5121 ns290,945.1304 ns7250.0000125.0000125.00004830439 B
'Array (Match - Fallback)'1000006,812,793.701 ns40,732.0373 ns36,107.8901 ns754.687554.687554.68751738653 B
'HashSet (Different Comparer)'1000007,497,254.730 ns80,339.5419 ns75,149.6574 ns862.500062.500062.50001738753 B
'ImmutableHashSet (Mismatch - Same Count)'10000012,946,989.847 ns94,279.9494 ns83,576.7194 ns9----
'ImmutableHashSet (Match - Same Comparer)'10000013,615,905.022 ns57,544.4439 ns48,052.2169 ns10----

Performance Analysis Summary (100,000 Elements)

Case / MethodBefore (ns)After (ns)Speedup RatioMemory Improvement
ImmutableHashSet (Larger Count)9,477,3002.328~4,071,005xZero Alloc
ImmutableHashSet (Smaller Count)4,733,2002.300~2,057,913xZero Alloc
HashSet (Smaller - Diff Comparer)1,449,3002.644~548,146xZero Alloc
Array (Smaller Count)647,9002.711~238,989xZero Alloc
BCL HashSet (Smaller Count)313,8002.595~120,924xZero Alloc
HashSet (Different Comparer)9,839,2007,497,2541.31xStable (~1.7 MB)
LINQ (Match/Mismatch)11,341,5006,406,1881.77xStable (~4.8 MB)
BCL HashSet (Mismatch - Same Count)8,636,3005,640,6511.53xZero Alloc
ImmutableHashSet (Match)17,410,20013,615,9051.28xZero Alloc
ImmutableHashSet (Mismatch)17,015,50012,946,9891.31xZero Alloc
List (Match/Diff - Fallback)9,172,5004,722,2111.94xStable (~1.7 MB)
BCL HashSet (Match - Same Comp)7,084,0005,576,7211.27xZero Alloc
List (Duplicates - Mismatch)954,100794,8761.20xStable (~1.7 MB)
List (Duplicates - Match)9,310,2005,517,4221.69xStable (~3.6 MB)
Array (Match - Fallback)7,821,7006,812,7931.15xStable (~1.7 MB)

@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Mar 30, 2026
@aw0lid
aw0lidforce-pushed the fix-immutablehashset-setequals-allocs branch from 9910d86 to ff6af74CompareApril 3, 2026 14:44
@aw0lid
aw0lidforce-pushed the fix-immutablehashset-setequals-allocs branch from ff6af74 to 5f2749eCompareApril 3, 2026 19:27
@aw0lid
aw0lid requested a review from stephentoubApril 4, 2026 11:46
@aw0lid
aw0lidforce-pushed the fix-immutablehashset-setequals-allocs branch 3 times, most recently from 3c685c8 to 45c2c14CompareApril 8, 2026 23:05
@aw0lid
aw0lidforce-pushed the fix-immutablehashset-setequals-allocs branch from 45c2c14 to 6a3ebf6CompareApril 12, 2026 20:53

@eiriktsarpaliseiriktsarpalis 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.

This change is adding a whole lot of runtime type checks. Is there tangible evidence (e.g. in the form of microbenchmarks) showing improvement here (both when other is a set but more importantly when it is not)?.

@aw0lid

Copy link
Copy Markdown
ContributorAuthor

This change is adding a whole lot of runtime type checks. Is there tangible evidence (e.g. in the form of microbenchmarks) showing improvement here (both when other is a set but more importantly when it is not)?.

As the benchmark results indicate, there is no performance regression even in the fallback paths. This demonstrates that the added runtime type checks do not impact performance, while providing massive gains in the optimized paths

@aw0lid
aw0lidforce-pushed the fix-immutablehashset-setequals-allocs branch 2 times, most recently from edeeb71 to d769373CompareApril 26, 2026 14:12
@eiriktsarpalis

Copy link
Copy Markdown
Member

As the benchmark results indicate, there is no performance regression even in the fallback paths. This demonstrates that the added runtime type checks do not impact performance, while providing massive gains in the optimized paths

What do the numbers show when comparing small (0-10 elements) or collections that are not equal?

@aw0lid
aw0lidforce-pushed the fix-immutablehashset-setequals-allocs branch from d769373 to 5e1f434CompareApril 28, 2026 18:54
@aw0lid

Copy link
Copy Markdown
ContributorAuthor

What do the numbers show when comparing small (0-10 elements) or collections that are not equal?

Performance Comparison: Before vs. After Optimization (10 elements)

Case / MethodTime (Before)Time (After)Speedup / RegressionMemory (Before)Memory (After)Memory Gain
ImmutableHashSet (Larger Count)810.3 ns2.29 ns~353x Faster440 B0 B-100%
ImmutableHashSet (Smaller Count)408.5 ns2.31 ns~176x Faster376 B0 B-100%
HashSet (Smaller - Diff Comp)166.1 ns2.57 ns~64x Faster336 B0 B-100%
BCL HashSet (Smaller Count)164.0 ns2.59 ns~63x Faster232 B0 B-100%
Array (Smaller Count)166.1 ns3.15 ns~52x Faster296 B0 B-100%
ImmutableHashSet (Mismatch)857.6 ns426.8 ns~2.0x Faster440 B0 B-100%
ImmutableHashSet (Match)867.1 ns456.5 ns~1.9x Faster440 B0 B-100%
List with Duplicates (Mismatch)191.5 ns184.4 ns1.04x Faster440 B440 BStable
HashSet (Different Comparer)315.6 ns429.9 ns1.36x Slower336 B336 BStable
BCL HashSet (Mismatch)218.9 ns461.7 ns2.11x Slower296 B0 B-100%
BCL HashSet (Match)213.7 ns468.5 ns2.19x Slower296 B0 B-100%
Array (Match - Fallback)191.0 ns552.1 ns2.89x Slower296 B296 BStable
List (Match - Fallback)235.2 ns602.4 ns2.56x Slower336 B336 BStable
List (Last Diff - Fallback)221.1 ns618.7 ns2.80x Slower336 B336 BStable
List with Duplicates (Match)315.5 ns695.4 ns2.20x Slower528 B528 BStable
LINQ (Mismatch/Match)~470.0 ns~800.0 ns1.72x Slower736 B736 BStable

Based on the data, there is a minor increase in execution time for micro-collections (N=10) due to the necessary type-checks and comparer validation. In exchange, we achieved Zero Allocations for all Set-to-Set comparisons.

What do you think, is it a good trade-off ?

@aw0lid

aw0lid commented Apr 28, 2026

Copy link
Copy Markdown
ContributorAuthor

The count validation has been moved out of SetEqualsWithImmutableHashset and SetEqualsWithHashset, and the iteration logic in SetEqualsWithImmutableHashset has been reversed. This was done to make these methods reusable for optimizing other set operations, such as IsProperSubsetOf and IsSubsetOf, in subsequent PRs after this one is merged

This refactoring enables us to implement logic like the following:

privatestaticboolIsProperSubsetOf(IEnumerable<T>other,MutationInputorigin){Requires.NotNull(other,nameof(other));if(origin.Root.IsEmpty){returnother.Any();}if(otherisICollection<T>otherAsICollectionGeneric){// We check for < instead of != because other is not guaranteed to be a set, it could be a collection with duplicates.if(otherAsICollectionGeneric.Count<=origin.Count){returnfalse;}switch(other){caseImmutableHashSet<T>otherAsImmutableHashSet:if(origin.EqualityComparer.Equals(otherAsImmutableHashSet.KeyComparer)){returnSetEqualsWithImmutableHashset(otherAsImmutableHashSet,origin);}break;caseHashSet<T>otherAsHashset:if(origin.EqualityComparer.Equals(otherAsHashset.Comparer)){returnSetEqualsWithHashset(otherAsHashset,origin);}break;}}elseif(otherisICollectionotherAsICollection&&otherAsICollection.Count<=origin.Count){returnfalse;}varotherSet=newHashSet<T>(other,origin.EqualityComparer);if(otherSet.Count<=origin.Count){returnfalse;}returnSetEqualsWithHashset(otherSet,origin);}

@aw0lid
aw0lidforce-pushed the fix-immutablehashset-setequals-allocs branch from 5e1f434 to efd60f6CompareApril 29, 2026 11:17
@aw0lid
aw0lidforce-pushed the fix-immutablehashset-setequals-allocs branch from efd60f6 to 35150bcCompareApril 29, 2026 12:46
@aw0lid
aw0lidforce-pushed the fix-immutablehashset-setequals-allocs branch from 35150bc to bc9e969CompareApril 29, 2026 13:33

@eiriktsarpaliseiriktsarpalis 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.

Could you share updated benchmark numbers please?

@aw0lid

aw0lid commented Apr 29, 2026

Copy link
Copy Markdown
ContributorAuthor

Could you share updated benchmark numbers please?

I have already updated the PR description with the latest benchmark results. You can find the comparison and the performance improvements detailed there

@aw0lid

Copy link
Copy Markdown
ContributorAuthor

CI seems to be stuck on infrastructure issues. Could someone please trigger a re-run for the failed checks? Thanks!

@eiriktsarpalis

Copy link
Copy Markdown
Member

/ba-g test failures are unrelated

@eiriktsarpalis
eiriktsarpalis merged commit dd17b6d into dotnet:mainApr 30, 2026
81 of 87 checks passed
@aw0lid
aw0lid deleted the fix-immutablehashset-setequals-allocs branch April 30, 2026 14:30
eiriktsarpalis pushed a commit that referenced this pull request May 4, 2026
…127633)
## Summary
This PR fixes a correctness issue in `SetEquals` introduced in my
previous PR #126309.
The `Count` check was moved inside the `Comparer` equality block. This
ensures that when comparers differ, we don't return a false negative and
instead fall back to the safe path.
### Example of the issue fixed:
```csharp
// This should return true, but was returning false
var main = ImmutableHashSet.Create(StringComparer.OrdinalIgnoreCase, "a");
var other = ImmutableHashSet.Create("a", "A");
Console.WriteLine(main.SetEquals(other));
```
## Changes
- Moved `if (count != origin.Count)` inside the
`EqualityComparer<IEqualityComparer<T>>.Default.Equals` check.
- This ensures mismatched comparers safely bypass the fast-path and
proceed to a proper set comparison.
## Testing
In addition to the fix, I have added comprehensive unit tests covering
various scenarios to ensure correctness:
- **Mismatched Comparers (Ordinal vs. OrdinalIgnoreCase):** Verified
that `SetEquals` returns `true` when logically equal but with different
comparers, and `false` when logically different.
- **ICollection with Duplicates:** Verified the fallback path correctly
handles collections like `List<T>` with duplicate elements.
- **Count Optimizations:** - Verified that mismatched comparers correctly bypass the fast-path
count check.
- Verified that `SetEquals` still performs early-exit when `other.Count
< origin.Count`.
- **Fast-Path Validation:** Ensured that when comparers match, the
optimized count-based comparison still works as expected.
- **Edge Cases:** Included tests for empty sets with different comparers
and content-specific mismatches.
**Related to:** #126309
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jun 5, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Collectionscommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ImmutableHashSet<T>.SetEquals always creates a new HashSet<T>

7 participants

@aw0lid@tannergooding@eiriktsarpalis@pentp@stephentoub@PranavSenthilnathan@KalleOlaviNiemitalo