-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Avoid sort allocations #116109
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Avoid sort allocations #116109
Conversation
@EgorBot -intel using BenchmarkDotNet.Attributes;
public class Bench
{
int[] a = new int[10000];
Comparison<int> c = (int x, int y) => x - y;
[IterationSetup]
public void IterationSetup() { for (int i = 0; i < a.Length; i++) a[i] = i; }
[Benchmark]
public void Sort() => Array.Sort(a, c);
} |
Could you please share perf numbers that demonstrate the performance improvements? The one micro-benchmark that I have kicked off above shows ~10% regression: EgorBot/runtime-utils#370 (comment) . |
No, I haven't done any performance testing. |
@2A5F You can use the benchmarks from the performance repo (we use them for ensuring there are no regressions): You can both trace files and disassembler output: https://github.com/dotnet/performance/tree/main/src/benchmarks/micro#quick-start |
c46122b
to
1b22a69
Compare
@adamsitnik Thanks for the notice Test Code[Benchmark]
public void Span_ComparerStruct() => _arrays[_iterationIndex++].AsSpan(0, Size).Sort(new ComparableComparerStruct());
private readonly struct ComparableComparerStruct : IComparer<T>
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int Compare(T x, T y) => x.CompareTo(y);
} All test on
Int32
BigStruct
IntClass
IntStruct
String
|
Ideas and todo:
|
To avoid duplicate code, could you do something like this: struct DelegateWrappedComparer<T> : IComparer<T>
{
private Comparer<T> _comparer;
public int Compare(T? x, T? y) => _comparer.Compare(x, y);
} And point the "old" version of the method to the new one, with this wrapper? Hope this is clear. |
@andrewjsaid For Delegate is This test has expired/is invalid. I will retest the new code later when I have time. (benchmarks are really slow) |
My reasoning was that by using the struct wrapper, methods like the below would be specialized for that struct and the JIT would be able to inline the call to private static void SwapIfGreater<TComparer>(Span<T> keys, TComparer comparer, int i, int j)
where TComparer : IComparer<T>, allows ref struct However on further reflection I note that currently since |
|
Oops already suggested 🤦 Thanks for the link |
@2A5F I assume the following case is mainly the case we are improving in
@jkotas you were asking before about the benchmark numbers which is here now #116109 (comment). Any feedback? I looked at the changes and it looks good to me as mostly we are mimicking the non-value type case. |
src/libraries/System.Private.CoreLib/src/System/Collections/Generic/ArraySortHelper.cs
Outdated
Show resolved
Hide resolved
if (span.Length > 1) | ||
{ | ||
ArraySortHelper<T>.Default.Sort(span, comparer); // value-type comparer will be boxed | ||
if (typeof(TComparer).IsValueType) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This works well only when:
- TComparer is a small struct. If TComparer is a larger struct, this change is going to be a performance regression.
- TComparer implementation does not carry state like cached information. If it is not the case, this change is going to be an observable behavior change.
For example, the following program is going to print "Too many cache misses" after this change:
using System;
using System.Collections.Generic;
int[] a = new int[10000];
a.AsSpan().Sort(new CachingComparer<string>());
struct CachingComparer<T> : IComparer<int>
{
static int cacheMisses;
bool cached;
public int Compare(int x, int y)
{
if (!cached)
{
... code to cache something useful ...
cacheMisses++;
if (cacheMisses > 1000) Console.WriteLine("Too many cache misses");
cached = true;
}
return x-y;
}
}
I think it is ok to assume that TComparer implementation is well-behaving and that it does not have these issues. I am just pointing out the potential risks.
@EgorBot -intel using BenchmarkDotNet.Attributes;
public class Bench
{
int[] a = new int[10000];
int[] b = new int[10000];
MyComparer c = new MyComparer();
[IterationSetup]
public void IterationSetup() { for (int i = 0; i < a.Length; i++) a[i] = i; }
[Benchmark]
public void Sort() => Array.Sort(a, b, c);
class MyComparer : IComparer<int>
{
public int Compare(int x, int y) => x-y;
}
} |
The microbenchmark that I have posted shows 1.27x regression. |
Considering that, should we close this PR? or is there any suggestion to improve the regressed case? |
646f4d0
to
6c02cb3
Compare
6c02cb3
to
25b1914
Compare
I accidentally pushed to the wrong branch; the tags and review may need to be adjusted. |
src/libraries/System.Private.CoreLib/src/System/Collections/Generic/ArraySortHelper.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Private.CoreLib/src/System/MemoryExtensions.cs
Outdated
Show resolved
Hide resolved
The throughput should be fixable by introducing even more code duplication. (A separate question is whether the code duplication is worth it.) |
} | ||
else | ||
{ | ||
ArraySortHelper<T>.Default.Sort(span, comparer); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this path allocation-free, or does it still have allocations from the IComparer.Compare delegate? (I am not able to tell by looking at the code - want to make sure that we understand what we are getting.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, this path still use delegate.
Modified
ArraySortHelper
and addedArraySortHelper<T, TComparer>
to allow generic struct TComparer without memory allocationSpan.Sort<T, TComparer>
will not box now#39466
#39543