Skip to content
This repository has been archived by the owner on Jan 23, 2023. It is now read-only.
/ corefx Public archive

Fix blittable array instead of copying #30099

Merged
merged 3 commits into from
Jun 6, 2018

Conversation

JeremyKuhne
Copy link
Member

System.Drawing is doing a lot of unnecessary work. This is a start on trying to remove some of it. This change is meant to show the general pattern I intend to follow.

  • Fixes the managed array and uses it directly instead of copying multiple times
  • Adds some basic sanity tests to validate that we're getting the expected output
  • Splits existing negative bezier tests into new file
  • Adds a performance project

Output tests were generated and run before making the functional change.

@JeremyKuhne
Copy link
Member Author

Drawing Beziers before:

  System.Drawing.Common.Performance.Tests.dll                      | Metric                                        | Unit  | Iterations |    Average | STDEV.S |        Min |        Max
  :---------------------------------------------------------------- |:--------------------------------------------- |:-----:|:----------:| ----------:| -------:| ----------:| ----------:
   System.Drawing.Tests.Perf_Graphics_DrawBeziers.DrawBezier_Points | Duration                                      | msec  |    100     |     38.219 |   0.565 |     36.993 |     40.016
   System.Drawing.Tests.Perf_Graphics_DrawBeziers.DrawBezier_Points | Allocation Size on Benchmark Execution Thread | bytes |    100     | 1.680E+006 |   0.000 | 1.680E+006 | 1.680E+006  

After:

  System.Drawing.Common.Performance.Tests.dll                      | Metric                                        | Unit  | Iterations | Average | STDEV.S |    Min |    Max
  :---------------------------------------------------------------- |:--------------------------------------------- |:-----:|:----------:| -------:| -------:| ------:| ------:
   System.Drawing.Tests.Perf_Graphics_DrawBeziers.DrawBezier_Points | Duration                                      | msec  |    100     |  34.866 |   0.980 | 33.530 | 40.966
   System.Drawing.Tests.Perf_Graphics_DrawBeziers.DrawBezier_Points | Allocation Size on Benchmark Execution Thread | bytes |    100     |   0.000 |   0.000 |  0.000 |  0.000

throw new ArgumentNullException(nameof(pen));
}
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure why these braces are being removed.

Copy link
Member Author

Choose a reason for hiding this comment

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

Not needed per our guidelines, pulls the argument validation in a bit.

Copy link
Member

Choose a reason for hiding this comment

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

But also at the cost of making the code bigger in collapsed view 😉

Copy link
Member Author

Choose a reason for hiding this comment

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

But also at the cost of making the code bigger in collapsed view

I'd call it a plus that you don't have to expand for one line.

Copy link
Member

Choose a reason for hiding this comment

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

It really comes down to personal preference 😄 . For example, I find the native code in CoreCLR significantly harder to navigate because of all the one liners that don't have braces.

Copy link
Member Author

Choose a reason for hiding this comment

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

the style guidelines don't say that this new version is preferred

Perhaps they should so we don't rathole on nits...

it's not actually fixing anything, just changing it to your preference

The diff isn't hard to follow. I think I'm entitled to cleaning up the methods I'm changing here for style consistency that I prefer and that I personally think makes an improvement in readability. I've made these methods concise & clean and made a major improvement. I'll end up changing most of the file, so Jeremy's style will be predominant in this one file. And if you look at the other changes I've made outside of this one, you'll see my style is predominant in the other files in this project. Shouldn't there be an argument for dll consistency as well?

the next person who comes along to edit this file might change it back

If someone else comes in and makes these methods even better than I did (improves them functionally), more power to them- I think they've earned the right to make a minor style tweak in their favor.

Quite frankly this is really disheartening. I've made a big improvement on the way to even more big improvements and the key feedback I've gotten is a long debate over whether I should have removed an unnecessary set of brackets.

All that said, I do understand the intent of the rules and your concerns. I do think about the impact of what I do under those specific criteria (among others) and I try to exercise my best judgement in balancing trade-offs. I think I've chosen well here, or at least well enough that further arguments for alternatives are just niggling.

Copy link
Member

Choose a reason for hiding this comment

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

Quite frankly this is really disheartening. I've made a big improvement on the way to even more big improvements and the key feedback I've gotten is a long debate over whether I should have removed an unnecessary set of brackets.

I'm sorry it's disheartening. That's certainly not the intent. I provided feedback on everything I had to provide feedback on: this, validating the HandleRef usage, and questioning the interop. Your original change (prior to updating it after questions) was only a few lines of product src, so not a ton to comment on, and this style change was a large portion of the diff. You chose to push back on the feedback, from multiple people. When I've received such feedback, I've accepted it and moved on.

Copy link
Member

@jkotas jkotas Jun 3, 2018

Choose a reason for hiding this comment

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

@JeremyKuhne Feedback similar to what you got here was given on number of occasions, by multiple different reviewers. We are just being consistent here. For example, here is my comment where I am asking Ashon to submit formatting changes separately from actual code changes: dotnet/coreclr#16223 (comment)

It is fine with me if you plan to make significant improvements in these files, want to claim their ownership and change them to your liking. Such style changes should be done in separate PR, and ideally the entire files should be changed to have consistent style, not just a few functions. Here are examples of PRs where component owners are changing the code to their liking #22471 or dotnet/coreclr#17451

Copy link
Member

Choose a reason for hiding this comment

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

@JeremyKuhne I suggest moving formatting changes in a separate PR. You're right that this code could be better formatted but reviewers are asking not to do it in this change.

Copy link
Member Author

Choose a reason for hiding this comment

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

We are just being consistent here. For example, here is my comment where I am asking Ashon to submit formatting changes separately from actual code changes

And in that comment you explicitly said to restrict formatting changes to the areas where you're actually changing code. Which I agree with fully and make every attempt to do and I believe I've done in this case. (And I've pushed others to do.)

It is fine with me if you plan to make significant improvements in these files, want to claim their ownership and change them to your liking. Such style changes should be done in separate PR, and ideally the entire files should be changed to have consistent style, not just a few functions.

I'm happy to do a follow up that makes a pass through the file(s) for consistency.

This caught me off guard. This is the first time I've been tagged for changing the formatting of the code local to what I'm working on (honestly, it is just one line away from "real" changes). It really bothers me as it seems super nitpicky/dogmatic- did you really have a hard time understanding what changed? To me I can't see how it is in the least bit confusing- perhaps I just don't have the right perspective.

I'll put back the brackets. Hopefully my time explaining my perspective isn't wasted. I think we need to be careful about dogma and it worries me that we make things unnecessarily difficult.

int status = SafeNativeMethods.Gdip.GdipDrawBeziersI(
new HandleRef(this, NativeGraphics),
new HandleRef(pen, pen.NativePen),
new HandleRef(this, new IntPtr(p)), points.Length);
Copy link
Member

Choose a reason for hiding this comment

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

Do you plan to also address use of HandleRef? That's an old pattern prior to SafeHandles.

Copy link
Member Author

Choose a reason for hiding this comment

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

Do you plan to also address use of HandleRef?

I'm cognizant about it. I'm not sure that I'll deal with it myself, but I'll at least open a follow-up issue.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'll at least clean out the HandleRef around the pinned arrays in the initial work.


IntPtr buf = SafeNativeMethods.Gdip.ConvertPointToMemory(points);
try
fixed (Point* p = points)
Copy link
Member

Choose a reason for hiding this comment

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

It looks like ConvertPointToMemory is doing some complicated marshaling. That's all unnecessary, and Point is blittable and matching the corresponding native types in layout?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, Point matches the native type and is blittable. I'm not sure how they stumbled into that loop originally.

Copy link
Member

Choose a reason for hiding this comment

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

The same pattern is used at about 60 places. It would be nice to get all of them fixed up.

Copy link
Member Author

Choose a reason for hiding this comment

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

The same pattern is used at about 60 places.

Yep. Following up on it right now. Just wanted to get an initial change up to show the general pattern of what I'm doing.

@JeremyKuhne
Copy link
Member Author

@dotnet-bot test OSX x64 Debug Build

Failed with #29940

using (var image = new Bitmap(10, 10))
using (Graphics graphics = Graphics.FromImage(image))
using (var pen = new Pen(Color.Red))
{
Copy link
Member

Choose a reason for hiding this comment

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

This hdc ought to have a disposable wrapper perhaps

Copy link
Member Author

Choose a reason for hiding this comment

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

The finally releases it, but it might be nice to add a helper.

@JeremyKuhne
Copy link
Member Author

Did another update:

  • Converted more usages
  • Stopped wrapping the pointers
  • Fully removed some of the helpers

Should be enough to see the pattern pretty clearly. There is a lot left to remove, but this is a good start I think. Significant cut in memory usage and should get at least 10% better performance with methods that work with arrays. That gain will get bigger the larger the arrays are.

@JeremyKuhne JeremyKuhne self-assigned this Jun 3, 2018
@JeremyKuhne
Copy link
Member Author

On the upper end, transforming points is wayyy faster (26X).

Before:

   System.Drawing.Common.Performance.Tests.dll                          | Metric                                        | Unit  | Iterations |    Average | STDEV.S |        Min |        Max
  :-------------------------------------------------------------------- |:--------------------------------------------- |:-----:|:----------:| ----------:| -------:| ----------:| ----------:
   System.Drawing.Tests.Perf_Graphics_TransformPoints.Transform_Points  | Duration                                      | msec  |    100     |     94.878 |   5.830 |     91.804 |    140.345
   System.Drawing.Tests.Perf_Graphics_TransformPoints.Transform_Points  | Allocation Size on Benchmark Execution Thread | bytes |    100     | 1.248E+007 |   0.000 | 1.248E+007 | 1.248E+007

After:

   System.Drawing.Common.Performance.Tests.dll                          | Metric                                        | Unit  | Iterations | Average | STDEV.S |    Min |    Max
  :-------------------------------------------------------------------- |:--------------------------------------------- |:-----:|:----------:| -------:| -------:| ------:| ------:
   System.Drawing.Tests.Perf_Graphics_TransformPoints.Transform_Points  | Duration                                      | msec  |    100     |   3.584 |   1.129 |  3.181 | 13.747
   System.Drawing.Tests.Perf_Graphics_TransformPoints.Transform_Points  | Allocation Size on Benchmark Execution Thread | bytes |    100     |   0.000 |   0.000 |  0.000 |  0.000

We're riddled with this sort of overhead in our oldest UI stacks. I hope to change that. 😜


PathData pathData = new PathData() { Types = new byte[numPts] };
if (count == 0)
return pathData;
Copy link
Member

Choose a reason for hiding this comment

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

How common is this case? If common, consider moving it earlier and doing:

if (count == 0)
{
    return new PathData() { Types = Array.Empty<byte>(), Points = Array.Empty<PointF>() };
}

Copy link
Member Author

Choose a reason for hiding this comment

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

Not common.

{
typesHandle.Free();
throw SafeNativeMethods.Gdip.StatusException(status);
Copy link
Member

Choose a reason for hiding this comment

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

There's a helper for this:

SafeNativeMethods.Gdip.CheckStatus(status);

internal static void CheckStatus(int status)
{
if (status != Ok)
{
throw StatusException(status);
}
}

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, I plan to rationalize out the variety of helpers in a separate change.

This is a start on trying to remove some of it. This change is meant to show the general pattern I intend to follow.

- Fixes the managed array and uses it directly instead of copying multiple times
- Adds some basic sanity tests to validate that we're getting the expected output
- Splits existing negative tests into new files with sanity tests
- Adds a performance project

Output tests were generated and run before making the functional change.
@JeremyKuhne
Copy link
Member Author

I've put back brackets around the if statements where I made no other change to said if statements.


try
fixed(PointF* p = pts)
Copy link
Member

Choose a reason for hiding this comment

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

Nit: space before (

@@ -378,5 +378,8 @@
<Reference Include="System" />
<Reference Include="System.Drawing" />
</ItemGroup>
<ItemGroup>
<Compile Include="System\Drawing\Internal\GpPathData.cs" />
Copy link
Member

Choose a reason for hiding this comment

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

If this is a Windows specific could you move it up to the ItemGroup conditioned to TargetWindows L207? if we plan to use this in Unix code as well, could you please move it up the the common ItemGroup L29?

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure

IntPtr buf = Marshal.AllocHGlobal(checked(count * size));
try
PointF[] points = new PointF[PointCount];
fixed(PointF* p = points)
Copy link
Member

Choose a reason for hiding this comment

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

Nit: space before (

Copy link
Member Author

Choose a reason for hiding this comment

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

D'oh, I searched and didn't click on this result. I'll get it in the next change.

IntPtr buf = SafeNativeMethods.Gdip.ConvertPointToMemory(pts);

try
fixed(Point* p = pts)
Copy link
Member

Choose a reason for hiding this comment

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

Nit: space before (


namespace System.Drawing.Tests
{
public abstract class DrawingTest
Copy link
Member

Choose a reason for hiding this comment

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

Should we name this DrawingTestBase?

Copy link
Member Author

Choose a reason for hiding this comment

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

I haven't used Base in other assemblies.

<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<BuildConfigurations>
netstandard;
Copy link
Member

Choose a reason for hiding this comment

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

Should we run the perf tests on netcoreapp only? The netstandard implementation is a turd assembly. In fact the regular tests only have a netcoreapp and desktop configurations

Copy link
Member Author

Choose a reason for hiding this comment

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

Do we run them anywhere else? I was just copy/pasting from FileSystem. If we can/should specify netcoreapp I'm happy to change it.

Copy link
Member

Choose a reason for hiding this comment

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

I’m don’t think we yet do but if in the future we start it might cause pain to whoever brings the infra to do so, but I can live with this being NS

{
int status = SafeNativeMethods.Gdip.GdipPathIterEnumerate(new HandleRef(this, nativeIter), out resultCount,
Copy link
Member

Choose a reason for hiding this comment

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

Would be nice to rename nativeIter to nativeIterator, or something like this, perhaps not in the scope of this change.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm going to follow up this change with a style pass. I'll look at private/internal members.

Copy link
Member

@safern safern left a comment

Choose a reason for hiding this comment

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

LGTM. Just a missing space after a fixed statement.

I would like to also use this in the Unix code if possible in the future and I think there is a lot that can be improved in the Drawing area. Thanks for starting this.


graphics.DrawBeziers(pen, points);
ValidateImageContent(image,
PlatformDetection.IsWindows
Copy link
Member

Choose a reason for hiding this comment

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

Just curious, why is IsWindows needed if this test is already conditioned on GDI+ availability? What is the other platform?

Copy link
Member Author

Choose a reason for hiding this comment

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

The check for GDI+ availability is also checking for the mono version of GDI+, which we use on Unix. The implementation isn't exactly the same and has some gaps.

@JeremyKuhne JeremyKuhne merged commit ba496ae into dotnet:master Jun 6, 2018
@JeremyKuhne JeremyKuhne deleted the blitpoints branch June 6, 2018 17:26
Count = count,
Points = p,
Types = t
};
Copy link
Member

@filipnavara filipnavara Feb 21, 2019

Choose a reason for hiding this comment

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

Doesn't this have to be fixed (...) as well? (or is it guaranteed to be on stack and hence not moving?)

Copy link
Member

@stephentoub stephentoub Feb 21, 2019

Choose a reason for hiding this comment

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

or is it guaranteed to be on stack and hence not moving

Correct. The language won't even let you use fixed here (assuming you're referring to data); it'll produce an error something about not letting you use fixed with an already fixed expression.

picenka21 pushed a commit to picenka21/runtime that referenced this pull request Feb 18, 2022
* System.Drawing is doing a lot of unnecessary work.

This is a start on trying to remove some of it. This change is meant to show the general pattern I intend to follow.

- Fixes the managed array and uses it directly instead of copying multiple times
- Adds some basic sanity tests to validate that we're getting the expected output
- Splits existing negative tests into new files with sanity tests
- Adds a performance project

Output tests were generated and run before making the functional change.

* Address feedback

* Add space after fixed statements.


Commit migrated from dotnet/corefx@ba496ae
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants