Skip to content

Add AggressiveInlining to Double.CompareTo() and Single.CompareTo() - #56501

Merged
tannergooding merged 3 commits into
dotnet:mainfrom
rickbrew:main
Aug 20, 2021
Merged

Add AggressiveInlining to Double.CompareTo() and Single.CompareTo()#56501
tannergooding merged 3 commits into
dotnet:mainfrom
rickbrew:main

Conversation

@rickbrew

Copy link
Copy Markdown
Contributor

As per discussion in #56493

@ghost

Copy link
Copy Markdown

I couldn't figure out the best area label to add to this PR. If you have write-permissions please help me learn by adding exactly one area label.

@ghostghost added the community-contribution Indicates that the PR has been added by a community member label Jul 28, 2021
@dnfadmin

dnfadmin commented Jul 28, 2021

Copy link
Copy Markdown

CLA assistant check
All CLA requirements met.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Double.cs
@stephentoub

Copy link
Copy Markdown
Member

@ghost

Copy link
Copy Markdown

Tagging subscribers to this area: @dotnet/area-system-runtime
See info in area-owners.md if you want to be subscribed.

Issue Details

As per discussion in #56493

Author:rickbrew
Assignees:-
Labels:

area-System.Runtime, community-contribution

Milestone:-

@stephentoubstephentoub added the tenet-performance Performance related issue label Jul 29, 2021
@GrabYourPitchforks

Copy link
Copy Markdown
Member

I agree with Tanner that special-casing this in the JIT to emit specialized instructions would result in the best codegen. I don't know how difficult that would be in practice, as you don't want to end up in a situation where we're really effective at setting eax to the appropriate value, but where we still incur the series of jumps after the fact. Ideally you'd want the JIT to be able to generate jumps directly to the applicable targets.

@stephentoub

Copy link
Copy Markdown
Member

Sounds like the general consensus is this should be merged as-is but there should be an issue for .NET 7 tracking making CompareTo an intrinsic?

@rickbrew

Copy link
Copy Markdown
ContributorAuthor

Sounds like the general consensus is this should be merged as-is but there should be an issue for .NET 7 tracking making CompareTo an intrinsic?

Do you want to changes you suggested above with splitting NaN handling into its own method?

@stephentoub

Copy link
Copy Markdown
Member

I'll defer to Tanner/Levi's judgement on that.

@rickbrew

Copy link
Copy Markdown
ContributorAuthor

If the method is split, as you suggested, I believe performance would remain about the same for datasets with large amounts (>99%, although I'm guessing/estimating here) of NaN values, although codegen size would increase due to inlining the first part of the method. But, sorting a list of all NaNs seems like an uncommon/pathological corner case that doesn't need to be considered w.r.t. optimization? @tannergooding might know better though.

@tannergooding

tannergooding commented Jul 30, 2021

Copy link
Copy Markdown
Member

I agree that even when you have NaN it's likely to be more of a corner case than not. However, I would like to see more numbers/data of how this impacts sparse data sets or other scenarios first.

Likewise, I think that overall the best thing is to get the JIT to treat this intrinsicly. Today we have:

if(m_value<value)return-1;if(m_value>value)return1;if(m_value==value)return0;// At least one of the values is NaN.if(double.IsNaN(m_value))returndouble.IsNaN(value)?0:-1;elsereturn1;

which generates assembly that is effectively:

 L0000: vzeroupper L0003: vmovsd xmm0,[rcx] L0007: vucomisd xmm1,xmm0 L000b: ja short L0029 L000d: vucomisd xmm0,xmm1 L0011: ja short L0032 L0013: vucomisd xmm0,xmm1 L0017: jp short L001b L0019: je short L002f L001b: vucomisd xmm0,xmm0 L001f: jp short L0023 L0021: je short L0032 L0023: vucomisd xmm1,xmm1 L0027: jp short L002f L0029: moveax,0xffffffff L002e: ret L002f: xoreax,eax L0031: ret L0032: moveax,1 L0037: ret

While in practice, we only need no more than 3 ucomisd (1 if neither input is NaN) and then a brief set of jumps and we could generate something more like:

entry:xoreax,eax ; Clear result to zero vmovsd xmm0,[rcx] ; Load "this" into xmm0 vucomisd xmm0,xmm1 ; Compare "this" to "value"jp nan ; Parity flag is set, one input is NaN (unpredicted)jc less_than ; Carry flag is set, "this" is less than "value"jnz greater_than ; Zero flag is not set, "this" is greater than "value"ret ; Return value is already zero (inputs are equal)greater_than:inceax ; Return value should be oneretless_than:deceax ; Return value should be negative oneretnan: vucomisd xmm0,xmm0 ; Compare "this" to "this"jnp greater_than ; Parity flag is not set, "this" is not NaN; "value" is a NaN vucomisd xmm1,xmm1 ; Compare "value" to "value"jnp less_than ; Parity flag is not set, "this" is NaN; "value" is not a NaNret ; Return value is already zero (inputs are both NaN)

This would result in approx. 36 bytes of assembly which is down from 56 bytes. It has significantly fewer comparisons which saves us a number of cycles (~3-7 cycles per ucomis on modern CPUs) and many fewer branches in the "tight" window which should improve the branch predictor throughput. -- This could probably be even more efficient and branch predictor friendly with CMOVcc, but we don't generate those today.

Intrinsicly recognizing CompareTo might also be better for scenarios where users are relying on CompareTo for comparison, as we could then correctly optimize something like if (value.CompareTo(other) < 0) to be just value < other and not generate the rest of the code. Although I'm not sure how useful that would be in practice.

Likewise, even without intrinsifying, we should get the JIT to understand cases like:

 vucomisd xmm0,xmm0jp short label1je short label2label1: vucomisd xmm1,xmm1 ; more codelabel2: ; more code

and have it generate:

 vucomisd xmm0,xmm0je short label2 vucomisd xmm1,xmm1 ; more codelabel2: ; more code

@danmoseley

Copy link
Copy Markdown
Contributor

@rickbrew it sounds like the next action here is to do some quick perf tests including lots of NaN's.

@tannergooding do we not have any perf tests for comparing numbers? Maybe I'm missing them..
https://github.com/dotnet/performance/blob/main/src/benchmarks/micro/libraries/System.Runtime/Perf.Double.cs

@danmoseley

Copy link
Copy Markdown
Contributor

@rickbrew do you expect to be able to gather this data in time to possibly get this into 6.0?

@rickbrew

Copy link
Copy Markdown
ContributorAuthor

@danmoseley Cutoff for .NET 6 is 8/17? No, that won't be possible for me.

I also wasn't sure if that was up to me or @tannergooding, since #56493 was assigned to him. Chalk it up to being a first-timer in the dotnet/runtime repo.

I can still take a look in a bit, I've been very busy finishing, preparing, and stabilizing Paint.NET 4.3 (finally ported to .NET 5)

@danmoseley

Copy link
Copy Markdown
Contributor

Congratulations on the port! Do you notice any differences compared to running it on .NET Framework?

@rickbrew

rickbrew commented Aug 15, 2021

Copy link
Copy Markdown
ContributorAuthor

Oh yes, lots. It's faster, maybe by about 15% across the board before doing any further optimizations that weren't possible with Framework.. Except for startup performance which has about a 30% penalty due to using a lot of C++/CLI code, which can't be crossgen'd. I interop heavily with the likes of D2D, WIC, et. al. That will slowly evaporate/improve as I port all of that over to C# (@tannergooding 's TerraFX will be put to good use).

SCD is fantastic for simplifying and alleviating a lot of various install issues. Installation and updating is much faster because I crossgen on my build machine instead of NGEN on the user's system. I've also been able to sink a lot of time into SIMD optimizations, both to add newly optimized code paths and to port many others from native C code that I was p/invoking. ARM64 support is great, really glad that landed in 5.0.9 for WinForms and WPF. Being able to load plugins into isolated AssemblyLoadContexts is a good thing and solves a lot of issues.

Plugin compatibility remains a struggle, although most work fine as-is w/o recompilation (including super old .NET 2.0 or even 1.1 DLLs). Some framework DLLs have been removed (System.Windows.Forms.DataVisualization, WCF), some classes have been removed (Menu and ContextMenu), and a few things have bugs in them (XmlSerializer dies when used in a collectible AssemblyLoadContext, but that's fixed in .NET 6 and I have a workaround). I'm steadily working through as many of those as possible with various workarounds, fixes, or even hot patching via Mono.Cecil. Some plugin authors will be putting out new updates, while others have wandered elsewhere in life and so their plugins will need app-side fixes/workarounds/shims.

Download size is also a struggle. SCD grows my installer from ~12MB to >50MB, so hopefully my hosting provider doesn't get stressed out. I'm also actively pursuing ways to trim the size through various creative methods (e.g. #56699).

Having access to all the new stuff in the runtime and framework, more libraries on nuget, and the massively improved JIT/codegen is really great. It finally feels like I'm working with the runtime now, instead of working around it.

@danmoseley

Copy link
Copy Markdown
Contributor

That's a great result so far thank you for sharing cc @richlander although I'm guessing he's aware of your progress.

@danmoseley

Copy link
Copy Markdown
Contributor

@tannergooding i will be out next week. Do you think it would be reasonable to take this change Monday with the evidence we have? It seems we have daily good confidence it will on balance be a benefit. Your call.

@danmoseley

danmoseley commented Aug 15, 2021

Copy link
Copy Markdown
Contributor

One other question @rickbrew as it wasn't clear - have you experimented with trimming before crossgen? We did a lot of work to make the shared framework safe to trim. WPF/Winforms are not as you likely know. Cc @eerhardt

@rickbrew

rickbrew commented Aug 15, 2021

Copy link
Copy Markdown
ContributorAuthor

@danmoseley I haven't tried trimming just yet, partly because I couldn't get ILLink's command-line parameters to work. But also, I suspect PDN's trimmability is low because of the plugin situation. They need access to both PDN and runtime/framework DLLs. I may look into it more later, after 4.3's release, maybe after .NET 6's release. I see value in being able to trim libraries like Newtonsoft.Json.dll and will definitely need it when I start using TerraFX.Interop.dll (8MB!), neither of which plugins would be using (and they can have their own private copy now anyway, thanks to AseemblyLoadContext).

Also, the biggest bang-for-buck action I can see right now is trimming out R2R data from large framework DLLs that are not used, or not used enough, at least not on the startup path (e.g. PresentationFramework.dll is 17MB, but only 6MB w/o R2R data, see also #56699). An uncrossgen utility would be really helpful to enable an easy, impactful win.

@rickbrew

rickbrew commented Aug 15, 2021

Copy link
Copy Markdown
ContributorAuthor

I decided to start running benchmarks and gathering the data anyway, as I could use a break from the PDN stuff.

Anyway the first result I'm seeing is that @stephentoub 's "split inlined" idea is actually faster than the fully inlined version, at least on NaN-less data. Still fleshing out the benchmark to get data on various mixtures of NaNs in the data, but I wanted to share that first before anything got merged. nope, that wasn't necessarily right, I'll wait until I've got enough data to profess any actual conclusions 😂

@rickbrew

rickbrew commented Aug 15, 2021

Copy link
Copy Markdown
ContributorAuthor

I've created some benchmark code in this repository: https://github.com/rickbrew/CompareInliningBenchmarks

VS 2022 17.0.0 Preview 3.0
Ryzen 5950X, PBO enabled, 64GB DDR4-3600 RAM
Because PBO (precision boost overdrive, aka auto-overclocking) is temperature sensitive, I'm running the CPU fans at max to help it run at a consistent clock speed. Otherwise it might run at a slower speed later in the benchmark run. There's also a portable AC unit in the room.

The benchmark consists of creating an array with several million elements, sized so as to fit just within the 32MB L3 cache available to each chiplet on the CPU. The array is filled with values 0 through N-1 (ints cast to float/double). The NaNPercentage determines how many NaNs are evenly spread throughout the array. Then the array is shuffled using Fisher-Yates. The random seed is the same every time, for both floats and doubles.

The implementation of Sort() is Timsort, pulled from my Paint.NET codebase, which has full interfaces+structs+generics+inlining. It is not the exact same as the framework's Sort(), which I'm told boxes the comparer (which would be very bad for this benchmark).

Here are the results for doubles with 4M elements in the array:

BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19043.1165 (21H1/May2021Update)
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK=6.0.100-preview.7.21379.14
[Host] : .NET 6.0.0 (6.0.21.37719), X64 RyuJIT
Job-JXESFV : .NET 6.0.0 (6.0.21.37719), X64 RyuJIT
MethodNaNPercentageMeanErrorStdDevMedianRatio
SortStandard0296.9 ms2.36 ms2.20 ms297.6 ms1.00
SortFullyInlined0192.4 ms3.58 ms3.35 ms191.4 ms0.65
SortSplitInlined0197.1 ms2.74 ms2.57 ms197.2 ms0.66
SortStandard10299.1 ms2.45 ms2.29 ms299.6 ms1.00
SortFullyInlined10193.1 ms3.20 ms2.99 ms192.6 ms0.65
SortSplitInlined10197.4 ms3.03 ms2.84 ms196.3 ms0.66
SortStandard20295.4 ms4.62 ms4.33 ms295.2 ms1.00
SortFullyInlined20187.6 ms3.65 ms4.21 ms186.8 ms0.64
SortSplitInlined20192.1 ms2.60 ms2.43 ms192.2 ms0.65
SortStandard30293.2 ms5.50 ms5.40 ms291.7 ms1.00
SortFullyInlined30177.0 ms3.54 ms7.14 ms173.9 ms0.63
SortSplitInlined30192.1 ms3.82 ms6.69 ms189.9 ms0.68
SortStandard40275.6 ms2.54 ms2.26 ms275.7 ms1.00
SortFullyInlined40166.0 ms3.29 ms5.94 ms163.9 ms0.63
SortSplitInlined40184.7 ms3.62 ms4.17 ms184.7 ms0.68
SortStandard50271.7 ms5.40 ms10.27 ms268.9 ms1.00
SortFullyInlined50158.7 ms3.17 ms6.69 ms156.7 ms0.59
SortSplitInlined50180.1 ms3.60 ms7.18 ms177.6 ms0.67

And for floats with 8M elements in the array. Same amount of memory and bandwidth, but higher compute cost:

MethodNaNPercentageMeanErrorStdDevRatio
SortStandard0867.2 ms1.69 ms1.49 ms1.00
SortFullyInlined0640.1 ms2.66 ms2.22 ms0.74
SortSplitInlined0670.8 ms3.08 ms2.88 ms0.77
SortStandard10827.5 ms5.48 ms5.13 ms1.00
SortFullyInlined10642.7 ms3.07 ms2.87 ms0.78
SortSplitInlined10661.3 ms3.64 ms3.40 ms0.80
SortStandard20808.0 ms3.01 ms2.82 ms1.00
SortFullyInlined20593.6 ms2.23 ms2.08 ms0.73
SortSplitInlined20643.8 ms3.05 ms2.85 ms0.80
SortStandard30748.6 ms2.88 ms2.41 ms1.00
SortFullyInlined30565.9 ms2.54 ms2.38 ms0.76
SortSplitInlined30617.4 ms3.49 ms3.26 ms0.82
SortStandard40726.1 ms5.10 ms4.77 ms1.00
SortFullyInlined40504.0 ms1.86 ms1.74 ms0.69
SortSplitInlined40576.4 ms2.98 ms2.64 ms0.79
SortStandard50671.1 ms6.76 ms6.32 ms1.00
SortFullyInlined50456.7 ms1.51 ms1.34 ms0.68
SortSplitInlined50548.9 ms2.18 ms1.82 ms0.82

I'm not sure that having 50% NaNs (or even 10%!) is a realistic scenario, but it does illustrate some divergence in results between fully- and split-inlined.

I didn't go above 50% NaNs because by the time you reach 100% NaNs the data is fully sorted and then what's the point of sorting, and it didn't seem like a realistic scenario either.

@rickbrew

Copy link
Copy Markdown
ContributorAuthor

So this illustrates that for sorting an array of elements, inlining CompareTo() has great results. It's hard to say what the impact would be in other scenarios that have more/less reliance on CompareTo() for their performance.

@tannergooding

Copy link
Copy Markdown
Member

Given the above, and that split inlining is still a lot faster than no inlining, I think its reasonable as a stop gap.

@danmoseley, do you have any concerns with us merging this for RC1? If the official benchmarks show any regression not covered here or on older hardware, we'd likely need to pull it back out.

return CompareToWithAtLeastOneNaN(value);
}

private int CompareToWithAtLeastOneNaN(double value)

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.

Does the manual split inlining actually make the code significantly smaller in real world situations vs. just aggressive inlining the whole thing? The call introduced by the split inlining is going to have secondary effect like extra register spilling, so it is not obvious to me that it is actually profitable.

@tannergoodingtannergoodingAug 17, 2021

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.

The entire inlined assembly is fairly large: #56501 (comment)

NaN checks in particular aren't done very efficiently today and we should try to improve it in .NET 7. So with the partial inlining, the call will remove 2-4 extra branches from the inlined code-path (in favor of a call which may spill on a probably unlikely path).

In the link, I gave an example of what we could generate for this entire comparison if we did something "more optimally".

@rickbrewrickbrewAug 17, 2021

Copy link
Copy Markdown
ContributorAuthor

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.

I don't have a particular preference on which we go with or if we hold off and wait for the JIT to make this more intrinsic or even simply improve the general handling for these kinds of checks and branches.

@jkotasjkotasAug 17, 2021

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.

https://sharplab.io/#v2:EYLgxg9gTgpgtADwGwBYA0AXEBDAzgWwB8ABAJgEYBYAKGIAYACY8gOgCUBXAOwwEt8YLAMIR8AB14AbGFADKMgG68wMXAG4aNYgGYmpBkIYBvGgzNNdxFAwCyACgCUx0+YC+Nd9S27cGKBzAMBgAVVQwaE2pzJnIkBmAICEkGAElcWWgMGAATAH0RcWxYYIgAMQ5JSQBPFK5JXi4cuzYYbGyAeTqq2TFsLgAebIgOYGkAPgZsBxczSOjogDNoBjsGoN4GAF4GOjUGDf7JlgAZGC4AcwwACwY4BnI93gBqJ+mo+Y/eBZWC3uKyirVWr1RrZOzYADavAAumhJlCGE97tCnIc6E5iAB2BgLbCSXAwDTvD5Yhh+DiEmYMVzRKlUiE2GDXCDZFLiSR2RnM1ns9piPgQLi4FgAQXO51guFwvAUMGBDQa5wc0KpOhicTWBlEfxgJXKlRqdQaTQaDAAIsNRjAGPhcgo8RS4RaRtIGPbJBS3tE5h99t87Lb3RSGIcgzAMdi4A8qdEvitAw7rRMwxH7kTfX743bE1ttimmNjdjHzAB6EsMEVBaR4IKC60Qb7Xa1h3D7VsAOWw7ZYxbMcbsQxdgjSnfbAezHvDXoz5lJg6tLBHXbs+YA/DsGCBbtHifMYPiYL35qSd9FPLTd8w4gkkql0pkcvltUVdRAevUMPLQc1Wh0uj0+kGS1xkmadZiPJYoBWTUNm2XZ9hDI5Tgua5bjTfYXjAmd+1+F8SnfXhPyNb9IRhOFSMRZFUR2VNcQPdNfVJclKV3GlzE0XcGSZK4WTZMQOS5HieX4vkBSFUVxUlaVZS/RVlVVSxYn2HgtUKf4CKIkETS4c1gOtBNJzhedXRTKkfU+f0DODUNE1TKMGIsrMwwYZNbILNMj37KzrU2PM3NJItd2iUlcP+AB1QirkrU4a06GBR3HMM4RTBzqQ46IxCgGVsCydVlKCULXwi65otaXw4oS01nStG0J0dBhjObWyzKPMsKyrMra0aBgGzJK4msnVteA7LseyC8x+0axdcAS7yHCwjM5z06aErXDct3so99wJI9guxU83A8IA=

shows the behavior of the two options in a simple real world case. Notice that the IsSorted_CompareToFullyInlined (code size 0x59) produces smaller code than IsSorted_CompareToSplitInlined (code size 0x6f). The obvious problem is that the slow path is a good inlining candidate and the JIT decides to inline it. Once you try to fix it by marking the slow path with NoInlining:

https://sharplab.io/#v2:EYLgxg9gTgpgtADwGwBYA0AXEBDAzgWwB8ABAJgEYBYAKGIAYACY8gOgCUBXAOwwEt8YLAMIR8AB14AbGFADKMgG68wMXAG4aNYgGYmpBkIYBvGgzNNdxFAwCyACgCUx0+YC+Nd9S27cGKBzAMBgAVVQwaE2pzJnIkBmAICEkGAElcWWgMGAATAH0RcWxYYIgAMQ5JSQBPFK5JXi4cuzYYbGyAeTqq2TFsLgAebIgOYGkAPgZsBxczSOjogDNoBjsGoN4GAF4GOjUGDf7JlgAZGC4AcwwACwY4BnI93gBqJ+mo+Y/eBZWC3uKyirVWr1RrZOzYADavAAumhJlCGE97tCnIc6E5iAB2BgLbCSXAwDTvD5Yhh+DiEmYMVzRKlUiE2GDXCDZFLiSR2RnM1ns9piPgQLi4FgAQXO51guFwvAUMGBDQa5wc0KpOhicTWBlEfxgJXKlRqdQaTQaDAAIsNRjAGPhcgo8RS4RaRtIGPbJBS3tE5h99t87Lb3RSGIcgzAMdi4A8qdEvitAw7rRMwxH7kTfX743bE1ttimmNjdjHzAB6EsMEVBaR4IKC60Qb7Xa1h3D7VsAOWw7ZYxbMcbsQxdgjSnfbAezHvDXoz5lJg6tLBHXbs+YA/DsGCBbtHifMYPiYL35qSd9FPLTd8w4gkkql0pkcvltUVdRAevUMPLQc1Wh0uj0+kGS1xkmadZiPJYoBWTUNm2XZ9hDI5Tgua5bjTfYXjAmd+1+F8SnfXhPyNb9IRhOFSMRZFUR2VNcQPdNfVJclKV3GlzE0XcGSZK4WTZMQOS5HieX4vkBSFUVxUlaVZS/RVlVVSxYn2HgtUKf4CKIkETS4c1gOtBNJzhedXRTKkfU+f0DODUNE1TKMGIsrMwwYZNbILNMj37KzrU2PM3NJItd2iUlcP+AB1QirkrU4a06GBR3HMM4RTBzqQ46IuO5PiBO43jeX5XhBWFdsIFki55N3MQoBlbAsnVZSglC18IuuaLWl8OKEtNZ0rRtCdHQYYzm1ssyjzLCsq3a2tGgYBsySuYbJ1bXgOy7HsgvMfshsXXAEu8hwsIzOc9J2hK1w3Ld7KPfcCSPYLsVPNwPCAA

it gets a bit better, but the code size of the non-split inlined version is still smaller.

I don't have a particular preference on which we go with or if we hold off and wait for the JIT to make this more intrinsic

I agree that it would be nice to teach the JIT to deal with the efficiently. If we want to tweak the performance by manual inlining, simple AggressiveInlining should be better than the manual split inlinining as my examples demonstrated.

@tannergoodingtannergoodingAug 17, 2021

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.

If we want to tweak the performance by manual inlining, simple AggressiveInlining should be better than the manual split inlinining as my examples demonstrated.

I'm fine with this. I would like to disagree on the examples adequately demonstrating the difference however as smaller code isn't necessarily better.

There are many factors that can impact perf here including the likelihood that a given path will be hit (NaNs are likely rare) and considerations such as the number of branches in a "small window" (16-32 aligned bytes of assembly). This was why I was initially hesitant about the change as it could regress certain "hot loops" due to the additional branches (7) that would now be present directly in the loop. Of course, the partial inlining could also impact this in interesting ways with its overall larger codegen but it does reduce the branch count by 3 compared to full inlining.

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.

I agree that you would be likely able to come up with cases where the split inlining is better due for micro-architecture reasons, if you tried hard enough. The data we have so far in this thread is that a simple AggressiveInlining produces faster and smaller code.

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.

To satisfy my own curiosity, I tried it in double.cs in corelib. Here main is what's currently checked in, pr1 is CompareTo aggressively inlined, and pr2 is it split.

privatedouble[]_doubles=Enumerable.Range(0,1_000_000).Select(i =>i*1.23).ToArray();privatedouble[]_scratch=newdouble[1_000_000];[Benchmark]publicboolIsSorted(){ReadOnlySpan<double>a=_doubles;for(inti=0;i<a.Length-1;i++)if(a[i].CompareTo(a[i+1])>0)returnfalse;returntrue;}[Benchmark]publicvoidCopyAndSort(){_doubles.CopyTo(_scratch,0);Array.Sort(_scratch);}[Benchmark]publicintSearch()=>Array.BinarySearch(_doubles,2_000_000);[Benchmark]publicintCompareSequence()=>_doubles.AsSpan().SequenceCompareTo(_doubles);
MethodToolchainMeanRatioCode Size
IsSortedmain\corerun.exe1,921,366.71 ns1.00155 B
IsSortedpr1\corerun.exe739,726.37 ns0.3997 B
IsSortedpr2\corerun.exe970,601.44 ns0.50117 B
CopyAndSortmain\corerun.exe11,761,377.29 ns1.001,060 B
CopyAndSortpr1\corerun.exe11,810,071.67 ns1.001,060 B
CopyAndSortpr2\corerun.exe11,837,290.42 ns1.011,060 B
Searchmain\corerun.exe58.53 ns1.00207 B
Searchpr1\corerun.exe35.51 ns0.61207 B
Searchpr2\corerun.exe38.65 ns0.66207 B
CompareSequencemain\corerun.exe1,829,633.22 ns1.00330 B
CompareSequencepr1\corerun.exe1,452,953.83 ns0.79306 B
CompareSequencepr2\corerun.exe1,523,349.48 ns0.83316 B

I asked whether it would make sense to split it. I'm fine with the answer being "no" 😄

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Alright I have reverted the commit that split the inlining, so we're back to just regular aggressive inlined

@danmoseley

Copy link
Copy Markdown
Contributor

@danmoseley, do you have any concerns with us merging this for RC1? If the official benchmarks show any regression not covered here or on older hardware, we'd likely need to pull it back out

Nope, it'd very low risk if we have to pull it out. If you've concluded it's a worthwhile change in the real world which it sounds like you have.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Single.cs
@tannergoodingtannergooding added this to the 7.0.0 milestone Aug 19, 2021
@ghost

Copy link
Copy Markdown

Hello @tannergooding!

Because this pull request has the auto-merge label, I will be glad to assist with helping to merge this pull request once all check-in policies pass.

p.s. you can customize the way I help with merging this pull request, such as holding this pull request until a specific person approves. Simply @mention me (@msftbot) and give me an instruction to get started! Learn more here.

@stephentoub

Copy link
Copy Markdown
Member

@tannergooding, is the plan to backport this?

@jeffhandley

Copy link
Copy Markdown
Member

@stephentoub Yep. @tannergooding and I chatted about it earlier. Before we merge into release/6.0 though, I'd like to make sure the latest perf run against main doesn't indicate any unintended regressions (even though likelihood is low).

@GrabYourPitchforks

Copy link
Copy Markdown
Member

Before we merge into release/6.0 though, I'd like to make sure the latest perf run against main doesn't indicate any unintended regressions

As @rickbrew mentioned earlier, we may see a minor size-on-disk regression as would accompany most "this method is now inlined where previously it wasn't" changes. I assume we're still ok with that?

@jeffhandley

Copy link
Copy Markdown
Member

Assuming it's truly minor, yeah.

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

Labels

area-System.Runtimecommunity-contributionIndicates that the PR has been added by a community membertenet-performancePerformance related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants

@rickbrew@dnfadmin@stephentoub@GrabYourPitchforks@tannergooding@danmoseley@jeffhandley@jkotas