runtime: Double.CompareTo() performance hurt by not being inlined

I was profiling some Paint.NET code in WPA the other day and noticed that Double.CompareTo() was taking a non-trivial amount of the execution time:

image

The scenario here in the app is to select an area of the canvas (Rectangle Select, Ellipse Select, Magic Wand, whatever), switch to the Move Selected Pixels too, and do some transforms on it (rotate, scale, etc.).

The selection is represented by a poly-polygon (IReadOnlyList<Point2Double[]>) which is then scan-converted into an alpha mask (bitmap). Scan-conversion is the standard algorithm for doing this and involves lots of sorting, at one point by Y values and then repeatedly by X values. Double.CompareTo() is used quite a bit here, in other words. In the screenshot, you can see that the most time is spent in 1) filling an alpha mask bitmap from the list of scans/rectangles from the scan-conversion code (MaskFromScansRenderer) (this code is vectorized quite a bit now, lots of buffer filling), 2) rendering the transformed pixels onto the current layer’s bitmap (TransformedNearestNeighborContentRenderer), 3) some other rendering/compositing and hit-testing (MoveToolContentRenderer), then 4) System.Double::CompareTo() (!!!), and then most of the rest is the actual scan-conversion, sorting, and some more buffer copying.

To investigate further, I crafted up a toy benchmark in LINQPad where I timed how long it took to sort an array of 10 million randomly generated doubles (10 iterations per benchmark). All the usual perf tricks were employed, including pointers to elide bounds checks, and an optimized copy of Sort<T, TList, TComparer>() where everything is structs, interfaces, and tagged with AggressiveInlining. For the first benchmark, the stock Double.CompareTo() is used, and for the second benchmark a copy of it with [MethodImpl(MethodImplOptions.AggressiveInlining)] is employed:

My results:

image

(you can ignore the CompareNoNaN result, it was just for fun to see what happens if I remove the branches that test for NaN)

More than twice as fast – indicating that the overhead of not inlining Double.CompareTo() is substantial.

My hypothesis here is that adding [MethodImpl(MethodImplOptions.AggressiveInlining)] to Double.CompareTo() (and probably Single.CompareTo()) would be an easy and impactful win. I suspect that these methods aren’t used extensively throughout the runtime or other codebases, so the codegen size impact should be small, and where they are used the performance gain would be worth it.

I’m happy to craft up a proper benchmark with real data that others can inspect. I’m submitting this issue so I stop procrastinating on it 😃

cc @EgorBo, @tannergooding , who were part of the conversation about this on Discord #lowlevel

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Comments: 18 (17 by maintainers)

Commits related to this issue

Most upvoted comments

Maybe Paint.NET should be a training scenario somehow?

We rebuilt the whole PGO system in .NET 6. I have been thinking about how we can take advantage of real apps going forward. Clearly, Paint.NET being on .NET 5+ is a big win for that.

In the future, I’d like to establish a model where we can change PGO data as part of a servicing update. We’d need a compelling and coherent plan for that (which I don’t have), but that’s the direction I have in mind.

@davidwrighton

This isn’t critical for me – I made my own InlinedCompareTo method that I’m using for now inside my app.

Thanks @rickbrew. I set the milestone to .NET 7. @tannergooding I assigned this issue to you to review the PR and merge it.

We can certainly arrange that, @richlander and I already have something in place to enable that sort of thing

@rickbrew what is the perf of adding inlining to Single.CompareTo?

For the sorting benchmark the results were near identical. Same amount of time, same amount of % improvement.