Fix regression in Array.Sort for floats/doubles - #37941

Merged
stephentoub merged 3 commits into
dotnet:masterfrom
stephentoub:fixsortperf_utilslessthanref
Jun 28, 2020
Merged

Fix regression in Array.Sort for floats/doubles#37941
stephentoub merged 3 commits into
dotnet:masterfrom
stephentoub:fixsortperf_utilslessthanref

Conversation

@stephentoub

@stephentoubstephentoub commented Jun 16, 2020

Copy link
Copy Markdown
Member

Several months back we moved the sorting logic for primtive types out of native code into managed. Doing so helped to make the logic reusable for spans and helped to reduce GC latency, and also actually helped with throughput in a variety of cases. But it ended up regressing throughput for sorting larger arrays of floating-point values, with float/double.CompareTo not getting inlined, and even if it were inlined, containing much more logic than was present in the native implementation. The native implementation did a pre-pass to move all NaNs to the front and then just used simple < and > comparison operations, so the managed implementation now does as well.

With the exception of a large array of already-sorted Int32 values where there is still a small regression after this PR, all of the cases I've tested are either as good or better than .NET Core 3.1.

@jkotas, @GrabYourPitchforks, @tannergooding, thanks for your offline suggestions on approaches here; I tried out a variety of them, including vectorized float/double.CompareTo as well as unsafe casts to wrapper types with customized IComparable implementations, and this ended up being the best overall. Thanks as well to @nietras for pointing out the regression.

Benchmark:

usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;usingSystem;usingSystem.Diagnostics.CodeAnalysis;usingSystem.Linq;[MemoryDiagnoser]publicclassProgram{staticvoidMain(string[]args)=>BenchmarkSwitcher.FromAssemblies(new[]{typeof(Program).Assembly}).Run(args);}publicclassDoubleSorting:Sorting<double>{protectedoverridedoubleGetNext()=>_random.Next();}publicclassInt32Sorting:Sorting<int>{protectedoverrideintGetNext()=>_random.Next();}publicclassStringSorting:Sorting<string>{protectedoverridestringGetNext()=>string.Create(_random.Next(1,5),_random,(dest,r)=>{for(inti=0;i<dest.Length;i++)dest[i]=(char)('a'+r.Next(26));});}publicabstractclassSorting<T>{protectedRandom_random;privateT[]_orig,_array;[Params(10,100_000)]publicintSize{get;set;}protectedabstractTGetNext();[GlobalSetup]publicvoidSetup(){_random=newRandom(42);_orig=Enumerable.Range(0,Size).Select(_ =>GetNext()).ToArray();_array=(T[])_orig.Clone();Array.Sort(_array);}[Benchmark]publicvoidSorted()=>Array.Sort(_array);[Benchmark]publicvoidRandom(){_orig.AsSpan().CopyTo(_array);Array.Sort(_array);}}
TypeMethodToolchainSizeMeanRatio
DoubleSortingSortednetcore311059.58 ns2.13
DoubleSortingSortedmaster1030.52 ns1.09
DoubleSortingSortedpr1028.01 ns1.00
DoubleSortingRandomnetcore311077.50 ns1.71
DoubleSortingRandommaster1067.61 ns1.49
DoubleSortingRandompr1045.44 ns1.00
DoubleSortingSortednetcore31100000990,325.98 ns1.27
DoubleSortingSortedmaster1000002,898,290.96 ns3.73
DoubleSortingSortedpr100000777,209.08 ns1.00
DoubleSortingRandomnetcore311000005,940,056.30 ns1.08
DoubleSortingRandommaster1000007,880,560.62 ns1.44
DoubleSortingRandompr1000005,473,335.58 ns1.00
Int32SortingSortednetcore311038.02 ns2.31
Int32SortingSortedmaster1017.09 ns1.04
Int32SortingSortedpr1016.49 ns1.00
Int32SortingRandomnetcore311049.97 ns1.63
Int32SortingRandommaster1031.30 ns1.02
Int32SortingRandompr1030.63 ns1.00
Int32SortingSortednetcore31100000572,908.37 ns0.90
Int32SortingSortedmaster100000640,966.00 ns1.00
Int32SortingSortedpr100000639,118.53 ns1.00
Int32SortingRandomnetcore311000005,072,236.56 ns1.06
Int32SortingRandommaster1000005,024,276.17 ns1.05
Int32SortingRandompr1000004,801,833.82 ns1.00
StringSortingSortednetcore3110575.87 ns1.24
StringSortingSortedmaster10432.40 ns0.93
StringSortingSortedpr10465.86 ns1.00
StringSortingRandomnetcore31101,758.64 ns1.15
StringSortingRandommaster101,425.75 ns0.93
StringSortingRandompr101,532.01 ns1.00
StringSortingSortednetcore3110000083,774,554.44 ns1.14
StringSortingSortedmaster10000074,485,247.25 ns1.01
StringSortingSortedpr10000073,450,518.68 ns1.00
StringSortingRandomnetcore31100000103,108,672.31 ns1.14
StringSortingRandommaster10000089,373,058.33 ns0.99
StringSortingRandompr10000090,577,543.59 ns1.00

@stephentoubstephentoub added this to the 5.0.0 milestone Jun 16, 2020

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

Nice!

@stephentoub
stephentoubforce-pushed the fixsortperf_utilslessthanref branch from 046f85c to 07dcfe1CompareJune 17, 2020 02:27
@stephentoub
stephentoubforce-pushed the fixsortperf_utilslessthanref branch from 07dcfe1 to 04e1399CompareJune 17, 2020 09:43
@nietras

Copy link
Copy Markdown
Contributor

@stephentoub perhaps consider testing performance of following type too:

publicclassComparableClassInt32:IComparable<ComparableClassInt32>{publicreadonlyintValue;publicComparableClassInt32(intvalue)=>Value=value;publicintCompareTo(ComparableClassInt32other)=>Value.CompareTo(other.Value);}

basic reference type overhead. Since string compares are "slow" you won't see possible reference type regressions for this. Just a suggestion :)

@stephentoub

Copy link
Copy Markdown
MemberAuthor

Just a suggestion :)

A knowing smile? 😉 Yes, this case regresses. It appears to be due to dictionary lookups when calling LessThan/GreaterThan, which also prevent inlining. Evaluating options...

@nietras

Copy link
Copy Markdown
Contributor

knowing smile? 😉

Ha yeah 😉 Back in 2018 I went through all "stages of inlining": huh?, wuhuu! (AggressiveInlining), doh! (reference type regression), oh come on! (JIT-me-not issues) 😅

Looking forward to what you come up with. 😀

@stephentoubstephentoub added the NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) label Jun 18, 2020
@stephentoub

Copy link
Copy Markdown
MemberAuthor

#38229 will hopefully be the solution here.

@nietras

Copy link
Copy Markdown
Contributor

Nice 👍 Now if #35791/#10048 were resolved too, we would have something to talk about 😉 Happy to help with that if I could get some pointers 😀

@AndyAyersMS

Copy link
Copy Markdown
Member

Happy to help with that if I could get some pointers

I'm certainly open to reconsidering #10048. The changes are simple enough, though I might end up restricting it to AggressiveInlining callees since the jit underestimates the size impact of a delegate invoke.

But I still don't have a clear picture of how allowing this actually provides benefit -- it sounds like you think with this we can unify some code and either make it perform better or at least not lose any perf. So perhaps a benchmark along these lines would be instructive?

@stephentoub

Copy link
Copy Markdown
MemberAuthor

it sounds like you think with this we can unify some code and either make it perform better or at least not lose any perf.

Not to put words in @nietras mouth, but I expect what he's hoping to do is improve the Array.Sort(..., IComparer<T>) code paths. Today we have one sort implementation that covers both providing an IComparer<T> and a Comparison<T> (a delegate); the former is implemented by creating a delegate to its Compare method, which means Array.Sort(..., IComparer<T>) is allocating. On top of that, we just recently added a span-based sort, and it's currently defined with a generic TComparer : IComparer<T> comparer, with the idea that you could provide a struct-based comparer and it wouldn't allocate... unfortunately, it's the worst of all worlds right now, in that we box the TComparer into an IComparer<T>, and then create a delegate from its Compare method. If all of these code quality issues got sorted out, you could imagine we just had a single code path for TComparer that was written generically to use its Compare sans boxing, and then if you used the Comparison<T> overload, we'd create a struct-based TComparer that just wrapped that Comparison<T> to invoke it. If we everything worked out really well, it could potentially even be unified with the separate, duplicate code path we currently have that targets T : IComparable<T>, using a TComparer comparer struct that delegated to the T : IComparable<T> implementation.

So perhaps a benchmark along these lines would be instructive?

Sounds like a good thing to add to dotnet/performance.

@nietras

nietras commented Jun 23, 2020

Copy link
Copy Markdown
Contributor

If all of these code quality issues got sorted out, you could imagine we just had a single code path for TComparer that was written generically to use its Compare sans boxing, and then if you used the Comparison overload, we'd create a struct-based TComparer that just wrapped that Comparison to invoke it. If we everything worked out really well, it could potentially even be unified with the separate, duplicate code path we currently have that targets T : IComparable, using a TComparer comparer struct that delegated to the T : IComparable implementation.

@stephentoub exactly. :) Although, I am not sure we could unify on TComparer as such, but potentially yes. This would be the fulfillment of the API I have proposed and the on/off work I have been doing on this for the last 3 years... failing due to the many issues around inlining. At the very minimum we could replace the Comparison<T> path with the TComparer path without duplicating code.

Note this is not just about sorting. Sorting, however, for me is a good example of where .NET comes short. I use the value type as a inlineable "functor" pattern for data processing algorithms. Think loops over millions of elements. Unfortunately, we can't unify on this pattern due to these kinds of issues, which Sort exemplifies very well. Lots of other devs use this pattern, but we often end up having to duplicate code, and since this is code that is combinatorial on rank, different kinds of transformations etc. it adds up. It's a lot of replicated code. I have talked about this before and don't want to sound like a broken record player 😅

with the idea that you could provide a struct-based comparer and it wouldn't allocate...

It's not just the allocation. It's so the compare can be inlined. So the "functor" can be applied inlined. To yield a customized loop for performance. As you probably know.

perhaps a benchmark along these lines would be instructive?

@AndyAyersMS I don't know what kind of benchmark you are thinking about but something simple like below shows the issue.

usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;usingSystem;usingSystem.Collections.Generic;usingSystem.Runtime.CompilerServices;namespaceCompareBenchmarking{publicclassProgram{staticvoidMain(string[]args)=>BenchmarkSwitcher.FromAssemblies(new[]{typeof(Program).Assembly}).Run(args);}publicclassCompareFloat:Compare<float>{protectedoverridefloatGetNext()=>_random.Next();}publicclassCompareInt32:Compare<int>{protectedoverrideintGetNext()=>_random.Next();}publicstructComparisonComparer<T>:IComparer<T>{readonlyComparison<T>_comparison;publicComparisonComparer(Comparison<T>comparison)=>_comparison=comparison;[MethodImpl(MethodImplOptions.AggressiveInlining)]publicintCompare(Tx,Ty)=>_comparison(x,y);}[MemoryDiagnoser][DisassemblyDiagnoser]publicabstractclassCompare<T>whereT:IComparable<T>{staticreadonlyComparer<T>_comparer=Comparer<T>.Default;staticreadonlyComparison<T>_comparison=Comparer<T>.Default.Compare;readonlyComparisonComparer<T>_comparisonComparer=newComparisonComparer<T>(_comparison);protectedRandom_random;T_x;T_y;protectedabstractTGetNext();[GlobalSetup]publicvoidSetup(){_random=newRandom(42);_x=GetNext();_y=GetNext();}[Benchmark]publicintCompareTo()=>_x.CompareTo(_y);[Benchmark]publicintComparer()=>_comparer.Compare(_x,_y);[Benchmark(Baseline=true)]publicintComparison()=>_comparison(_x,_y);[Benchmark]publicintComparisonComparer()=>_comparisonComparer.Compare(_x,_y);}}

With the following results on .NET 5.0 Preview 2. The factor of 2.31x pretty much says it all although that would be pretty self-evident given the extra indirection and code generation issues. Just imagine this in a tight loop. :)

BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19041.329 (2004/?/20H1)
Intel Core i7-8700 CPU 3.20GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores
.NET Core SDK=5.0.100-preview.2.20176.6
[Host] : .NET Core 5.0.0 (CoreCLR 5.0.20.16006, CoreFX 5.0.20.16006), X64 RyuJIT
DefaultJob : .NET Core 5.0.0 (CoreCLR 5.0.20.16006, CoreFX 5.0.20.16006), X64 RyuJIT

CompareInt32

MethodMeanErrorStdDevRatio
CompareTo0.4995 ns0.0051 ns0.0048 ns0.38
Comparer1.1854 ns0.0032 ns0.0028 ns0.89
Comparison1.3258 ns0.0065 ns0.0054 ns1.00
ComparisonComparer3.0613 ns0.0096 ns0.0089 ns2.31

CompareFloat

MethodMeanErrorStdDevRatioRatioSD
CompareTo1.237 ns0.0018 ns0.0015 ns0.600.00
Comparer1.905 ns0.0628 ns0.0557 ns0.930.03
Comparison2.060 ns0.0047 ns0.0039 ns1.000.00
ComparisonComparer3.479 ns0.0096 ns0.0090 ns1.690.01

If this is interesting I can make a PR to the benchmark repo.

@stephentoub
stephentoubforce-pushed the fixsortperf_utilslessthanref branch from 04e1399 to 356828cCompareJune 25, 2020 22:27
@stephentoub
stephentoubforce-pushed the fixsortperf_utilslessthanref branch from 356828c to a3e3d79CompareJune 26, 2020 21:34
AndyAyersMS added a commit to AndyAyersMS/runtime that referenced this pull request Jun 26, 2020
RyuJit would not inline methods that contained delegate invokes. Remove
this limitation.
Closesdotnet#10048. See also dotnet#37941.
jkotas pushed a commit that referenced this pull request Jun 27, 2020
RyuJit would not inline methods that contained delegate invokes. Remove
this limitation.
Closes#10048. See also #37941.
Several months back we moved the sorting logic for primtive types out of native code into managed. Doing so helped to make the logic reusable for spans and helped to reduce GC latency, and also actually helped with throughput in a variety of cases. But it ended up regressing throughput for sorting larger arrays of floating-point values, with float/double.CompareTo not getting inlined, and even if it were inlined, containing much more logic than was present in the native implementation. The native implementation did a pre-pass to move all NaNs to the front and then just used simple < and > comparison operations, so the managed implementation now does as well.
@stephentoub
stephentoubforce-pushed the fixsortperf_utilslessthanref branch from a3e3d79 to 2bec1bfCompareJune 27, 2020 23:57
@stephentoub

Copy link
Copy Markdown
MemberAuthor

I moved the comparer functions into the two generic helper classes, and with #38229 (thanks, @jkotas), the reference-type-Int32-wrapper case is good again, with everything else being appx what it was before as well.

I'll merge when this is green.

@stephentoub
stephentoub merged commit e1c9ab4 into dotnet:masterJun 28, 2020
@stephentoub
stephentoub deleted the fixsortperf_utilslessthanref branch June 28, 2020 01:57
@nietras

Copy link
Copy Markdown
Contributor

@stephentoub should I try make a PR for the TComparer change? Is there interest in getting this in? The change itself to TComparer is easy enough, it's the whole sort helpers creation etc. that needs to change a lot, and to support reference type delegate comparer will likely require a "unsafe" cast of Comparison<T> to Comparison<object>. Is that acceptable?

Also thank you for the mention in your awesome Performance Improvements in .NET 5 :)

@jkotas

jkotas commented Jul 16, 2020

Copy link
Copy Markdown
Member

Hi @nietras,

@stephentoub is OOF.

I think it would be a good idea to start the PR for the TComparer changes so that we can start sorting out the code quality issues that it is likely to expose. Preferably, we would fix them instead of working around them by duplicating large amounts of code.

I have opened #39466 to have this tracked. For time line, I do not expect we would be able to get this change into .NET 5.

"unsafe" cast of Comparison<T> to Comparison<object>

Why would that be needed?

I am wondering whether it would make sense to delete the TComparer overloads from the public surface for now until we can get the right implementation for them in place. I think they can stay, but just wanted to bring it up.

cc @eiriktsarpalis

@ghostghost locked as resolved and limited conversation to collaborators Dec 8, 2020
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.RuntimeNO-MERGEThe PR is not ready for merge yet (see discussion for detailed reasons)tenet-performancePerformance related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants

@stephentoub@nietras@AndyAyersMS@jkotas@EgorBo@GrabYourPitchforks@danmoseley@tannergooding
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Fix regression in Array.Sort for floats/doubles - #37941

Merged
stephentoub merged 3 commits into
dotnet:masterfrom
stephentoub:fixsortperf_utilslessthanref
Jun 28, 2020
Merged

Fix regression in Array.Sort for floats/doubles#37941
stephentoub merged 3 commits into
dotnet:masterfrom
stephentoub:fixsortperf_utilslessthanref

Conversation

@stephentoub

@stephentoubstephentoub commented Jun 16, 2020

Copy link
Copy Markdown
Member

Several months back we moved the sorting logic for primtive types out of native code into managed. Doing so helped to make the logic reusable for spans and helped to reduce GC latency, and also actually helped with throughput in a variety of cases. But it ended up regressing throughput for sorting larger arrays of floating-point values, with float/double.CompareTo not getting inlined, and even if it were inlined, containing much more logic than was present in the native implementation. The native implementation did a pre-pass to move all NaNs to the front and then just used simple < and > comparison operations, so the managed implementation now does as well.

With the exception of a large array of already-sorted Int32 values where there is still a small regression after this PR, all of the cases I've tested are either as good or better than .NET Core 3.1.

@jkotas, @GrabYourPitchforks, @tannergooding, thanks for your offline suggestions on approaches here; I tried out a variety of them, including vectorized float/double.CompareTo as well as unsafe casts to wrapper types with customized IComparable implementations, and this ended up being the best overall. Thanks as well to @nietras for pointing out the regression.

Benchmark:

usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;usingSystem;usingSystem.Diagnostics.CodeAnalysis;usingSystem.Linq;[MemoryDiagnoser]publicclassProgram{staticvoidMain(string[]args)=>BenchmarkSwitcher.FromAssemblies(new[]{typeof(Program).Assembly}).Run(args);}publicclassDoubleSorting:Sorting<double>{protectedoverridedoubleGetNext()=>_random.Next();}publicclassInt32Sorting:Sorting<int>{protectedoverrideintGetNext()=>_random.Next();}publicclassStringSorting:Sorting<string>{protectedoverridestringGetNext()=>string.Create(_random.Next(1,5),_random,(dest,r)=>{for(inti=0;i<dest.Length;i++)dest[i]=(char)('a'+r.Next(26));});}publicabstractclassSorting<T>{protectedRandom_random;privateT[]_orig,_array;[Params(10,100_000)]publicintSize{get;set;}protectedabstractTGetNext();[GlobalSetup]publicvoidSetup(){_random=newRandom(42);_orig=Enumerable.Range(0,Size).Select(_ =>GetNext()).ToArray();_array=(T[])_orig.Clone();Array.Sort(_array);}[Benchmark]publicvoidSorted()=>Array.Sort(_array);[Benchmark]publicvoidRandom(){_orig.AsSpan().CopyTo(_array);Array.Sort(_array);}}
TypeMethodToolchainSizeMeanRatio
DoubleSortingSortednetcore311059.58 ns2.13
DoubleSortingSortedmaster1030.52 ns1.09
DoubleSortingSortedpr1028.01 ns1.00
DoubleSortingRandomnetcore311077.50 ns1.71
DoubleSortingRandommaster1067.61 ns1.49
DoubleSortingRandompr1045.44 ns1.00
DoubleSortingSortednetcore31100000990,325.98 ns1.27
DoubleSortingSortedmaster1000002,898,290.96 ns3.73
DoubleSortingSortedpr100000777,209.08 ns1.00
DoubleSortingRandomnetcore311000005,940,056.30 ns1.08
DoubleSortingRandommaster1000007,880,560.62 ns1.44
DoubleSortingRandompr1000005,473,335.58 ns1.00
Int32SortingSortednetcore311038.02 ns2.31
Int32SortingSortedmaster1017.09 ns1.04
Int32SortingSortedpr1016.49 ns1.00
Int32SortingRandomnetcore311049.97 ns1.63
Int32SortingRandommaster1031.30 ns1.02
Int32SortingRandompr1030.63 ns1.00
Int32SortingSortednetcore31100000572,908.37 ns0.90
Int32SortingSortedmaster100000640,966.00 ns1.00
Int32SortingSortedpr100000639,118.53 ns1.00
Int32SortingRandomnetcore311000005,072,236.56 ns1.06
Int32SortingRandommaster1000005,024,276.17 ns1.05
Int32SortingRandompr1000004,801,833.82 ns1.00
StringSortingSortednetcore3110575.87 ns1.24
StringSortingSortedmaster10432.40 ns0.93
StringSortingSortedpr10465.86 ns1.00
StringSortingRandomnetcore31101,758.64 ns1.15
StringSortingRandommaster101,425.75 ns0.93
StringSortingRandompr101,532.01 ns1.00
StringSortingSortednetcore3110000083,774,554.44 ns1.14
StringSortingSortedmaster10000074,485,247.25 ns1.01
StringSortingSortedpr10000073,450,518.68 ns1.00
StringSortingRandomnetcore31100000103,108,672.31 ns1.14
StringSortingRandommaster10000089,373,058.33 ns0.99
StringSortingRandompr10000090,577,543.59 ns1.00

@stephentoubstephentoub added this to the 5.0.0 milestone Jun 16, 2020

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

Nice!

@stephentoub
stephentoubforce-pushed the fixsortperf_utilslessthanref branch from 046f85c to 07dcfe1CompareJune 17, 2020 02:27
@stephentoub
stephentoubforce-pushed the fixsortperf_utilslessthanref branch from 07dcfe1 to 04e1399CompareJune 17, 2020 09:43
@nietras

Copy link
Copy Markdown
Contributor

@stephentoub perhaps consider testing performance of following type too:

publicclassComparableClassInt32:IComparable<ComparableClassInt32>{publicreadonlyintValue;publicComparableClassInt32(intvalue)=>Value=value;publicintCompareTo(ComparableClassInt32other)=>Value.CompareTo(other.Value);}

basic reference type overhead. Since string compares are "slow" you won't see possible reference type regressions for this. Just a suggestion :)

@stephentoub

Copy link
Copy Markdown
MemberAuthor

Just a suggestion :)

A knowing smile? 😉 Yes, this case regresses. It appears to be due to dictionary lookups when calling LessThan/GreaterThan, which also prevent inlining. Evaluating options...

@nietras

Copy link
Copy Markdown
Contributor

knowing smile? 😉

Ha yeah 😉 Back in 2018 I went through all "stages of inlining": huh?, wuhuu! (AggressiveInlining), doh! (reference type regression), oh come on! (JIT-me-not issues) 😅

Looking forward to what you come up with. 😀

@stephentoubstephentoub added the NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) label Jun 18, 2020
@stephentoub

Copy link
Copy Markdown
MemberAuthor

#38229 will hopefully be the solution here.

@nietras

Copy link
Copy Markdown
Contributor

Nice 👍 Now if #35791/#10048 were resolved too, we would have something to talk about 😉 Happy to help with that if I could get some pointers 😀

@AndyAyersMS

Copy link
Copy Markdown
Member

Happy to help with that if I could get some pointers

I'm certainly open to reconsidering #10048. The changes are simple enough, though I might end up restricting it to AggressiveInlining callees since the jit underestimates the size impact of a delegate invoke.

But I still don't have a clear picture of how allowing this actually provides benefit -- it sounds like you think with this we can unify some code and either make it perform better or at least not lose any perf. So perhaps a benchmark along these lines would be instructive?

@stephentoub

Copy link
Copy Markdown
MemberAuthor

it sounds like you think with this we can unify some code and either make it perform better or at least not lose any perf.

Not to put words in @nietras mouth, but I expect what he's hoping to do is improve the Array.Sort(..., IComparer<T>) code paths. Today we have one sort implementation that covers both providing an IComparer<T> and a Comparison<T> (a delegate); the former is implemented by creating a delegate to its Compare method, which means Array.Sort(..., IComparer<T>) is allocating. On top of that, we just recently added a span-based sort, and it's currently defined with a generic TComparer : IComparer<T> comparer, with the idea that you could provide a struct-based comparer and it wouldn't allocate... unfortunately, it's the worst of all worlds right now, in that we box the TComparer into an IComparer<T>, and then create a delegate from its Compare method. If all of these code quality issues got sorted out, you could imagine we just had a single code path for TComparer that was written generically to use its Compare sans boxing, and then if you used the Comparison<T> overload, we'd create a struct-based TComparer that just wrapped that Comparison<T> to invoke it. If we everything worked out really well, it could potentially even be unified with the separate, duplicate code path we currently have that targets T : IComparable<T>, using a TComparer comparer struct that delegated to the T : IComparable<T> implementation.

So perhaps a benchmark along these lines would be instructive?

Sounds like a good thing to add to dotnet/performance.

@nietras

nietras commented Jun 23, 2020

Copy link
Copy Markdown
Contributor

If all of these code quality issues got sorted out, you could imagine we just had a single code path for TComparer that was written generically to use its Compare sans boxing, and then if you used the Comparison overload, we'd create a struct-based TComparer that just wrapped that Comparison to invoke it. If we everything worked out really well, it could potentially even be unified with the separate, duplicate code path we currently have that targets T : IComparable, using a TComparer comparer struct that delegated to the T : IComparable implementation.

@stephentoub exactly. :) Although, I am not sure we could unify on TComparer as such, but potentially yes. This would be the fulfillment of the API I have proposed and the on/off work I have been doing on this for the last 3 years... failing due to the many issues around inlining. At the very minimum we could replace the Comparison<T> path with the TComparer path without duplicating code.

Note this is not just about sorting. Sorting, however, for me is a good example of where .NET comes short. I use the value type as a inlineable "functor" pattern for data processing algorithms. Think loops over millions of elements. Unfortunately, we can't unify on this pattern due to these kinds of issues, which Sort exemplifies very well. Lots of other devs use this pattern, but we often end up having to duplicate code, and since this is code that is combinatorial on rank, different kinds of transformations etc. it adds up. It's a lot of replicated code. I have talked about this before and don't want to sound like a broken record player 😅

with the idea that you could provide a struct-based comparer and it wouldn't allocate...

It's not just the allocation. It's so the compare can be inlined. So the "functor" can be applied inlined. To yield a customized loop for performance. As you probably know.

perhaps a benchmark along these lines would be instructive?

@AndyAyersMS I don't know what kind of benchmark you are thinking about but something simple like below shows the issue.

usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;usingSystem;usingSystem.Collections.Generic;usingSystem.Runtime.CompilerServices;namespaceCompareBenchmarking{publicclassProgram{staticvoidMain(string[]args)=>BenchmarkSwitcher.FromAssemblies(new[]{typeof(Program).Assembly}).Run(args);}publicclassCompareFloat:Compare<float>{protectedoverridefloatGetNext()=>_random.Next();}publicclassCompareInt32:Compare<int>{protectedoverrideintGetNext()=>_random.Next();}publicstructComparisonComparer<T>:IComparer<T>{readonlyComparison<T>_comparison;publicComparisonComparer(Comparison<T>comparison)=>_comparison=comparison;[MethodImpl(MethodImplOptions.AggressiveInlining)]publicintCompare(Tx,Ty)=>_comparison(x,y);}[MemoryDiagnoser][DisassemblyDiagnoser]publicabstractclassCompare<T>whereT:IComparable<T>{staticreadonlyComparer<T>_comparer=Comparer<T>.Default;staticreadonlyComparison<T>_comparison=Comparer<T>.Default.Compare;readonlyComparisonComparer<T>_comparisonComparer=newComparisonComparer<T>(_comparison);protectedRandom_random;T_x;T_y;protectedabstractTGetNext();[GlobalSetup]publicvoidSetup(){_random=newRandom(42);_x=GetNext();_y=GetNext();}[Benchmark]publicintCompareTo()=>_x.CompareTo(_y);[Benchmark]publicintComparer()=>_comparer.Compare(_x,_y);[Benchmark(Baseline=true)]publicintComparison()=>_comparison(_x,_y);[Benchmark]publicintComparisonComparer()=>_comparisonComparer.Compare(_x,_y);}}

With the following results on .NET 5.0 Preview 2. The factor of 2.31x pretty much says it all although that would be pretty self-evident given the extra indirection and code generation issues. Just imagine this in a tight loop. :)

BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19041.329 (2004/?/20H1)
Intel Core i7-8700 CPU 3.20GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores
.NET Core SDK=5.0.100-preview.2.20176.6
[Host] : .NET Core 5.0.0 (CoreCLR 5.0.20.16006, CoreFX 5.0.20.16006), X64 RyuJIT
DefaultJob : .NET Core 5.0.0 (CoreCLR 5.0.20.16006, CoreFX 5.0.20.16006), X64 RyuJIT

CompareInt32

MethodMeanErrorStdDevRatio
CompareTo0.4995 ns0.0051 ns0.0048 ns0.38
Comparer1.1854 ns0.0032 ns0.0028 ns0.89
Comparison1.3258 ns0.0065 ns0.0054 ns1.00
ComparisonComparer3.0613 ns0.0096 ns0.0089 ns2.31

CompareFloat

MethodMeanErrorStdDevRatioRatioSD
CompareTo1.237 ns0.0018 ns0.0015 ns0.600.00
Comparer1.905 ns0.0628 ns0.0557 ns0.930.03
Comparison2.060 ns0.0047 ns0.0039 ns1.000.00
ComparisonComparer3.479 ns0.0096 ns0.0090 ns1.690.01

If this is interesting I can make a PR to the benchmark repo.

@stephentoub
stephentoubforce-pushed the fixsortperf_utilslessthanref branch from 04e1399 to 356828cCompareJune 25, 2020 22:27
@stephentoub
stephentoubforce-pushed the fixsortperf_utilslessthanref branch from 356828c to a3e3d79CompareJune 26, 2020 21:34
AndyAyersMS added a commit to AndyAyersMS/runtime that referenced this pull request Jun 26, 2020
RyuJit would not inline methods that contained delegate invokes. Remove
this limitation.
Closesdotnet#10048. See also dotnet#37941.
jkotas pushed a commit that referenced this pull request Jun 27, 2020
RyuJit would not inline methods that contained delegate invokes. Remove
this limitation.
Closes#10048. See also #37941.
Several months back we moved the sorting logic for primtive types out of native code into managed. Doing so helped to make the logic reusable for spans and helped to reduce GC latency, and also actually helped with throughput in a variety of cases. But it ended up regressing throughput for sorting larger arrays of floating-point values, with float/double.CompareTo not getting inlined, and even if it were inlined, containing much more logic than was present in the native implementation. The native implementation did a pre-pass to move all NaNs to the front and then just used simple < and > comparison operations, so the managed implementation now does as well.
@stephentoub
stephentoubforce-pushed the fixsortperf_utilslessthanref branch from a3e3d79 to 2bec1bfCompareJune 27, 2020 23:57
@stephentoub

Copy link
Copy Markdown
MemberAuthor

I moved the comparer functions into the two generic helper classes, and with #38229 (thanks, @jkotas), the reference-type-Int32-wrapper case is good again, with everything else being appx what it was before as well.

I'll merge when this is green.

@stephentoub
stephentoub merged commit e1c9ab4 into dotnet:masterJun 28, 2020
@stephentoub
stephentoub deleted the fixsortperf_utilslessthanref branch June 28, 2020 01:57
@nietras

Copy link
Copy Markdown
Contributor

@stephentoub should I try make a PR for the TComparer change? Is there interest in getting this in? The change itself to TComparer is easy enough, it's the whole sort helpers creation etc. that needs to change a lot, and to support reference type delegate comparer will likely require a "unsafe" cast of Comparison<T> to Comparison<object>. Is that acceptable?

Also thank you for the mention in your awesome Performance Improvements in .NET 5 :)

@jkotas

jkotas commented Jul 16, 2020

Copy link
Copy Markdown
Member

Hi @nietras,

@stephentoub is OOF.

I think it would be a good idea to start the PR for the TComparer changes so that we can start sorting out the code quality issues that it is likely to expose. Preferably, we would fix them instead of working around them by duplicating large amounts of code.

I have opened #39466 to have this tracked. For time line, I do not expect we would be able to get this change into .NET 5.

"unsafe" cast of Comparison<T> to Comparison<object>

Why would that be needed?

I am wondering whether it would make sense to delete the TComparer overloads from the public surface for now until we can get the right implementation for them in place. I think they can stay, but just wanted to bring it up.

cc @eiriktsarpalis

@ghostghost locked as resolved and limited conversation to collaborators Dec 8, 2020
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.RuntimeNO-MERGEThe PR is not ready for merge yet (see discussion for detailed reasons)tenet-performancePerformance related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants

@stephentoub@nietras@AndyAyersMS@jkotas@EgorBo@GrabYourPitchforks@danmoseley@tannergooding
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix regression in Array.Sort for floats/doubles - #37941

Merged
stephentoub merged 3 commits into
dotnet:masterfrom
stephentoub:fixsortperf_utilslessthanref
Jun 28, 2020
Merged

Fix regression in Array.Sort for floats/doubles#37941
stephentoub merged 3 commits into
dotnet:masterfrom
stephentoub:fixsortperf_utilslessthanref

Conversation

@stephentoub

@stephentoubstephentoub commented Jun 16, 2020

Copy link
Copy Markdown
Member

Several months back we moved the sorting logic for primtive types out of native code into managed. Doing so helped to make the logic reusable for spans and helped to reduce GC latency, and also actually helped with throughput in a variety of cases. But it ended up regressing throughput for sorting larger arrays of floating-point values, with float/double.CompareTo not getting inlined, and even if it were inlined, containing much more logic than was present in the native implementation. The native implementation did a pre-pass to move all NaNs to the front and then just used simple < and > comparison operations, so the managed implementation now does as well.

With the exception of a large array of already-sorted Int32 values where there is still a small regression after this PR, all of the cases I've tested are either as good or better than .NET Core 3.1.

@jkotas, @GrabYourPitchforks, @tannergooding, thanks for your offline suggestions on approaches here; I tried out a variety of them, including vectorized float/double.CompareTo as well as unsafe casts to wrapper types with customized IComparable implementations, and this ended up being the best overall. Thanks as well to @nietras for pointing out the regression.

Benchmark:

usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;usingSystem;usingSystem.Diagnostics.CodeAnalysis;usingSystem.Linq;[MemoryDiagnoser]publicclassProgram{staticvoidMain(string[]args)=>BenchmarkSwitcher.FromAssemblies(new[]{typeof(Program).Assembly}).Run(args);}publicclassDoubleSorting:Sorting<double>{protectedoverridedoubleGetNext()=>_random.Next();}publicclassInt32Sorting:Sorting<int>{protectedoverrideintGetNext()=>_random.Next();}publicclassStringSorting:Sorting<string>{protectedoverridestringGetNext()=>string.Create(_random.Next(1,5),_random,(dest,r)=>{for(inti=0;i<dest.Length;i++)dest[i]=(char)('a'+r.Next(26));});}publicabstractclassSorting<T>{protectedRandom_random;privateT[]_orig,_array;[Params(10,100_000)]publicintSize{get;set;}protectedabstractTGetNext();[GlobalSetup]publicvoidSetup(){_random=newRandom(42);_orig=Enumerable.Range(0,Size).Select(_ =>GetNext()).ToArray();_array=(T[])_orig.Clone();Array.Sort(_array);}[Benchmark]publicvoidSorted()=>Array.Sort(_array);[Benchmark]publicvoidRandom(){_orig.AsSpan().CopyTo(_array);Array.Sort(_array);}}
TypeMethodToolchainSizeMeanRatio
DoubleSortingSortednetcore311059.58 ns2.13
DoubleSortingSortedmaster1030.52 ns1.09
DoubleSortingSortedpr1028.01 ns1.00
DoubleSortingRandomnetcore311077.50 ns1.71
DoubleSortingRandommaster1067.61 ns1.49
DoubleSortingRandompr1045.44 ns1.00
DoubleSortingSortednetcore31100000990,325.98 ns1.27
DoubleSortingSortedmaster1000002,898,290.96 ns3.73
DoubleSortingSortedpr100000777,209.08 ns1.00
DoubleSortingRandomnetcore311000005,940,056.30 ns1.08
DoubleSortingRandommaster1000007,880,560.62 ns1.44
DoubleSortingRandompr1000005,473,335.58 ns1.00
Int32SortingSortednetcore311038.02 ns2.31
Int32SortingSortedmaster1017.09 ns1.04
Int32SortingSortedpr1016.49 ns1.00
Int32SortingRandomnetcore311049.97 ns1.63
Int32SortingRandommaster1031.30 ns1.02
Int32SortingRandompr1030.63 ns1.00
Int32SortingSortednetcore31100000572,908.37 ns0.90
Int32SortingSortedmaster100000640,966.00 ns1.00
Int32SortingSortedpr100000639,118.53 ns1.00
Int32SortingRandomnetcore311000005,072,236.56 ns1.06
Int32SortingRandommaster1000005,024,276.17 ns1.05
Int32SortingRandompr1000004,801,833.82 ns1.00
StringSortingSortednetcore3110575.87 ns1.24
StringSortingSortedmaster10432.40 ns0.93
StringSortingSortedpr10465.86 ns1.00
StringSortingRandomnetcore31101,758.64 ns1.15
StringSortingRandommaster101,425.75 ns0.93
StringSortingRandompr101,532.01 ns1.00
StringSortingSortednetcore3110000083,774,554.44 ns1.14
StringSortingSortedmaster10000074,485,247.25 ns1.01
StringSortingSortedpr10000073,450,518.68 ns1.00
StringSortingRandomnetcore31100000103,108,672.31 ns1.14
StringSortingRandommaster10000089,373,058.33 ns0.99
StringSortingRandompr10000090,577,543.59 ns1.00

@stephentoubstephentoub added this to the 5.0.0 milestone Jun 16, 2020

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

Nice!

@stephentoub
stephentoubforce-pushed the fixsortperf_utilslessthanref branch from 046f85c to 07dcfe1CompareJune 17, 2020 02:27
@stephentoub
stephentoubforce-pushed the fixsortperf_utilslessthanref branch from 07dcfe1 to 04e1399CompareJune 17, 2020 09:43
@nietras

Copy link
Copy Markdown
Contributor

@stephentoub perhaps consider testing performance of following type too:

publicclassComparableClassInt32:IComparable<ComparableClassInt32>{publicreadonlyintValue;publicComparableClassInt32(intvalue)=>Value=value;publicintCompareTo(ComparableClassInt32other)=>Value.CompareTo(other.Value);}

basic reference type overhead. Since string compares are "slow" you won't see possible reference type regressions for this. Just a suggestion :)

@stephentoub

Copy link
Copy Markdown
MemberAuthor

Just a suggestion :)

A knowing smile? 😉 Yes, this case regresses. It appears to be due to dictionary lookups when calling LessThan/GreaterThan, which also prevent inlining. Evaluating options...

@nietras

Copy link
Copy Markdown
Contributor

knowing smile? 😉

Ha yeah 😉 Back in 2018 I went through all "stages of inlining": huh?, wuhuu! (AggressiveInlining), doh! (reference type regression), oh come on! (JIT-me-not issues) 😅

Looking forward to what you come up with. 😀

@stephentoubstephentoub added the NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) label Jun 18, 2020
@stephentoub

Copy link
Copy Markdown
MemberAuthor

#38229 will hopefully be the solution here.

@nietras

Copy link
Copy Markdown
Contributor

Nice 👍 Now if #35791/#10048 were resolved too, we would have something to talk about 😉 Happy to help with that if I could get some pointers 😀

@AndyAyersMS

Copy link
Copy Markdown
Member

Happy to help with that if I could get some pointers

I'm certainly open to reconsidering #10048. The changes are simple enough, though I might end up restricting it to AggressiveInlining callees since the jit underestimates the size impact of a delegate invoke.

But I still don't have a clear picture of how allowing this actually provides benefit -- it sounds like you think with this we can unify some code and either make it perform better or at least not lose any perf. So perhaps a benchmark along these lines would be instructive?

@stephentoub

Copy link
Copy Markdown
MemberAuthor

it sounds like you think with this we can unify some code and either make it perform better or at least not lose any perf.

Not to put words in @nietras mouth, but I expect what he's hoping to do is improve the Array.Sort(..., IComparer<T>) code paths. Today we have one sort implementation that covers both providing an IComparer<T> and a Comparison<T> (a delegate); the former is implemented by creating a delegate to its Compare method, which means Array.Sort(..., IComparer<T>) is allocating. On top of that, we just recently added a span-based sort, and it's currently defined with a generic TComparer : IComparer<T> comparer, with the idea that you could provide a struct-based comparer and it wouldn't allocate... unfortunately, it's the worst of all worlds right now, in that we box the TComparer into an IComparer<T>, and then create a delegate from its Compare method. If all of these code quality issues got sorted out, you could imagine we just had a single code path for TComparer that was written generically to use its Compare sans boxing, and then if you used the Comparison<T> overload, we'd create a struct-based TComparer that just wrapped that Comparison<T> to invoke it. If we everything worked out really well, it could potentially even be unified with the separate, duplicate code path we currently have that targets T : IComparable<T>, using a TComparer comparer struct that delegated to the T : IComparable<T> implementation.

So perhaps a benchmark along these lines would be instructive?

Sounds like a good thing to add to dotnet/performance.

@nietras

nietras commented Jun 23, 2020

Copy link
Copy Markdown
Contributor

If all of these code quality issues got sorted out, you could imagine we just had a single code path for TComparer that was written generically to use its Compare sans boxing, and then if you used the Comparison overload, we'd create a struct-based TComparer that just wrapped that Comparison to invoke it. If we everything worked out really well, it could potentially even be unified with the separate, duplicate code path we currently have that targets T : IComparable, using a TComparer comparer struct that delegated to the T : IComparable implementation.

@stephentoub exactly. :) Although, I am not sure we could unify on TComparer as such, but potentially yes. This would be the fulfillment of the API I have proposed and the on/off work I have been doing on this for the last 3 years... failing due to the many issues around inlining. At the very minimum we could replace the Comparison<T> path with the TComparer path without duplicating code.

Note this is not just about sorting. Sorting, however, for me is a good example of where .NET comes short. I use the value type as a inlineable "functor" pattern for data processing algorithms. Think loops over millions of elements. Unfortunately, we can't unify on this pattern due to these kinds of issues, which Sort exemplifies very well. Lots of other devs use this pattern, but we often end up having to duplicate code, and since this is code that is combinatorial on rank, different kinds of transformations etc. it adds up. It's a lot of replicated code. I have talked about this before and don't want to sound like a broken record player 😅

with the idea that you could provide a struct-based comparer and it wouldn't allocate...

It's not just the allocation. It's so the compare can be inlined. So the "functor" can be applied inlined. To yield a customized loop for performance. As you probably know.

perhaps a benchmark along these lines would be instructive?

@AndyAyersMS I don't know what kind of benchmark you are thinking about but something simple like below shows the issue.

usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;usingSystem;usingSystem.Collections.Generic;usingSystem.Runtime.CompilerServices;namespaceCompareBenchmarking{publicclassProgram{staticvoidMain(string[]args)=>BenchmarkSwitcher.FromAssemblies(new[]{typeof(Program).Assembly}).Run(args);}publicclassCompareFloat:Compare<float>{protectedoverridefloatGetNext()=>_random.Next();}publicclassCompareInt32:Compare<int>{protectedoverrideintGetNext()=>_random.Next();}publicstructComparisonComparer<T>:IComparer<T>{readonlyComparison<T>_comparison;publicComparisonComparer(Comparison<T>comparison)=>_comparison=comparison;[MethodImpl(MethodImplOptions.AggressiveInlining)]publicintCompare(Tx,Ty)=>_comparison(x,y);}[MemoryDiagnoser][DisassemblyDiagnoser]publicabstractclassCompare<T>whereT:IComparable<T>{staticreadonlyComparer<T>_comparer=Comparer<T>.Default;staticreadonlyComparison<T>_comparison=Comparer<T>.Default.Compare;readonlyComparisonComparer<T>_comparisonComparer=newComparisonComparer<T>(_comparison);protectedRandom_random;T_x;T_y;protectedabstractTGetNext();[GlobalSetup]publicvoidSetup(){_random=newRandom(42);_x=GetNext();_y=GetNext();}[Benchmark]publicintCompareTo()=>_x.CompareTo(_y);[Benchmark]publicintComparer()=>_comparer.Compare(_x,_y);[Benchmark(Baseline=true)]publicintComparison()=>_comparison(_x,_y);[Benchmark]publicintComparisonComparer()=>_comparisonComparer.Compare(_x,_y);}}

With the following results on .NET 5.0 Preview 2. The factor of 2.31x pretty much says it all although that would be pretty self-evident given the extra indirection and code generation issues. Just imagine this in a tight loop. :)

BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19041.329 (2004/?/20H1)
Intel Core i7-8700 CPU 3.20GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores
.NET Core SDK=5.0.100-preview.2.20176.6
[Host] : .NET Core 5.0.0 (CoreCLR 5.0.20.16006, CoreFX 5.0.20.16006), X64 RyuJIT
DefaultJob : .NET Core 5.0.0 (CoreCLR 5.0.20.16006, CoreFX 5.0.20.16006), X64 RyuJIT

CompareInt32

MethodMeanErrorStdDevRatio
CompareTo0.4995 ns0.0051 ns0.0048 ns0.38
Comparer1.1854 ns0.0032 ns0.0028 ns0.89
Comparison1.3258 ns0.0065 ns0.0054 ns1.00
ComparisonComparer3.0613 ns0.0096 ns0.0089 ns2.31

CompareFloat

MethodMeanErrorStdDevRatioRatioSD
CompareTo1.237 ns0.0018 ns0.0015 ns0.600.00
Comparer1.905 ns0.0628 ns0.0557 ns0.930.03
Comparison2.060 ns0.0047 ns0.0039 ns1.000.00
ComparisonComparer3.479 ns0.0096 ns0.0090 ns1.690.01

If this is interesting I can make a PR to the benchmark repo.

@stephentoub
stephentoubforce-pushed the fixsortperf_utilslessthanref branch from 04e1399 to 356828cCompareJune 25, 2020 22:27
@stephentoub
stephentoubforce-pushed the fixsortperf_utilslessthanref branch from 356828c to a3e3d79CompareJune 26, 2020 21:34
AndyAyersMS added a commit to AndyAyersMS/runtime that referenced this pull request Jun 26, 2020
RyuJit would not inline methods that contained delegate invokes. Remove
this limitation.
Closesdotnet#10048. See also dotnet#37941.
jkotas pushed a commit that referenced this pull request Jun 27, 2020
RyuJit would not inline methods that contained delegate invokes. Remove
this limitation.
Closes#10048. See also #37941.
Several months back we moved the sorting logic for primtive types out of native code into managed. Doing so helped to make the logic reusable for spans and helped to reduce GC latency, and also actually helped with throughput in a variety of cases. But it ended up regressing throughput for sorting larger arrays of floating-point values, with float/double.CompareTo not getting inlined, and even if it were inlined, containing much more logic than was present in the native implementation. The native implementation did a pre-pass to move all NaNs to the front and then just used simple < and > comparison operations, so the managed implementation now does as well.
@stephentoub
stephentoubforce-pushed the fixsortperf_utilslessthanref branch from a3e3d79 to 2bec1bfCompareJune 27, 2020 23:57
@stephentoub

Copy link
Copy Markdown
MemberAuthor

I moved the comparer functions into the two generic helper classes, and with #38229 (thanks, @jkotas), the reference-type-Int32-wrapper case is good again, with everything else being appx what it was before as well.

I'll merge when this is green.

@stephentoub
stephentoub merged commit e1c9ab4 into dotnet:masterJun 28, 2020
@stephentoub
stephentoub deleted the fixsortperf_utilslessthanref branch June 28, 2020 01:57
@nietras

Copy link
Copy Markdown
Contributor

@stephentoub should I try make a PR for the TComparer change? Is there interest in getting this in? The change itself to TComparer is easy enough, it's the whole sort helpers creation etc. that needs to change a lot, and to support reference type delegate comparer will likely require a "unsafe" cast of Comparison<T> to Comparison<object>. Is that acceptable?

Also thank you for the mention in your awesome Performance Improvements in .NET 5 :)

@jkotas

jkotas commented Jul 16, 2020

Copy link
Copy Markdown
Member

Hi @nietras,

@stephentoub is OOF.

I think it would be a good idea to start the PR for the TComparer changes so that we can start sorting out the code quality issues that it is likely to expose. Preferably, we would fix them instead of working around them by duplicating large amounts of code.

I have opened #39466 to have this tracked. For time line, I do not expect we would be able to get this change into .NET 5.

"unsafe" cast of Comparison<T> to Comparison<object>

Why would that be needed?

I am wondering whether it would make sense to delete the TComparer overloads from the public surface for now until we can get the right implementation for them in place. I think they can stay, but just wanted to bring it up.

cc @eiriktsarpalis

@ghostghost locked as resolved and limited conversation to collaborators Dec 8, 2020
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.RuntimeNO-MERGEThe PR is not ready for merge yet (see discussion for detailed reasons)tenet-performancePerformance related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants

@stephentoub@nietras@AndyAyersMS@jkotas@EgorBo@GrabYourPitchforks@danmoseley@tannergooding
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix regression in Array.Sort for floats/doubles - #37941

Merged
stephentoub merged 3 commits into
dotnet:masterfrom
stephentoub:fixsortperf_utilslessthanref
Jun 28, 2020
Merged

Fix regression in Array.Sort for floats/doubles#37941
stephentoub merged 3 commits into
dotnet:masterfrom
stephentoub:fixsortperf_utilslessthanref

Conversation

@stephentoub

@stephentoubstephentoub commented Jun 16, 2020

Copy link
Copy Markdown
Member

Several months back we moved the sorting logic for primtive types out of native code into managed. Doing so helped to make the logic reusable for spans and helped to reduce GC latency, and also actually helped with throughput in a variety of cases. But it ended up regressing throughput for sorting larger arrays of floating-point values, with float/double.CompareTo not getting inlined, and even if it were inlined, containing much more logic than was present in the native implementation. The native implementation did a pre-pass to move all NaNs to the front and then just used simple < and > comparison operations, so the managed implementation now does as well.

With the exception of a large array of already-sorted Int32 values where there is still a small regression after this PR, all of the cases I've tested are either as good or better than .NET Core 3.1.

@jkotas, @GrabYourPitchforks, @tannergooding, thanks for your offline suggestions on approaches here; I tried out a variety of them, including vectorized float/double.CompareTo as well as unsafe casts to wrapper types with customized IComparable implementations, and this ended up being the best overall. Thanks as well to @nietras for pointing out the regression.

Benchmark:

usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;usingSystem;usingSystem.Diagnostics.CodeAnalysis;usingSystem.Linq;[MemoryDiagnoser]publicclassProgram{staticvoidMain(string[]args)=>BenchmarkSwitcher.FromAssemblies(new[]{typeof(Program).Assembly}).Run(args);}publicclassDoubleSorting:Sorting<double>{protectedoverridedoubleGetNext()=>_random.Next();}publicclassInt32Sorting:Sorting<int>{protectedoverrideintGetNext()=>_random.Next();}publicclassStringSorting:Sorting<string>{protectedoverridestringGetNext()=>string.Create(_random.Next(1,5),_random,(dest,r)=>{for(inti=0;i<dest.Length;i++)dest[i]=(char)('a'+r.Next(26));});}publicabstractclassSorting<T>{protectedRandom_random;privateT[]_orig,_array;[Params(10,100_000)]publicintSize{get;set;}protectedabstractTGetNext();[GlobalSetup]publicvoidSetup(){_random=newRandom(42);_orig=Enumerable.Range(0,Size).Select(_ =>GetNext()).ToArray();_array=(T[])_orig.Clone();Array.Sort(_array);}[Benchmark]publicvoidSorted()=>Array.Sort(_array);[Benchmark]publicvoidRandom(){_orig.AsSpan().CopyTo(_array);Array.Sort(_array);}}
TypeMethodToolchainSizeMeanRatio
DoubleSortingSortednetcore311059.58 ns2.13
DoubleSortingSortedmaster1030.52 ns1.09
DoubleSortingSortedpr1028.01 ns1.00
DoubleSortingRandomnetcore311077.50 ns1.71
DoubleSortingRandommaster1067.61 ns1.49
DoubleSortingRandompr1045.44 ns1.00
DoubleSortingSortednetcore31100000990,325.98 ns1.27
DoubleSortingSortedmaster1000002,898,290.96 ns3.73
DoubleSortingSortedpr100000777,209.08 ns1.00
DoubleSortingRandomnetcore311000005,940,056.30 ns1.08
DoubleSortingRandommaster1000007,880,560.62 ns1.44
DoubleSortingRandompr1000005,473,335.58 ns1.00
Int32SortingSortednetcore311038.02 ns2.31
Int32SortingSortedmaster1017.09 ns1.04
Int32SortingSortedpr1016.49 ns1.00
Int32SortingRandomnetcore311049.97 ns1.63
Int32SortingRandommaster1031.30 ns1.02
Int32SortingRandompr1030.63 ns1.00
Int32SortingSortednetcore31100000572,908.37 ns0.90
Int32SortingSortedmaster100000640,966.00 ns1.00
Int32SortingSortedpr100000639,118.53 ns1.00
Int32SortingRandomnetcore311000005,072,236.56 ns1.06
Int32SortingRandommaster1000005,024,276.17 ns1.05
Int32SortingRandompr1000004,801,833.82 ns1.00
StringSortingSortednetcore3110575.87 ns1.24
StringSortingSortedmaster10432.40 ns0.93
StringSortingSortedpr10465.86 ns1.00
StringSortingRandomnetcore31101,758.64 ns1.15
StringSortingRandommaster101,425.75 ns0.93
StringSortingRandompr101,532.01 ns1.00
StringSortingSortednetcore3110000083,774,554.44 ns1.14
StringSortingSortedmaster10000074,485,247.25 ns1.01
StringSortingSortedpr10000073,450,518.68 ns1.00
StringSortingRandomnetcore31100000103,108,672.31 ns1.14
StringSortingRandommaster10000089,373,058.33 ns0.99
StringSortingRandompr10000090,577,543.59 ns1.00

@stephentoubstephentoub added this to the 5.0.0 milestone Jun 16, 2020

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

Nice!

@stephentoub
stephentoubforce-pushed the fixsortperf_utilslessthanref branch from 046f85c to 07dcfe1CompareJune 17, 2020 02:27
@stephentoub
stephentoubforce-pushed the fixsortperf_utilslessthanref branch from 07dcfe1 to 04e1399CompareJune 17, 2020 09:43
@nietras

Copy link
Copy Markdown
Contributor

@stephentoub perhaps consider testing performance of following type too:

publicclassComparableClassInt32:IComparable<ComparableClassInt32>{publicreadonlyintValue;publicComparableClassInt32(intvalue)=>Value=value;publicintCompareTo(ComparableClassInt32other)=>Value.CompareTo(other.Value);}

basic reference type overhead. Since string compares are "slow" you won't see possible reference type regressions for this. Just a suggestion :)

@stephentoub

Copy link
Copy Markdown
MemberAuthor

Just a suggestion :)

A knowing smile? 😉 Yes, this case regresses. It appears to be due to dictionary lookups when calling LessThan/GreaterThan, which also prevent inlining. Evaluating options...

@nietras

Copy link
Copy Markdown
Contributor

knowing smile? 😉

Ha yeah 😉 Back in 2018 I went through all "stages of inlining": huh?, wuhuu! (AggressiveInlining), doh! (reference type regression), oh come on! (JIT-me-not issues) 😅

Looking forward to what you come up with. 😀

@stephentoubstephentoub added the NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) label Jun 18, 2020
@stephentoub

Copy link
Copy Markdown
MemberAuthor

#38229 will hopefully be the solution here.

@nietras

Copy link
Copy Markdown
Contributor

Nice 👍 Now if #35791/#10048 were resolved too, we would have something to talk about 😉 Happy to help with that if I could get some pointers 😀

@AndyAyersMS

Copy link
Copy Markdown
Member

Happy to help with that if I could get some pointers

I'm certainly open to reconsidering #10048. The changes are simple enough, though I might end up restricting it to AggressiveInlining callees since the jit underestimates the size impact of a delegate invoke.

But I still don't have a clear picture of how allowing this actually provides benefit -- it sounds like you think with this we can unify some code and either make it perform better or at least not lose any perf. So perhaps a benchmark along these lines would be instructive?

@stephentoub

Copy link
Copy Markdown
MemberAuthor

it sounds like you think with this we can unify some code and either make it perform better or at least not lose any perf.

Not to put words in @nietras mouth, but I expect what he's hoping to do is improve the Array.Sort(..., IComparer<T>) code paths. Today we have one sort implementation that covers both providing an IComparer<T> and a Comparison<T> (a delegate); the former is implemented by creating a delegate to its Compare method, which means Array.Sort(..., IComparer<T>) is allocating. On top of that, we just recently added a span-based sort, and it's currently defined with a generic TComparer : IComparer<T> comparer, with the idea that you could provide a struct-based comparer and it wouldn't allocate... unfortunately, it's the worst of all worlds right now, in that we box the TComparer into an IComparer<T>, and then create a delegate from its Compare method. If all of these code quality issues got sorted out, you could imagine we just had a single code path for TComparer that was written generically to use its Compare sans boxing, and then if you used the Comparison<T> overload, we'd create a struct-based TComparer that just wrapped that Comparison<T> to invoke it. If we everything worked out really well, it could potentially even be unified with the separate, duplicate code path we currently have that targets T : IComparable<T>, using a TComparer comparer struct that delegated to the T : IComparable<T> implementation.

So perhaps a benchmark along these lines would be instructive?

Sounds like a good thing to add to dotnet/performance.

@nietras

nietras commented Jun 23, 2020

Copy link
Copy Markdown
Contributor

If all of these code quality issues got sorted out, you could imagine we just had a single code path for TComparer that was written generically to use its Compare sans boxing, and then if you used the Comparison overload, we'd create a struct-based TComparer that just wrapped that Comparison to invoke it. If we everything worked out really well, it could potentially even be unified with the separate, duplicate code path we currently have that targets T : IComparable, using a TComparer comparer struct that delegated to the T : IComparable implementation.

@stephentoub exactly. :) Although, I am not sure we could unify on TComparer as such, but potentially yes. This would be the fulfillment of the API I have proposed and the on/off work I have been doing on this for the last 3 years... failing due to the many issues around inlining. At the very minimum we could replace the Comparison<T> path with the TComparer path without duplicating code.

Note this is not just about sorting. Sorting, however, for me is a good example of where .NET comes short. I use the value type as a inlineable "functor" pattern for data processing algorithms. Think loops over millions of elements. Unfortunately, we can't unify on this pattern due to these kinds of issues, which Sort exemplifies very well. Lots of other devs use this pattern, but we often end up having to duplicate code, and since this is code that is combinatorial on rank, different kinds of transformations etc. it adds up. It's a lot of replicated code. I have talked about this before and don't want to sound like a broken record player 😅

with the idea that you could provide a struct-based comparer and it wouldn't allocate...

It's not just the allocation. It's so the compare can be inlined. So the "functor" can be applied inlined. To yield a customized loop for performance. As you probably know.

perhaps a benchmark along these lines would be instructive?

@AndyAyersMS I don't know what kind of benchmark you are thinking about but something simple like below shows the issue.

usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;usingSystem;usingSystem.Collections.Generic;usingSystem.Runtime.CompilerServices;namespaceCompareBenchmarking{publicclassProgram{staticvoidMain(string[]args)=>BenchmarkSwitcher.FromAssemblies(new[]{typeof(Program).Assembly}).Run(args);}publicclassCompareFloat:Compare<float>{protectedoverridefloatGetNext()=>_random.Next();}publicclassCompareInt32:Compare<int>{protectedoverrideintGetNext()=>_random.Next();}publicstructComparisonComparer<T>:IComparer<T>{readonlyComparison<T>_comparison;publicComparisonComparer(Comparison<T>comparison)=>_comparison=comparison;[MethodImpl(MethodImplOptions.AggressiveInlining)]publicintCompare(Tx,Ty)=>_comparison(x,y);}[MemoryDiagnoser][DisassemblyDiagnoser]publicabstractclassCompare<T>whereT:IComparable<T>{staticreadonlyComparer<T>_comparer=Comparer<T>.Default;staticreadonlyComparison<T>_comparison=Comparer<T>.Default.Compare;readonlyComparisonComparer<T>_comparisonComparer=newComparisonComparer<T>(_comparison);protectedRandom_random;T_x;T_y;protectedabstractTGetNext();[GlobalSetup]publicvoidSetup(){_random=newRandom(42);_x=GetNext();_y=GetNext();}[Benchmark]publicintCompareTo()=>_x.CompareTo(_y);[Benchmark]publicintComparer()=>_comparer.Compare(_x,_y);[Benchmark(Baseline=true)]publicintComparison()=>_comparison(_x,_y);[Benchmark]publicintComparisonComparer()=>_comparisonComparer.Compare(_x,_y);}}

With the following results on .NET 5.0 Preview 2. The factor of 2.31x pretty much says it all although that would be pretty self-evident given the extra indirection and code generation issues. Just imagine this in a tight loop. :)

BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19041.329 (2004/?/20H1)
Intel Core i7-8700 CPU 3.20GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores
.NET Core SDK=5.0.100-preview.2.20176.6
[Host] : .NET Core 5.0.0 (CoreCLR 5.0.20.16006, CoreFX 5.0.20.16006), X64 RyuJIT
DefaultJob : .NET Core 5.0.0 (CoreCLR 5.0.20.16006, CoreFX 5.0.20.16006), X64 RyuJIT

CompareInt32

MethodMeanErrorStdDevRatio
CompareTo0.4995 ns0.0051 ns0.0048 ns0.38
Comparer1.1854 ns0.0032 ns0.0028 ns0.89
Comparison1.3258 ns0.0065 ns0.0054 ns1.00
ComparisonComparer3.0613 ns0.0096 ns0.0089 ns2.31

CompareFloat

MethodMeanErrorStdDevRatioRatioSD
CompareTo1.237 ns0.0018 ns0.0015 ns0.600.00
Comparer1.905 ns0.0628 ns0.0557 ns0.930.03
Comparison2.060 ns0.0047 ns0.0039 ns1.000.00
ComparisonComparer3.479 ns0.0096 ns0.0090 ns1.690.01

If this is interesting I can make a PR to the benchmark repo.

@stephentoub
stephentoubforce-pushed the fixsortperf_utilslessthanref branch from 04e1399 to 356828cCompareJune 25, 2020 22:27
@stephentoub
stephentoubforce-pushed the fixsortperf_utilslessthanref branch from 356828c to a3e3d79CompareJune 26, 2020 21:34
AndyAyersMS added a commit to AndyAyersMS/runtime that referenced this pull request Jun 26, 2020
RyuJit would not inline methods that contained delegate invokes. Remove
this limitation.
Closesdotnet#10048. See also dotnet#37941.
jkotas pushed a commit that referenced this pull request Jun 27, 2020
RyuJit would not inline methods that contained delegate invokes. Remove
this limitation.
Closes#10048. See also #37941.
Several months back we moved the sorting logic for primtive types out of native code into managed. Doing so helped to make the logic reusable for spans and helped to reduce GC latency, and also actually helped with throughput in a variety of cases. But it ended up regressing throughput for sorting larger arrays of floating-point values, with float/double.CompareTo not getting inlined, and even if it were inlined, containing much more logic than was present in the native implementation. The native implementation did a pre-pass to move all NaNs to the front and then just used simple < and > comparison operations, so the managed implementation now does as well.
@stephentoub
stephentoubforce-pushed the fixsortperf_utilslessthanref branch from a3e3d79 to 2bec1bfCompareJune 27, 2020 23:57
@stephentoub

Copy link
Copy Markdown
MemberAuthor

I moved the comparer functions into the two generic helper classes, and with #38229 (thanks, @jkotas), the reference-type-Int32-wrapper case is good again, with everything else being appx what it was before as well.

I'll merge when this is green.

@stephentoub
stephentoub merged commit e1c9ab4 into dotnet:masterJun 28, 2020
@stephentoub
stephentoub deleted the fixsortperf_utilslessthanref branch June 28, 2020 01:57
@nietras

Copy link
Copy Markdown
Contributor

@stephentoub should I try make a PR for the TComparer change? Is there interest in getting this in? The change itself to TComparer is easy enough, it's the whole sort helpers creation etc. that needs to change a lot, and to support reference type delegate comparer will likely require a "unsafe" cast of Comparison<T> to Comparison<object>. Is that acceptable?

Also thank you for the mention in your awesome Performance Improvements in .NET 5 :)

@jkotas

jkotas commented Jul 16, 2020

Copy link
Copy Markdown
Member

Hi @nietras,

@stephentoub is OOF.

I think it would be a good idea to start the PR for the TComparer changes so that we can start sorting out the code quality issues that it is likely to expose. Preferably, we would fix them instead of working around them by duplicating large amounts of code.

I have opened #39466 to have this tracked. For time line, I do not expect we would be able to get this change into .NET 5.

"unsafe" cast of Comparison<T> to Comparison<object>

Why would that be needed?

I am wondering whether it would make sense to delete the TComparer overloads from the public surface for now until we can get the right implementation for them in place. I think they can stay, but just wanted to bring it up.

cc @eiriktsarpalis

@ghostghost locked as resolved and limited conversation to collaborators Dec 8, 2020
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.RuntimeNO-MERGEThe PR is not ready for merge yet (see discussion for detailed reasons)tenet-performancePerformance related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants

@stephentoub@nietras@AndyAyersMS@jkotas@EgorBo@GrabYourPitchforks@danmoseley@tannergooding
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Fix regression in Array.Sort for floats/doubles - #37941

Merged
stephentoub merged 3 commits into
dotnet:masterfrom
stephentoub:fixsortperf_utilslessthanref
Jun 28, 2020
Merged

Fix regression in Array.Sort for floats/doubles#37941
stephentoub merged 3 commits into
dotnet:masterfrom
stephentoub:fixsortperf_utilslessthanref

Conversation

@stephentoub

@stephentoubstephentoub commented Jun 16, 2020

Copy link
Copy Markdown
Member

Several months back we moved the sorting logic for primtive types out of native code into managed. Doing so helped to make the logic reusable for spans and helped to reduce GC latency, and also actually helped with throughput in a variety of cases. But it ended up regressing throughput for sorting larger arrays of floating-point values, with float/double.CompareTo not getting inlined, and even if it were inlined, containing much more logic than was present in the native implementation. The native implementation did a pre-pass to move all NaNs to the front and then just used simple < and > comparison operations, so the managed implementation now does as well.

With the exception of a large array of already-sorted Int32 values where there is still a small regression after this PR, all of the cases I've tested are either as good or better than .NET Core 3.1.

@jkotas, @GrabYourPitchforks, @tannergooding, thanks for your offline suggestions on approaches here; I tried out a variety of them, including vectorized float/double.CompareTo as well as unsafe casts to wrapper types with customized IComparable implementations, and this ended up being the best overall. Thanks as well to @nietras for pointing out the regression.

Benchmark:

usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;usingSystem;usingSystem.Diagnostics.CodeAnalysis;usingSystem.Linq;[MemoryDiagnoser]publicclassProgram{staticvoidMain(string[]args)=>BenchmarkSwitcher.FromAssemblies(new[]{typeof(Program).Assembly}).Run(args);}publicclassDoubleSorting:Sorting<double>{protectedoverridedoubleGetNext()=>_random.Next();}publicclassInt32Sorting:Sorting<int>{protectedoverrideintGetNext()=>_random.Next();}publicclassStringSorting:Sorting<string>{protectedoverridestringGetNext()=>string.Create(_random.Next(1,5),_random,(dest,r)=>{for(inti=0;i<dest.Length;i++)dest[i]=(char)('a'+r.Next(26));});}publicabstractclassSorting<T>{protectedRandom_random;privateT[]_orig,_array;[Params(10,100_000)]publicintSize{get;set;}protectedabstractTGetNext();[GlobalSetup]publicvoidSetup(){_random=newRandom(42);_orig=Enumerable.Range(0,Size).Select(_ =>GetNext()).ToArray();_array=(T[])_orig.Clone();Array.Sort(_array);}[Benchmark]publicvoidSorted()=>Array.Sort(_array);[Benchmark]publicvoidRandom(){_orig.AsSpan().CopyTo(_array);Array.Sort(_array);}}
TypeMethodToolchainSizeMeanRatio
DoubleSortingSortednetcore311059.58 ns2.13
DoubleSortingSortedmaster1030.52 ns1.09
DoubleSortingSortedpr1028.01 ns1.00
DoubleSortingRandomnetcore311077.50 ns1.71
DoubleSortingRandommaster1067.61 ns1.49
DoubleSortingRandompr1045.44 ns1.00
DoubleSortingSortednetcore31100000990,325.98 ns1.27
DoubleSortingSortedmaster1000002,898,290.96 ns3.73
DoubleSortingSortedpr100000777,209.08 ns1.00
DoubleSortingRandomnetcore311000005,940,056.30 ns1.08
DoubleSortingRandommaster1000007,880,560.62 ns1.44
DoubleSortingRandompr1000005,473,335.58 ns1.00
Int32SortingSortednetcore311038.02 ns2.31
Int32SortingSortedmaster1017.09 ns1.04
Int32SortingSortedpr1016.49 ns1.00
Int32SortingRandomnetcore311049.97 ns1.63
Int32SortingRandommaster1031.30 ns1.02
Int32SortingRandompr1030.63 ns1.00
Int32SortingSortednetcore31100000572,908.37 ns0.90
Int32SortingSortedmaster100000640,966.00 ns1.00
Int32SortingSortedpr100000639,118.53 ns1.00
Int32SortingRandomnetcore311000005,072,236.56 ns1.06
Int32SortingRandommaster1000005,024,276.17 ns1.05
Int32SortingRandompr1000004,801,833.82 ns1.00
StringSortingSortednetcore3110575.87 ns1.24
StringSortingSortedmaster10432.40 ns0.93
StringSortingSortedpr10465.86 ns1.00
StringSortingRandomnetcore31101,758.64 ns1.15
StringSortingRandommaster101,425.75 ns0.93
StringSortingRandompr101,532.01 ns1.00
StringSortingSortednetcore3110000083,774,554.44 ns1.14
StringSortingSortedmaster10000074,485,247.25 ns1.01
StringSortingSortedpr10000073,450,518.68 ns1.00
StringSortingRandomnetcore31100000103,108,672.31 ns1.14
StringSortingRandommaster10000089,373,058.33 ns0.99
StringSortingRandompr10000090,577,543.59 ns1.00

@stephentoubstephentoub added this to the 5.0.0 milestone Jun 16, 2020

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

Nice!

@stephentoub
stephentoubforce-pushed the fixsortperf_utilslessthanref branch from 046f85c to 07dcfe1CompareJune 17, 2020 02:27
@stephentoub
stephentoubforce-pushed the fixsortperf_utilslessthanref branch from 07dcfe1 to 04e1399CompareJune 17, 2020 09:43
@nietras

Copy link
Copy Markdown
Contributor

@stephentoub perhaps consider testing performance of following type too:

publicclassComparableClassInt32:IComparable<ComparableClassInt32>{publicreadonlyintValue;publicComparableClassInt32(intvalue)=>Value=value;publicintCompareTo(ComparableClassInt32other)=>Value.CompareTo(other.Value);}

basic reference type overhead. Since string compares are "slow" you won't see possible reference type regressions for this. Just a suggestion :)

@stephentoub

Copy link
Copy Markdown
MemberAuthor

Just a suggestion :)

A knowing smile? 😉 Yes, this case regresses. It appears to be due to dictionary lookups when calling LessThan/GreaterThan, which also prevent inlining. Evaluating options...

@nietras

Copy link
Copy Markdown
Contributor

knowing smile? 😉

Ha yeah 😉 Back in 2018 I went through all "stages of inlining": huh?, wuhuu! (AggressiveInlining), doh! (reference type regression), oh come on! (JIT-me-not issues) 😅

Looking forward to what you come up with. 😀

@stephentoubstephentoub added the NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) label Jun 18, 2020
@stephentoub

Copy link
Copy Markdown
MemberAuthor

#38229 will hopefully be the solution here.

@nietras

Copy link
Copy Markdown
Contributor

Nice 👍 Now if #35791/#10048 were resolved too, we would have something to talk about 😉 Happy to help with that if I could get some pointers 😀

@AndyAyersMS

Copy link
Copy Markdown
Member

Happy to help with that if I could get some pointers

I'm certainly open to reconsidering #10048. The changes are simple enough, though I might end up restricting it to AggressiveInlining callees since the jit underestimates the size impact of a delegate invoke.

But I still don't have a clear picture of how allowing this actually provides benefit -- it sounds like you think with this we can unify some code and either make it perform better or at least not lose any perf. So perhaps a benchmark along these lines would be instructive?

@stephentoub

Copy link
Copy Markdown
MemberAuthor

it sounds like you think with this we can unify some code and either make it perform better or at least not lose any perf.

Not to put words in @nietras mouth, but I expect what he's hoping to do is improve the Array.Sort(..., IComparer<T>) code paths. Today we have one sort implementation that covers both providing an IComparer<T> and a Comparison<T> (a delegate); the former is implemented by creating a delegate to its Compare method, which means Array.Sort(..., IComparer<T>) is allocating. On top of that, we just recently added a span-based sort, and it's currently defined with a generic TComparer : IComparer<T> comparer, with the idea that you could provide a struct-based comparer and it wouldn't allocate... unfortunately, it's the worst of all worlds right now, in that we box the TComparer into an IComparer<T>, and then create a delegate from its Compare method. If all of these code quality issues got sorted out, you could imagine we just had a single code path for TComparer that was written generically to use its Compare sans boxing, and then if you used the Comparison<T> overload, we'd create a struct-based TComparer that just wrapped that Comparison<T> to invoke it. If we everything worked out really well, it could potentially even be unified with the separate, duplicate code path we currently have that targets T : IComparable<T>, using a TComparer comparer struct that delegated to the T : IComparable<T> implementation.

So perhaps a benchmark along these lines would be instructive?

Sounds like a good thing to add to dotnet/performance.

@nietras

nietras commented Jun 23, 2020

Copy link
Copy Markdown
Contributor

If all of these code quality issues got sorted out, you could imagine we just had a single code path for TComparer that was written generically to use its Compare sans boxing, and then if you used the Comparison overload, we'd create a struct-based TComparer that just wrapped that Comparison to invoke it. If we everything worked out really well, it could potentially even be unified with the separate, duplicate code path we currently have that targets T : IComparable, using a TComparer comparer struct that delegated to the T : IComparable implementation.

@stephentoub exactly. :) Although, I am not sure we could unify on TComparer as such, but potentially yes. This would be the fulfillment of the API I have proposed and the on/off work I have been doing on this for the last 3 years... failing due to the many issues around inlining. At the very minimum we could replace the Comparison<T> path with the TComparer path without duplicating code.

Note this is not just about sorting. Sorting, however, for me is a good example of where .NET comes short. I use the value type as a inlineable "functor" pattern for data processing algorithms. Think loops over millions of elements. Unfortunately, we can't unify on this pattern due to these kinds of issues, which Sort exemplifies very well. Lots of other devs use this pattern, but we often end up having to duplicate code, and since this is code that is combinatorial on rank, different kinds of transformations etc. it adds up. It's a lot of replicated code. I have talked about this before and don't want to sound like a broken record player 😅

with the idea that you could provide a struct-based comparer and it wouldn't allocate...

It's not just the allocation. It's so the compare can be inlined. So the "functor" can be applied inlined. To yield a customized loop for performance. As you probably know.

perhaps a benchmark along these lines would be instructive?

@AndyAyersMS I don't know what kind of benchmark you are thinking about but something simple like below shows the issue.

usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;usingSystem;usingSystem.Collections.Generic;usingSystem.Runtime.CompilerServices;namespaceCompareBenchmarking{publicclassProgram{staticvoidMain(string[]args)=>BenchmarkSwitcher.FromAssemblies(new[]{typeof(Program).Assembly}).Run(args);}publicclassCompareFloat:Compare<float>{protectedoverridefloatGetNext()=>_random.Next();}publicclassCompareInt32:Compare<int>{protectedoverrideintGetNext()=>_random.Next();}publicstructComparisonComparer<T>:IComparer<T>{readonlyComparison<T>_comparison;publicComparisonComparer(Comparison<T>comparison)=>_comparison=comparison;[MethodImpl(MethodImplOptions.AggressiveInlining)]publicintCompare(Tx,Ty)=>_comparison(x,y);}[MemoryDiagnoser][DisassemblyDiagnoser]publicabstractclassCompare<T>whereT:IComparable<T>{staticreadonlyComparer<T>_comparer=Comparer<T>.Default;staticreadonlyComparison<T>_comparison=Comparer<T>.Default.Compare;readonlyComparisonComparer<T>_comparisonComparer=newComparisonComparer<T>(_comparison);protectedRandom_random;T_x;T_y;protectedabstractTGetNext();[GlobalSetup]publicvoidSetup(){_random=newRandom(42);_x=GetNext();_y=GetNext();}[Benchmark]publicintCompareTo()=>_x.CompareTo(_y);[Benchmark]publicintComparer()=>_comparer.Compare(_x,_y);[Benchmark(Baseline=true)]publicintComparison()=>_comparison(_x,_y);[Benchmark]publicintComparisonComparer()=>_comparisonComparer.Compare(_x,_y);}}

With the following results on .NET 5.0 Preview 2. The factor of 2.31x pretty much says it all although that would be pretty self-evident given the extra indirection and code generation issues. Just imagine this in a tight loop. :)

BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19041.329 (2004/?/20H1)
Intel Core i7-8700 CPU 3.20GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores
.NET Core SDK=5.0.100-preview.2.20176.6
[Host] : .NET Core 5.0.0 (CoreCLR 5.0.20.16006, CoreFX 5.0.20.16006), X64 RyuJIT
DefaultJob : .NET Core 5.0.0 (CoreCLR 5.0.20.16006, CoreFX 5.0.20.16006), X64 RyuJIT

CompareInt32

MethodMeanErrorStdDevRatio
CompareTo0.4995 ns0.0051 ns0.0048 ns0.38
Comparer1.1854 ns0.0032 ns0.0028 ns0.89
Comparison1.3258 ns0.0065 ns0.0054 ns1.00
ComparisonComparer3.0613 ns0.0096 ns0.0089 ns2.31

CompareFloat

MethodMeanErrorStdDevRatioRatioSD
CompareTo1.237 ns0.0018 ns0.0015 ns0.600.00
Comparer1.905 ns0.0628 ns0.0557 ns0.930.03
Comparison2.060 ns0.0047 ns0.0039 ns1.000.00
ComparisonComparer3.479 ns0.0096 ns0.0090 ns1.690.01

If this is interesting I can make a PR to the benchmark repo.

@stephentoub
stephentoubforce-pushed the fixsortperf_utilslessthanref branch from 04e1399 to 356828cCompareJune 25, 2020 22:27
@stephentoub
stephentoubforce-pushed the fixsortperf_utilslessthanref branch from 356828c to a3e3d79CompareJune 26, 2020 21:34
AndyAyersMS added a commit to AndyAyersMS/runtime that referenced this pull request Jun 26, 2020
RyuJit would not inline methods that contained delegate invokes. Remove
this limitation.
Closesdotnet#10048. See also dotnet#37941.
jkotas pushed a commit that referenced this pull request Jun 27, 2020
RyuJit would not inline methods that contained delegate invokes. Remove
this limitation.
Closes#10048. See also #37941.
Several months back we moved the sorting logic for primtive types out of native code into managed. Doing so helped to make the logic reusable for spans and helped to reduce GC latency, and also actually helped with throughput in a variety of cases. But it ended up regressing throughput for sorting larger arrays of floating-point values, with float/double.CompareTo not getting inlined, and even if it were inlined, containing much more logic than was present in the native implementation. The native implementation did a pre-pass to move all NaNs to the front and then just used simple < and > comparison operations, so the managed implementation now does as well.
@stephentoub
stephentoubforce-pushed the fixsortperf_utilslessthanref branch from a3e3d79 to 2bec1bfCompareJune 27, 2020 23:57
@stephentoub

Copy link
Copy Markdown
MemberAuthor

I moved the comparer functions into the two generic helper classes, and with #38229 (thanks, @jkotas), the reference-type-Int32-wrapper case is good again, with everything else being appx what it was before as well.

I'll merge when this is green.

@stephentoub
stephentoub merged commit e1c9ab4 into dotnet:masterJun 28, 2020
@stephentoub
stephentoub deleted the fixsortperf_utilslessthanref branch June 28, 2020 01:57
@nietras

Copy link
Copy Markdown
Contributor

@stephentoub should I try make a PR for the TComparer change? Is there interest in getting this in? The change itself to TComparer is easy enough, it's the whole sort helpers creation etc. that needs to change a lot, and to support reference type delegate comparer will likely require a "unsafe" cast of Comparison<T> to Comparison<object>. Is that acceptable?

Also thank you for the mention in your awesome Performance Improvements in .NET 5 :)

@jkotas

jkotas commented Jul 16, 2020

Copy link
Copy Markdown
Member

Hi @nietras,

@stephentoub is OOF.

I think it would be a good idea to start the PR for the TComparer changes so that we can start sorting out the code quality issues that it is likely to expose. Preferably, we would fix them instead of working around them by duplicating large amounts of code.

I have opened #39466 to have this tracked. For time line, I do not expect we would be able to get this change into .NET 5.

"unsafe" cast of Comparison<T> to Comparison<object>

Why would that be needed?

I am wondering whether it would make sense to delete the TComparer overloads from the public surface for now until we can get the right implementation for them in place. I think they can stay, but just wanted to bring it up.

cc @eiriktsarpalis

@ghostghost locked as resolved and limited conversation to collaborators Dec 8, 2020
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.RuntimeNO-MERGEThe PR is not ready for merge yet (see discussion for detailed reasons)tenet-performancePerformance related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants

@stephentoub@nietras@AndyAyersMS@jkotas@EgorBo@GrabYourPitchforks@danmoseley@tannergooding
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix regression in Array.Sort for floats/doubles - #37941

Merged
stephentoub merged 3 commits into
dotnet:masterfrom
stephentoub:fixsortperf_utilslessthanref
Jun 28, 2020
Merged

Fix regression in Array.Sort for floats/doubles#37941
stephentoub merged 3 commits into
dotnet:masterfrom
stephentoub:fixsortperf_utilslessthanref

Conversation

@stephentoub

@stephentoubstephentoub commented Jun 16, 2020

Copy link
Copy Markdown
Member

Several months back we moved the sorting logic for primtive types out of native code into managed. Doing so helped to make the logic reusable for spans and helped to reduce GC latency, and also actually helped with throughput in a variety of cases. But it ended up regressing throughput for sorting larger arrays of floating-point values, with float/double.CompareTo not getting inlined, and even if it were inlined, containing much more logic than was present in the native implementation. The native implementation did a pre-pass to move all NaNs to the front and then just used simple < and > comparison operations, so the managed implementation now does as well.

With the exception of a large array of already-sorted Int32 values where there is still a small regression after this PR, all of the cases I've tested are either as good or better than .NET Core 3.1.

@jkotas, @GrabYourPitchforks, @tannergooding, thanks for your offline suggestions on approaches here; I tried out a variety of them, including vectorized float/double.CompareTo as well as unsafe casts to wrapper types with customized IComparable implementations, and this ended up being the best overall. Thanks as well to @nietras for pointing out the regression.

Benchmark:

usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;usingSystem;usingSystem.Diagnostics.CodeAnalysis;usingSystem.Linq;[MemoryDiagnoser]publicclassProgram{staticvoidMain(string[]args)=>BenchmarkSwitcher.FromAssemblies(new[]{typeof(Program).Assembly}).Run(args);}publicclassDoubleSorting:Sorting<double>{protectedoverridedoubleGetNext()=>_random.Next();}publicclassInt32Sorting:Sorting<int>{protectedoverrideintGetNext()=>_random.Next();}publicclassStringSorting:Sorting<string>{protectedoverridestringGetNext()=>string.Create(_random.Next(1,5),_random,(dest,r)=>{for(inti=0;i<dest.Length;i++)dest[i]=(char)('a'+r.Next(26));});}publicabstractclassSorting<T>{protectedRandom_random;privateT[]_orig,_array;[Params(10,100_000)]publicintSize{get;set;}protectedabstractTGetNext();[GlobalSetup]publicvoidSetup(){_random=newRandom(42);_orig=Enumerable.Range(0,Size).Select(_ =>GetNext()).ToArray();_array=(T[])_orig.Clone();Array.Sort(_array);}[Benchmark]publicvoidSorted()=>Array.Sort(_array);[Benchmark]publicvoidRandom(){_orig.AsSpan().CopyTo(_array);Array.Sort(_array);}}
TypeMethodToolchainSizeMeanRatio
DoubleSortingSortednetcore311059.58 ns2.13
DoubleSortingSortedmaster1030.52 ns1.09
DoubleSortingSortedpr1028.01 ns1.00
DoubleSortingRandomnetcore311077.50 ns1.71
DoubleSortingRandommaster1067.61 ns1.49
DoubleSortingRandompr1045.44 ns1.00
DoubleSortingSortednetcore31100000990,325.98 ns1.27
DoubleSortingSortedmaster1000002,898,290.96 ns3.73
DoubleSortingSortedpr100000777,209.08 ns1.00
DoubleSortingRandomnetcore311000005,940,056.30 ns1.08
DoubleSortingRandommaster1000007,880,560.62 ns1.44
DoubleSortingRandompr1000005,473,335.58 ns1.00
Int32SortingSortednetcore311038.02 ns2.31
Int32SortingSortedmaster1017.09 ns1.04
Int32SortingSortedpr1016.49 ns1.00
Int32SortingRandomnetcore311049.97 ns1.63
Int32SortingRandommaster1031.30 ns1.02
Int32SortingRandompr1030.63 ns1.00
Int32SortingSortednetcore31100000572,908.37 ns0.90
Int32SortingSortedmaster100000640,966.00 ns1.00
Int32SortingSortedpr100000639,118.53 ns1.00
Int32SortingRandomnetcore311000005,072,236.56 ns1.06
Int32SortingRandommaster1000005,024,276.17 ns1.05
Int32SortingRandompr1000004,801,833.82 ns1.00
StringSortingSortednetcore3110575.87 ns1.24
StringSortingSortedmaster10432.40 ns0.93
StringSortingSortedpr10465.86 ns1.00
StringSortingRandomnetcore31101,758.64 ns1.15
StringSortingRandommaster101,425.75 ns0.93
StringSortingRandompr101,532.01 ns1.00
StringSortingSortednetcore3110000083,774,554.44 ns1.14
StringSortingSortedmaster10000074,485,247.25 ns1.01
StringSortingSortedpr10000073,450,518.68 ns1.00
StringSortingRandomnetcore31100000103,108,672.31 ns1.14
StringSortingRandommaster10000089,373,058.33 ns0.99
StringSortingRandompr10000090,577,543.59 ns1.00

@stephentoubstephentoub added this to the 5.0.0 milestone Jun 16, 2020

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

Nice!

@stephentoub
stephentoubforce-pushed the fixsortperf_utilslessthanref branch from 046f85c to 07dcfe1CompareJune 17, 2020 02:27
@stephentoub
stephentoubforce-pushed the fixsortperf_utilslessthanref branch from 07dcfe1 to 04e1399CompareJune 17, 2020 09:43
@nietras

Copy link
Copy Markdown
Contributor

@stephentoub perhaps consider testing performance of following type too:

publicclassComparableClassInt32:IComparable<ComparableClassInt32>{publicreadonlyintValue;publicComparableClassInt32(intvalue)=>Value=value;publicintCompareTo(ComparableClassInt32other)=>Value.CompareTo(other.Value);}

basic reference type overhead. Since string compares are "slow" you won't see possible reference type regressions for this. Just a suggestion :)

@stephentoub

Copy link
Copy Markdown
MemberAuthor

Just a suggestion :)

A knowing smile? 😉 Yes, this case regresses. It appears to be due to dictionary lookups when calling LessThan/GreaterThan, which also prevent inlining. Evaluating options...

@nietras

Copy link
Copy Markdown
Contributor

knowing smile? 😉

Ha yeah 😉 Back in 2018 I went through all "stages of inlining": huh?, wuhuu! (AggressiveInlining), doh! (reference type regression), oh come on! (JIT-me-not issues) 😅

Looking forward to what you come up with. 😀

@stephentoubstephentoub added the NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) label Jun 18, 2020
@stephentoub

Copy link
Copy Markdown
MemberAuthor

#38229 will hopefully be the solution here.

@nietras

Copy link
Copy Markdown
Contributor

Nice 👍 Now if #35791/#10048 were resolved too, we would have something to talk about 😉 Happy to help with that if I could get some pointers 😀

@AndyAyersMS

Copy link
Copy Markdown
Member

Happy to help with that if I could get some pointers

I'm certainly open to reconsidering #10048. The changes are simple enough, though I might end up restricting it to AggressiveInlining callees since the jit underestimates the size impact of a delegate invoke.

But I still don't have a clear picture of how allowing this actually provides benefit -- it sounds like you think with this we can unify some code and either make it perform better or at least not lose any perf. So perhaps a benchmark along these lines would be instructive?

@stephentoub

Copy link
Copy Markdown
MemberAuthor

it sounds like you think with this we can unify some code and either make it perform better or at least not lose any perf.

Not to put words in @nietras mouth, but I expect what he's hoping to do is improve the Array.Sort(..., IComparer<T>) code paths. Today we have one sort implementation that covers both providing an IComparer<T> and a Comparison<T> (a delegate); the former is implemented by creating a delegate to its Compare method, which means Array.Sort(..., IComparer<T>) is allocating. On top of that, we just recently added a span-based sort, and it's currently defined with a generic TComparer : IComparer<T> comparer, with the idea that you could provide a struct-based comparer and it wouldn't allocate... unfortunately, it's the worst of all worlds right now, in that we box the TComparer into an IComparer<T>, and then create a delegate from its Compare method. If all of these code quality issues got sorted out, you could imagine we just had a single code path for TComparer that was written generically to use its Compare sans boxing, and then if you used the Comparison<T> overload, we'd create a struct-based TComparer that just wrapped that Comparison<T> to invoke it. If we everything worked out really well, it could potentially even be unified with the separate, duplicate code path we currently have that targets T : IComparable<T>, using a TComparer comparer struct that delegated to the T : IComparable<T> implementation.

So perhaps a benchmark along these lines would be instructive?

Sounds like a good thing to add to dotnet/performance.

@nietras

nietras commented Jun 23, 2020

Copy link
Copy Markdown
Contributor

If all of these code quality issues got sorted out, you could imagine we just had a single code path for TComparer that was written generically to use its Compare sans boxing, and then if you used the Comparison overload, we'd create a struct-based TComparer that just wrapped that Comparison to invoke it. If we everything worked out really well, it could potentially even be unified with the separate, duplicate code path we currently have that targets T : IComparable, using a TComparer comparer struct that delegated to the T : IComparable implementation.

@stephentoub exactly. :) Although, I am not sure we could unify on TComparer as such, but potentially yes. This would be the fulfillment of the API I have proposed and the on/off work I have been doing on this for the last 3 years... failing due to the many issues around inlining. At the very minimum we could replace the Comparison<T> path with the TComparer path without duplicating code.

Note this is not just about sorting. Sorting, however, for me is a good example of where .NET comes short. I use the value type as a inlineable "functor" pattern for data processing algorithms. Think loops over millions of elements. Unfortunately, we can't unify on this pattern due to these kinds of issues, which Sort exemplifies very well. Lots of other devs use this pattern, but we often end up having to duplicate code, and since this is code that is combinatorial on rank, different kinds of transformations etc. it adds up. It's a lot of replicated code. I have talked about this before and don't want to sound like a broken record player 😅

with the idea that you could provide a struct-based comparer and it wouldn't allocate...

It's not just the allocation. It's so the compare can be inlined. So the "functor" can be applied inlined. To yield a customized loop for performance. As you probably know.

perhaps a benchmark along these lines would be instructive?

@AndyAyersMS I don't know what kind of benchmark you are thinking about but something simple like below shows the issue.

usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;usingSystem;usingSystem.Collections.Generic;usingSystem.Runtime.CompilerServices;namespaceCompareBenchmarking{publicclassProgram{staticvoidMain(string[]args)=>BenchmarkSwitcher.FromAssemblies(new[]{typeof(Program).Assembly}).Run(args);}publicclassCompareFloat:Compare<float>{protectedoverridefloatGetNext()=>_random.Next();}publicclassCompareInt32:Compare<int>{protectedoverrideintGetNext()=>_random.Next();}publicstructComparisonComparer<T>:IComparer<T>{readonlyComparison<T>_comparison;publicComparisonComparer(Comparison<T>comparison)=>_comparison=comparison;[MethodImpl(MethodImplOptions.AggressiveInlining)]publicintCompare(Tx,Ty)=>_comparison(x,y);}[MemoryDiagnoser][DisassemblyDiagnoser]publicabstractclassCompare<T>whereT:IComparable<T>{staticreadonlyComparer<T>_comparer=Comparer<T>.Default;staticreadonlyComparison<T>_comparison=Comparer<T>.Default.Compare;readonlyComparisonComparer<T>_comparisonComparer=newComparisonComparer<T>(_comparison);protectedRandom_random;T_x;T_y;protectedabstractTGetNext();[GlobalSetup]publicvoidSetup(){_random=newRandom(42);_x=GetNext();_y=GetNext();}[Benchmark]publicintCompareTo()=>_x.CompareTo(_y);[Benchmark]publicintComparer()=>_comparer.Compare(_x,_y);[Benchmark(Baseline=true)]publicintComparison()=>_comparison(_x,_y);[Benchmark]publicintComparisonComparer()=>_comparisonComparer.Compare(_x,_y);}}

With the following results on .NET 5.0 Preview 2. The factor of 2.31x pretty much says it all although that would be pretty self-evident given the extra indirection and code generation issues. Just imagine this in a tight loop. :)

BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19041.329 (2004/?/20H1)
Intel Core i7-8700 CPU 3.20GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores
.NET Core SDK=5.0.100-preview.2.20176.6
[Host] : .NET Core 5.0.0 (CoreCLR 5.0.20.16006, CoreFX 5.0.20.16006), X64 RyuJIT
DefaultJob : .NET Core 5.0.0 (CoreCLR 5.0.20.16006, CoreFX 5.0.20.16006), X64 RyuJIT

CompareInt32

MethodMeanErrorStdDevRatio
CompareTo0.4995 ns0.0051 ns0.0048 ns0.38
Comparer1.1854 ns0.0032 ns0.0028 ns0.89
Comparison1.3258 ns0.0065 ns0.0054 ns1.00
ComparisonComparer3.0613 ns0.0096 ns0.0089 ns2.31

CompareFloat

MethodMeanErrorStdDevRatioRatioSD
CompareTo1.237 ns0.0018 ns0.0015 ns0.600.00
Comparer1.905 ns0.0628 ns0.0557 ns0.930.03
Comparison2.060 ns0.0047 ns0.0039 ns1.000.00
ComparisonComparer3.479 ns0.0096 ns0.0090 ns1.690.01

If this is interesting I can make a PR to the benchmark repo.

@stephentoub
stephentoubforce-pushed the fixsortperf_utilslessthanref branch from 04e1399 to 356828cCompareJune 25, 2020 22:27
@stephentoub
stephentoubforce-pushed the fixsortperf_utilslessthanref branch from 356828c to a3e3d79CompareJune 26, 2020 21:34
AndyAyersMS added a commit to AndyAyersMS/runtime that referenced this pull request Jun 26, 2020
RyuJit would not inline methods that contained delegate invokes. Remove
this limitation.
Closesdotnet#10048. See also dotnet#37941.
jkotas pushed a commit that referenced this pull request Jun 27, 2020
RyuJit would not inline methods that contained delegate invokes. Remove
this limitation.
Closes#10048. See also #37941.
Several months back we moved the sorting logic for primtive types out of native code into managed. Doing so helped to make the logic reusable for spans and helped to reduce GC latency, and also actually helped with throughput in a variety of cases. But it ended up regressing throughput for sorting larger arrays of floating-point values, with float/double.CompareTo not getting inlined, and even if it were inlined, containing much more logic than was present in the native implementation. The native implementation did a pre-pass to move all NaNs to the front and then just used simple < and > comparison operations, so the managed implementation now does as well.
@stephentoub
stephentoubforce-pushed the fixsortperf_utilslessthanref branch from a3e3d79 to 2bec1bfCompareJune 27, 2020 23:57
@stephentoub

Copy link
Copy Markdown
MemberAuthor

I moved the comparer functions into the two generic helper classes, and with #38229 (thanks, @jkotas), the reference-type-Int32-wrapper case is good again, with everything else being appx what it was before as well.

I'll merge when this is green.

@stephentoub
stephentoub merged commit e1c9ab4 into dotnet:masterJun 28, 2020
@stephentoub
stephentoub deleted the fixsortperf_utilslessthanref branch June 28, 2020 01:57
@nietras

Copy link
Copy Markdown
Contributor

@stephentoub should I try make a PR for the TComparer change? Is there interest in getting this in? The change itself to TComparer is easy enough, it's the whole sort helpers creation etc. that needs to change a lot, and to support reference type delegate comparer will likely require a "unsafe" cast of Comparison<T> to Comparison<object>. Is that acceptable?

Also thank you for the mention in your awesome Performance Improvements in .NET 5 :)

@jkotas

jkotas commented Jul 16, 2020

Copy link
Copy Markdown
Member

Hi @nietras,

@stephentoub is OOF.

I think it would be a good idea to start the PR for the TComparer changes so that we can start sorting out the code quality issues that it is likely to expose. Preferably, we would fix them instead of working around them by duplicating large amounts of code.

I have opened #39466 to have this tracked. For time line, I do not expect we would be able to get this change into .NET 5.

"unsafe" cast of Comparison<T> to Comparison<object>

Why would that be needed?

I am wondering whether it would make sense to delete the TComparer overloads from the public surface for now until we can get the right implementation for them in place. I think they can stay, but just wanted to bring it up.

cc @eiriktsarpalis

@ghostghost locked as resolved and limited conversation to collaborators Dec 8, 2020
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.RuntimeNO-MERGEThe PR is not ready for merge yet (see discussion for detailed reasons)tenet-performancePerformance related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants

@stephentoub@nietras@AndyAyersMS@jkotas@EgorBo@GrabYourPitchforks@danmoseley@tannergooding
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix regression in Array.Sort for floats/doubles - #37941

Merged
stephentoub merged 3 commits into
dotnet:masterfrom
stephentoub:fixsortperf_utilslessthanref
Jun 28, 2020
Merged

Fix regression in Array.Sort for floats/doubles#37941
stephentoub merged 3 commits into
dotnet:masterfrom
stephentoub:fixsortperf_utilslessthanref

Conversation

@stephentoub

@stephentoubstephentoub commented Jun 16, 2020

Copy link
Copy Markdown
Member

Several months back we moved the sorting logic for primtive types out of native code into managed. Doing so helped to make the logic reusable for spans and helped to reduce GC latency, and also actually helped with throughput in a variety of cases. But it ended up regressing throughput for sorting larger arrays of floating-point values, with float/double.CompareTo not getting inlined, and even if it were inlined, containing much more logic than was present in the native implementation. The native implementation did a pre-pass to move all NaNs to the front and then just used simple < and > comparison operations, so the managed implementation now does as well.

With the exception of a large array of already-sorted Int32 values where there is still a small regression after this PR, all of the cases I've tested are either as good or better than .NET Core 3.1.

@jkotas, @GrabYourPitchforks, @tannergooding, thanks for your offline suggestions on approaches here; I tried out a variety of them, including vectorized float/double.CompareTo as well as unsafe casts to wrapper types with customized IComparable implementations, and this ended up being the best overall. Thanks as well to @nietras for pointing out the regression.

Benchmark:

usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;usingSystem;usingSystem.Diagnostics.CodeAnalysis;usingSystem.Linq;[MemoryDiagnoser]publicclassProgram{staticvoidMain(string[]args)=>BenchmarkSwitcher.FromAssemblies(new[]{typeof(Program).Assembly}).Run(args);}publicclassDoubleSorting:Sorting<double>{protectedoverridedoubleGetNext()=>_random.Next();}publicclassInt32Sorting:Sorting<int>{protectedoverrideintGetNext()=>_random.Next();}publicclassStringSorting:Sorting<string>{protectedoverridestringGetNext()=>string.Create(_random.Next(1,5),_random,(dest,r)=>{for(inti=0;i<dest.Length;i++)dest[i]=(char)('a'+r.Next(26));});}publicabstractclassSorting<T>{protectedRandom_random;privateT[]_orig,_array;[Params(10,100_000)]publicintSize{get;set;}protectedabstractTGetNext();[GlobalSetup]publicvoidSetup(){_random=newRandom(42);_orig=Enumerable.Range(0,Size).Select(_ =>GetNext()).ToArray();_array=(T[])_orig.Clone();Array.Sort(_array);}[Benchmark]publicvoidSorted()=>Array.Sort(_array);[Benchmark]publicvoidRandom(){_orig.AsSpan().CopyTo(_array);Array.Sort(_array);}}
TypeMethodToolchainSizeMeanRatio
DoubleSortingSortednetcore311059.58 ns2.13
DoubleSortingSortedmaster1030.52 ns1.09
DoubleSortingSortedpr1028.01 ns1.00
DoubleSortingRandomnetcore311077.50 ns1.71
DoubleSortingRandommaster1067.61 ns1.49
DoubleSortingRandompr1045.44 ns1.00
DoubleSortingSortednetcore31100000990,325.98 ns1.27
DoubleSortingSortedmaster1000002,898,290.96 ns3.73
DoubleSortingSortedpr100000777,209.08 ns1.00
DoubleSortingRandomnetcore311000005,940,056.30 ns1.08
DoubleSortingRandommaster1000007,880,560.62 ns1.44
DoubleSortingRandompr1000005,473,335.58 ns1.00
Int32SortingSortednetcore311038.02 ns2.31
Int32SortingSortedmaster1017.09 ns1.04
Int32SortingSortedpr1016.49 ns1.00
Int32SortingRandomnetcore311049.97 ns1.63
Int32SortingRandommaster1031.30 ns1.02
Int32SortingRandompr1030.63 ns1.00
Int32SortingSortednetcore31100000572,908.37 ns0.90
Int32SortingSortedmaster100000640,966.00 ns1.00
Int32SortingSortedpr100000639,118.53 ns1.00
Int32SortingRandomnetcore311000005,072,236.56 ns1.06
Int32SortingRandommaster1000005,024,276.17 ns1.05
Int32SortingRandompr1000004,801,833.82 ns1.00
StringSortingSortednetcore3110575.87 ns1.24
StringSortingSortedmaster10432.40 ns0.93
StringSortingSortedpr10465.86 ns1.00
StringSortingRandomnetcore31101,758.64 ns1.15
StringSortingRandommaster101,425.75 ns0.93
StringSortingRandompr101,532.01 ns1.00
StringSortingSortednetcore3110000083,774,554.44 ns1.14
StringSortingSortedmaster10000074,485,247.25 ns1.01
StringSortingSortedpr10000073,450,518.68 ns1.00
StringSortingRandomnetcore31100000103,108,672.31 ns1.14
StringSortingRandommaster10000089,373,058.33 ns0.99
StringSortingRandompr10000090,577,543.59 ns1.00

@stephentoubstephentoub added this to the 5.0.0 milestone Jun 16, 2020

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

Nice!

@stephentoub
stephentoubforce-pushed the fixsortperf_utilslessthanref branch from 046f85c to 07dcfe1CompareJune 17, 2020 02:27
@stephentoub
stephentoubforce-pushed the fixsortperf_utilslessthanref branch from 07dcfe1 to 04e1399CompareJune 17, 2020 09:43
@nietras

Copy link
Copy Markdown
Contributor

@stephentoub perhaps consider testing performance of following type too:

publicclassComparableClassInt32:IComparable<ComparableClassInt32>{publicreadonlyintValue;publicComparableClassInt32(intvalue)=>Value=value;publicintCompareTo(ComparableClassInt32other)=>Value.CompareTo(other.Value);}

basic reference type overhead. Since string compares are "slow" you won't see possible reference type regressions for this. Just a suggestion :)

@stephentoub

Copy link
Copy Markdown
MemberAuthor

Just a suggestion :)

A knowing smile? 😉 Yes, this case regresses. It appears to be due to dictionary lookups when calling LessThan/GreaterThan, which also prevent inlining. Evaluating options...

@nietras

Copy link
Copy Markdown
Contributor

knowing smile? 😉

Ha yeah 😉 Back in 2018 I went through all "stages of inlining": huh?, wuhuu! (AggressiveInlining), doh! (reference type regression), oh come on! (JIT-me-not issues) 😅

Looking forward to what you come up with. 😀

@stephentoubstephentoub added the NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) label Jun 18, 2020
@stephentoub

Copy link
Copy Markdown
MemberAuthor

#38229 will hopefully be the solution here.

@nietras

Copy link
Copy Markdown
Contributor

Nice 👍 Now if #35791/#10048 were resolved too, we would have something to talk about 😉 Happy to help with that if I could get some pointers 😀

@AndyAyersMS

Copy link
Copy Markdown
Member

Happy to help with that if I could get some pointers

I'm certainly open to reconsidering #10048. The changes are simple enough, though I might end up restricting it to AggressiveInlining callees since the jit underestimates the size impact of a delegate invoke.

But I still don't have a clear picture of how allowing this actually provides benefit -- it sounds like you think with this we can unify some code and either make it perform better or at least not lose any perf. So perhaps a benchmark along these lines would be instructive?

@stephentoub

Copy link
Copy Markdown
MemberAuthor

it sounds like you think with this we can unify some code and either make it perform better or at least not lose any perf.

Not to put words in @nietras mouth, but I expect what he's hoping to do is improve the Array.Sort(..., IComparer<T>) code paths. Today we have one sort implementation that covers both providing an IComparer<T> and a Comparison<T> (a delegate); the former is implemented by creating a delegate to its Compare method, which means Array.Sort(..., IComparer<T>) is allocating. On top of that, we just recently added a span-based sort, and it's currently defined with a generic TComparer : IComparer<T> comparer, with the idea that you could provide a struct-based comparer and it wouldn't allocate... unfortunately, it's the worst of all worlds right now, in that we box the TComparer into an IComparer<T>, and then create a delegate from its Compare method. If all of these code quality issues got sorted out, you could imagine we just had a single code path for TComparer that was written generically to use its Compare sans boxing, and then if you used the Comparison<T> overload, we'd create a struct-based TComparer that just wrapped that Comparison<T> to invoke it. If we everything worked out really well, it could potentially even be unified with the separate, duplicate code path we currently have that targets T : IComparable<T>, using a TComparer comparer struct that delegated to the T : IComparable<T> implementation.

So perhaps a benchmark along these lines would be instructive?

Sounds like a good thing to add to dotnet/performance.

@nietras

nietras commented Jun 23, 2020

Copy link
Copy Markdown
Contributor

If all of these code quality issues got sorted out, you could imagine we just had a single code path for TComparer that was written generically to use its Compare sans boxing, and then if you used the Comparison overload, we'd create a struct-based TComparer that just wrapped that Comparison to invoke it. If we everything worked out really well, it could potentially even be unified with the separate, duplicate code path we currently have that targets T : IComparable, using a TComparer comparer struct that delegated to the T : IComparable implementation.

@stephentoub exactly. :) Although, I am not sure we could unify on TComparer as such, but potentially yes. This would be the fulfillment of the API I have proposed and the on/off work I have been doing on this for the last 3 years... failing due to the many issues around inlining. At the very minimum we could replace the Comparison<T> path with the TComparer path without duplicating code.

Note this is not just about sorting. Sorting, however, for me is a good example of where .NET comes short. I use the value type as a inlineable "functor" pattern for data processing algorithms. Think loops over millions of elements. Unfortunately, we can't unify on this pattern due to these kinds of issues, which Sort exemplifies very well. Lots of other devs use this pattern, but we often end up having to duplicate code, and since this is code that is combinatorial on rank, different kinds of transformations etc. it adds up. It's a lot of replicated code. I have talked about this before and don't want to sound like a broken record player 😅

with the idea that you could provide a struct-based comparer and it wouldn't allocate...

It's not just the allocation. It's so the compare can be inlined. So the "functor" can be applied inlined. To yield a customized loop for performance. As you probably know.

perhaps a benchmark along these lines would be instructive?

@AndyAyersMS I don't know what kind of benchmark you are thinking about but something simple like below shows the issue.

usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;usingSystem;usingSystem.Collections.Generic;usingSystem.Runtime.CompilerServices;namespaceCompareBenchmarking{publicclassProgram{staticvoidMain(string[]args)=>BenchmarkSwitcher.FromAssemblies(new[]{typeof(Program).Assembly}).Run(args);}publicclassCompareFloat:Compare<float>{protectedoverridefloatGetNext()=>_random.Next();}publicclassCompareInt32:Compare<int>{protectedoverrideintGetNext()=>_random.Next();}publicstructComparisonComparer<T>:IComparer<T>{readonlyComparison<T>_comparison;publicComparisonComparer(Comparison<T>comparison)=>_comparison=comparison;[MethodImpl(MethodImplOptions.AggressiveInlining)]publicintCompare(Tx,Ty)=>_comparison(x,y);}[MemoryDiagnoser][DisassemblyDiagnoser]publicabstractclassCompare<T>whereT:IComparable<T>{staticreadonlyComparer<T>_comparer=Comparer<T>.Default;staticreadonlyComparison<T>_comparison=Comparer<T>.Default.Compare;readonlyComparisonComparer<T>_comparisonComparer=newComparisonComparer<T>(_comparison);protectedRandom_random;T_x;T_y;protectedabstractTGetNext();[GlobalSetup]publicvoidSetup(){_random=newRandom(42);_x=GetNext();_y=GetNext();}[Benchmark]publicintCompareTo()=>_x.CompareTo(_y);[Benchmark]publicintComparer()=>_comparer.Compare(_x,_y);[Benchmark(Baseline=true)]publicintComparison()=>_comparison(_x,_y);[Benchmark]publicintComparisonComparer()=>_comparisonComparer.Compare(_x,_y);}}

With the following results on .NET 5.0 Preview 2. The factor of 2.31x pretty much says it all although that would be pretty self-evident given the extra indirection and code generation issues. Just imagine this in a tight loop. :)

BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19041.329 (2004/?/20H1)
Intel Core i7-8700 CPU 3.20GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores
.NET Core SDK=5.0.100-preview.2.20176.6
[Host] : .NET Core 5.0.0 (CoreCLR 5.0.20.16006, CoreFX 5.0.20.16006), X64 RyuJIT
DefaultJob : .NET Core 5.0.0 (CoreCLR 5.0.20.16006, CoreFX 5.0.20.16006), X64 RyuJIT

CompareInt32

MethodMeanErrorStdDevRatio
CompareTo0.4995 ns0.0051 ns0.0048 ns0.38
Comparer1.1854 ns0.0032 ns0.0028 ns0.89
Comparison1.3258 ns0.0065 ns0.0054 ns1.00
ComparisonComparer3.0613 ns0.0096 ns0.0089 ns2.31

CompareFloat

MethodMeanErrorStdDevRatioRatioSD
CompareTo1.237 ns0.0018 ns0.0015 ns0.600.00
Comparer1.905 ns0.0628 ns0.0557 ns0.930.03
Comparison2.060 ns0.0047 ns0.0039 ns1.000.00
ComparisonComparer3.479 ns0.0096 ns0.0090 ns1.690.01

If this is interesting I can make a PR to the benchmark repo.

@stephentoub
stephentoubforce-pushed the fixsortperf_utilslessthanref branch from 04e1399 to 356828cCompareJune 25, 2020 22:27
@stephentoub
stephentoubforce-pushed the fixsortperf_utilslessthanref branch from 356828c to a3e3d79CompareJune 26, 2020 21:34
AndyAyersMS added a commit to AndyAyersMS/runtime that referenced this pull request Jun 26, 2020
RyuJit would not inline methods that contained delegate invokes. Remove
this limitation.
Closesdotnet#10048. See also dotnet#37941.
jkotas pushed a commit that referenced this pull request Jun 27, 2020
RyuJit would not inline methods that contained delegate invokes. Remove
this limitation.
Closes#10048. See also #37941.
Several months back we moved the sorting logic for primtive types out of native code into managed. Doing so helped to make the logic reusable for spans and helped to reduce GC latency, and also actually helped with throughput in a variety of cases. But it ended up regressing throughput for sorting larger arrays of floating-point values, with float/double.CompareTo not getting inlined, and even if it were inlined, containing much more logic than was present in the native implementation. The native implementation did a pre-pass to move all NaNs to the front and then just used simple < and > comparison operations, so the managed implementation now does as well.
@stephentoub
stephentoubforce-pushed the fixsortperf_utilslessthanref branch from a3e3d79 to 2bec1bfCompareJune 27, 2020 23:57
@stephentoub

Copy link
Copy Markdown
MemberAuthor

I moved the comparer functions into the two generic helper classes, and with #38229 (thanks, @jkotas), the reference-type-Int32-wrapper case is good again, with everything else being appx what it was before as well.

I'll merge when this is green.

@stephentoub
stephentoub merged commit e1c9ab4 into dotnet:masterJun 28, 2020
@stephentoub
stephentoub deleted the fixsortperf_utilslessthanref branch June 28, 2020 01:57
@nietras

Copy link
Copy Markdown
Contributor

@stephentoub should I try make a PR for the TComparer change? Is there interest in getting this in? The change itself to TComparer is easy enough, it's the whole sort helpers creation etc. that needs to change a lot, and to support reference type delegate comparer will likely require a "unsafe" cast of Comparison<T> to Comparison<object>. Is that acceptable?

Also thank you for the mention in your awesome Performance Improvements in .NET 5 :)

@jkotas

jkotas commented Jul 16, 2020

Copy link
Copy Markdown
Member

Hi @nietras,

@stephentoub is OOF.

I think it would be a good idea to start the PR for the TComparer changes so that we can start sorting out the code quality issues that it is likely to expose. Preferably, we would fix them instead of working around them by duplicating large amounts of code.

I have opened #39466 to have this tracked. For time line, I do not expect we would be able to get this change into .NET 5.

"unsafe" cast of Comparison<T> to Comparison<object>

Why would that be needed?

I am wondering whether it would make sense to delete the TComparer overloads from the public surface for now until we can get the right implementation for them in place. I think they can stay, but just wanted to bring it up.

cc @eiriktsarpalis

@ghostghost locked as resolved and limited conversation to collaborators Dec 8, 2020
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.RuntimeNO-MERGEThe PR is not ready for merge yet (see discussion for detailed reasons)tenet-performancePerformance related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants

@stephentoub@nietras@AndyAyersMS@jkotas@EgorBo@GrabYourPitchforks@danmoseley@tannergooding
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Fix regression in Array.Sort for floats/doubles - #37941

Merged
stephentoub merged 3 commits into
dotnet:masterfrom
stephentoub:fixsortperf_utilslessthanref
Jun 28, 2020
Merged

Fix regression in Array.Sort for floats/doubles#37941
stephentoub merged 3 commits into
dotnet:masterfrom
stephentoub:fixsortperf_utilslessthanref

Conversation

@stephentoub

@stephentoubstephentoub commented Jun 16, 2020

Copy link
Copy Markdown
Member

Several months back we moved the sorting logic for primtive types out of native code into managed. Doing so helped to make the logic reusable for spans and helped to reduce GC latency, and also actually helped with throughput in a variety of cases. But it ended up regressing throughput for sorting larger arrays of floating-point values, with float/double.CompareTo not getting inlined, and even if it were inlined, containing much more logic than was present in the native implementation. The native implementation did a pre-pass to move all NaNs to the front and then just used simple < and > comparison operations, so the managed implementation now does as well.

With the exception of a large array of already-sorted Int32 values where there is still a small regression after this PR, all of the cases I've tested are either as good or better than .NET Core 3.1.

@jkotas, @GrabYourPitchforks, @tannergooding, thanks for your offline suggestions on approaches here; I tried out a variety of them, including vectorized float/double.CompareTo as well as unsafe casts to wrapper types with customized IComparable implementations, and this ended up being the best overall. Thanks as well to @nietras for pointing out the regression.

Benchmark:

usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;usingSystem;usingSystem.Diagnostics.CodeAnalysis;usingSystem.Linq;[MemoryDiagnoser]publicclassProgram{staticvoidMain(string[]args)=>BenchmarkSwitcher.FromAssemblies(new[]{typeof(Program).Assembly}).Run(args);}publicclassDoubleSorting:Sorting<double>{protectedoverridedoubleGetNext()=>_random.Next();}publicclassInt32Sorting:Sorting<int>{protectedoverrideintGetNext()=>_random.Next();}publicclassStringSorting:Sorting<string>{protectedoverridestringGetNext()=>string.Create(_random.Next(1,5),_random,(dest,r)=>{for(inti=0;i<dest.Length;i++)dest[i]=(char)('a'+r.Next(26));});}publicabstractclassSorting<T>{protectedRandom_random;privateT[]_orig,_array;[Params(10,100_000)]publicintSize{get;set;}protectedabstractTGetNext();[GlobalSetup]publicvoidSetup(){_random=newRandom(42);_orig=Enumerable.Range(0,Size).Select(_ =>GetNext()).ToArray();_array=(T[])_orig.Clone();Array.Sort(_array);}[Benchmark]publicvoidSorted()=>Array.Sort(_array);[Benchmark]publicvoidRandom(){_orig.AsSpan().CopyTo(_array);Array.Sort(_array);}}
TypeMethodToolchainSizeMeanRatio
DoubleSortingSortednetcore311059.58 ns2.13
DoubleSortingSortedmaster1030.52 ns1.09
DoubleSortingSortedpr1028.01 ns1.00
DoubleSortingRandomnetcore311077.50 ns1.71
DoubleSortingRandommaster1067.61 ns1.49
DoubleSortingRandompr1045.44 ns1.00
DoubleSortingSortednetcore31100000990,325.98 ns1.27
DoubleSortingSortedmaster1000002,898,290.96 ns3.73
DoubleSortingSortedpr100000777,209.08 ns1.00
DoubleSortingRandomnetcore311000005,940,056.30 ns1.08
DoubleSortingRandommaster1000007,880,560.62 ns1.44
DoubleSortingRandompr1000005,473,335.58 ns1.00
Int32SortingSortednetcore311038.02 ns2.31
Int32SortingSortedmaster1017.09 ns1.04
Int32SortingSortedpr1016.49 ns1.00
Int32SortingRandomnetcore311049.97 ns1.63
Int32SortingRandommaster1031.30 ns1.02
Int32SortingRandompr1030.63 ns1.00
Int32SortingSortednetcore31100000572,908.37 ns0.90
Int32SortingSortedmaster100000640,966.00 ns1.00
Int32SortingSortedpr100000639,118.53 ns1.00
Int32SortingRandomnetcore311000005,072,236.56 ns1.06
Int32SortingRandommaster1000005,024,276.17 ns1.05
Int32SortingRandompr1000004,801,833.82 ns1.00
StringSortingSortednetcore3110575.87 ns1.24
StringSortingSortedmaster10432.40 ns0.93
StringSortingSortedpr10465.86 ns1.00
StringSortingRandomnetcore31101,758.64 ns1.15
StringSortingRandommaster101,425.75 ns0.93
StringSortingRandompr101,532.01 ns1.00
StringSortingSortednetcore3110000083,774,554.44 ns1.14
StringSortingSortedmaster10000074,485,247.25 ns1.01
StringSortingSortedpr10000073,450,518.68 ns1.00
StringSortingRandomnetcore31100000103,108,672.31 ns1.14
StringSortingRandommaster10000089,373,058.33 ns0.99
StringSortingRandompr10000090,577,543.59 ns1.00

@stephentoubstephentoub added this to the 5.0.0 milestone Jun 16, 2020

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

Nice!

@stephentoub
stephentoubforce-pushed the fixsortperf_utilslessthanref branch from 046f85c to 07dcfe1CompareJune 17, 2020 02:27
@stephentoub
stephentoubforce-pushed the fixsortperf_utilslessthanref branch from 07dcfe1 to 04e1399CompareJune 17, 2020 09:43
@nietras

Copy link
Copy Markdown
Contributor

@stephentoub perhaps consider testing performance of following type too:

publicclassComparableClassInt32:IComparable<ComparableClassInt32>{publicreadonlyintValue;publicComparableClassInt32(intvalue)=>Value=value;publicintCompareTo(ComparableClassInt32other)=>Value.CompareTo(other.Value);}

basic reference type overhead. Since string compares are "slow" you won't see possible reference type regressions for this. Just a suggestion :)

@stephentoub

Copy link
Copy Markdown
MemberAuthor

Just a suggestion :)

A knowing smile? 😉 Yes, this case regresses. It appears to be due to dictionary lookups when calling LessThan/GreaterThan, which also prevent inlining. Evaluating options...

@nietras

Copy link
Copy Markdown
Contributor

knowing smile? 😉

Ha yeah 😉 Back in 2018 I went through all "stages of inlining": huh?, wuhuu! (AggressiveInlining), doh! (reference type regression), oh come on! (JIT-me-not issues) 😅

Looking forward to what you come up with. 😀

@stephentoubstephentoub added the NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) label Jun 18, 2020
@stephentoub

Copy link
Copy Markdown
MemberAuthor

#38229 will hopefully be the solution here.

@nietras

Copy link
Copy Markdown
Contributor

Nice 👍 Now if #35791/#10048 were resolved too, we would have something to talk about 😉 Happy to help with that if I could get some pointers 😀

@AndyAyersMS

Copy link
Copy Markdown
Member

Happy to help with that if I could get some pointers

I'm certainly open to reconsidering #10048. The changes are simple enough, though I might end up restricting it to AggressiveInlining callees since the jit underestimates the size impact of a delegate invoke.

But I still don't have a clear picture of how allowing this actually provides benefit -- it sounds like you think with this we can unify some code and either make it perform better or at least not lose any perf. So perhaps a benchmark along these lines would be instructive?

@stephentoub

Copy link
Copy Markdown
MemberAuthor

it sounds like you think with this we can unify some code and either make it perform better or at least not lose any perf.

Not to put words in @nietras mouth, but I expect what he's hoping to do is improve the Array.Sort(..., IComparer<T>) code paths. Today we have one sort implementation that covers both providing an IComparer<T> and a Comparison<T> (a delegate); the former is implemented by creating a delegate to its Compare method, which means Array.Sort(..., IComparer<T>) is allocating. On top of that, we just recently added a span-based sort, and it's currently defined with a generic TComparer : IComparer<T> comparer, with the idea that you could provide a struct-based comparer and it wouldn't allocate... unfortunately, it's the worst of all worlds right now, in that we box the TComparer into an IComparer<T>, and then create a delegate from its Compare method. If all of these code quality issues got sorted out, you could imagine we just had a single code path for TComparer that was written generically to use its Compare sans boxing, and then if you used the Comparison<T> overload, we'd create a struct-based TComparer that just wrapped that Comparison<T> to invoke it. If we everything worked out really well, it could potentially even be unified with the separate, duplicate code path we currently have that targets T : IComparable<T>, using a TComparer comparer struct that delegated to the T : IComparable<T> implementation.

So perhaps a benchmark along these lines would be instructive?

Sounds like a good thing to add to dotnet/performance.

@nietras

nietras commented Jun 23, 2020

Copy link
Copy Markdown
Contributor

If all of these code quality issues got sorted out, you could imagine we just had a single code path for TComparer that was written generically to use its Compare sans boxing, and then if you used the Comparison overload, we'd create a struct-based TComparer that just wrapped that Comparison to invoke it. If we everything worked out really well, it could potentially even be unified with the separate, duplicate code path we currently have that targets T : IComparable, using a TComparer comparer struct that delegated to the T : IComparable implementation.

@stephentoub exactly. :) Although, I am not sure we could unify on TComparer as such, but potentially yes. This would be the fulfillment of the API I have proposed and the on/off work I have been doing on this for the last 3 years... failing due to the many issues around inlining. At the very minimum we could replace the Comparison<T> path with the TComparer path without duplicating code.

Note this is not just about sorting. Sorting, however, for me is a good example of where .NET comes short. I use the value type as a inlineable "functor" pattern for data processing algorithms. Think loops over millions of elements. Unfortunately, we can't unify on this pattern due to these kinds of issues, which Sort exemplifies very well. Lots of other devs use this pattern, but we often end up having to duplicate code, and since this is code that is combinatorial on rank, different kinds of transformations etc. it adds up. It's a lot of replicated code. I have talked about this before and don't want to sound like a broken record player 😅

with the idea that you could provide a struct-based comparer and it wouldn't allocate...

It's not just the allocation. It's so the compare can be inlined. So the "functor" can be applied inlined. To yield a customized loop for performance. As you probably know.

perhaps a benchmark along these lines would be instructive?

@AndyAyersMS I don't know what kind of benchmark you are thinking about but something simple like below shows the issue.

usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;usingSystem;usingSystem.Collections.Generic;usingSystem.Runtime.CompilerServices;namespaceCompareBenchmarking{publicclassProgram{staticvoidMain(string[]args)=>BenchmarkSwitcher.FromAssemblies(new[]{typeof(Program).Assembly}).Run(args);}publicclassCompareFloat:Compare<float>{protectedoverridefloatGetNext()=>_random.Next();}publicclassCompareInt32:Compare<int>{protectedoverrideintGetNext()=>_random.Next();}publicstructComparisonComparer<T>:IComparer<T>{readonlyComparison<T>_comparison;publicComparisonComparer(Comparison<T>comparison)=>_comparison=comparison;[MethodImpl(MethodImplOptions.AggressiveInlining)]publicintCompare(Tx,Ty)=>_comparison(x,y);}[MemoryDiagnoser][DisassemblyDiagnoser]publicabstractclassCompare<T>whereT:IComparable<T>{staticreadonlyComparer<T>_comparer=Comparer<T>.Default;staticreadonlyComparison<T>_comparison=Comparer<T>.Default.Compare;readonlyComparisonComparer<T>_comparisonComparer=newComparisonComparer<T>(_comparison);protectedRandom_random;T_x;T_y;protectedabstractTGetNext();[GlobalSetup]publicvoidSetup(){_random=newRandom(42);_x=GetNext();_y=GetNext();}[Benchmark]publicintCompareTo()=>_x.CompareTo(_y);[Benchmark]publicintComparer()=>_comparer.Compare(_x,_y);[Benchmark(Baseline=true)]publicintComparison()=>_comparison(_x,_y);[Benchmark]publicintComparisonComparer()=>_comparisonComparer.Compare(_x,_y);}}

With the following results on .NET 5.0 Preview 2. The factor of 2.31x pretty much says it all although that would be pretty self-evident given the extra indirection and code generation issues. Just imagine this in a tight loop. :)

BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19041.329 (2004/?/20H1)
Intel Core i7-8700 CPU 3.20GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores
.NET Core SDK=5.0.100-preview.2.20176.6
[Host] : .NET Core 5.0.0 (CoreCLR 5.0.20.16006, CoreFX 5.0.20.16006), X64 RyuJIT
DefaultJob : .NET Core 5.0.0 (CoreCLR 5.0.20.16006, CoreFX 5.0.20.16006), X64 RyuJIT

CompareInt32

MethodMeanErrorStdDevRatio
CompareTo0.4995 ns0.0051 ns0.0048 ns0.38
Comparer1.1854 ns0.0032 ns0.0028 ns0.89
Comparison1.3258 ns0.0065 ns0.0054 ns1.00
ComparisonComparer3.0613 ns0.0096 ns0.0089 ns2.31

CompareFloat

MethodMeanErrorStdDevRatioRatioSD
CompareTo1.237 ns0.0018 ns0.0015 ns0.600.00
Comparer1.905 ns0.0628 ns0.0557 ns0.930.03
Comparison2.060 ns0.0047 ns0.0039 ns1.000.00
ComparisonComparer3.479 ns0.0096 ns0.0090 ns1.690.01

If this is interesting I can make a PR to the benchmark repo.

@stephentoub
stephentoubforce-pushed the fixsortperf_utilslessthanref branch from 04e1399 to 356828cCompareJune 25, 2020 22:27
@stephentoub
stephentoubforce-pushed the fixsortperf_utilslessthanref branch from 356828c to a3e3d79CompareJune 26, 2020 21:34
AndyAyersMS added a commit to AndyAyersMS/runtime that referenced this pull request Jun 26, 2020
RyuJit would not inline methods that contained delegate invokes. Remove
this limitation.
Closesdotnet#10048. See also dotnet#37941.
jkotas pushed a commit that referenced this pull request Jun 27, 2020
RyuJit would not inline methods that contained delegate invokes. Remove
this limitation.
Closes#10048. See also #37941.
Several months back we moved the sorting logic for primtive types out of native code into managed. Doing so helped to make the logic reusable for spans and helped to reduce GC latency, and also actually helped with throughput in a variety of cases. But it ended up regressing throughput for sorting larger arrays of floating-point values, with float/double.CompareTo not getting inlined, and even if it were inlined, containing much more logic than was present in the native implementation. The native implementation did a pre-pass to move all NaNs to the front and then just used simple < and > comparison operations, so the managed implementation now does as well.
@stephentoub
stephentoubforce-pushed the fixsortperf_utilslessthanref branch from a3e3d79 to 2bec1bfCompareJune 27, 2020 23:57
@stephentoub

Copy link
Copy Markdown
MemberAuthor

I moved the comparer functions into the two generic helper classes, and with #38229 (thanks, @jkotas), the reference-type-Int32-wrapper case is good again, with everything else being appx what it was before as well.

I'll merge when this is green.

@stephentoub
stephentoub merged commit e1c9ab4 into dotnet:masterJun 28, 2020
@stephentoub
stephentoub deleted the fixsortperf_utilslessthanref branch June 28, 2020 01:57
@nietras

Copy link
Copy Markdown
Contributor

@stephentoub should I try make a PR for the TComparer change? Is there interest in getting this in? The change itself to TComparer is easy enough, it's the whole sort helpers creation etc. that needs to change a lot, and to support reference type delegate comparer will likely require a "unsafe" cast of Comparison<T> to Comparison<object>. Is that acceptable?

Also thank you for the mention in your awesome Performance Improvements in .NET 5 :)

@jkotas

jkotas commented Jul 16, 2020

Copy link
Copy Markdown
Member

Hi @nietras,

@stephentoub is OOF.

I think it would be a good idea to start the PR for the TComparer changes so that we can start sorting out the code quality issues that it is likely to expose. Preferably, we would fix them instead of working around them by duplicating large amounts of code.

I have opened #39466 to have this tracked. For time line, I do not expect we would be able to get this change into .NET 5.

"unsafe" cast of Comparison<T> to Comparison<object>

Why would that be needed?

I am wondering whether it would make sense to delete the TComparer overloads from the public surface for now until we can get the right implementation for them in place. I think they can stay, but just wanted to bring it up.

cc @eiriktsarpalis

@ghostghost locked as resolved and limited conversation to collaborators Dec 8, 2020
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.RuntimeNO-MERGEThe PR is not ready for merge yet (see discussion for detailed reasons)tenet-performancePerformance related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants

@stephentoub@nietras@AndyAyersMS@jkotas@EgorBo@GrabYourPitchforks@danmoseley@tannergooding