-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Optimize KnownColor value lookup via a combined color table #50489
Conversation
* Fixes perf issue dotnet#46153 related to commit d21fe17 * Brings the following dotnet/performance microbenchmarks back on par with .NET5 * System.Drawing.Tests.Perf_Color.GetBrightness * System.Drawing.Tests.Perf_Color.GetSaturation * System.Drawing.Tests.Perf_Color.GetHue * Use one known color table for both system and non-system colors (indexed by the corresponding KnownColor enum value). * This table stores both the color value and color category. * Add color category enum for Unknown, System, and Web. * Combined table removes the need to handle mismatches corresponding with the KnownColor enum value. * Obtaining the value of and verifying if a known color is a system color is simplified to a single lookup instead of multiple base bounds comparisons inside the fragmented KnownColor enum table. * These base bounds comparisons would grow linearly as the KnownColor enum table had more colors added to it, mixed between system and non-system. Now no comparisons are needed.
Tagging subscribers to this area: @safern, @tarekgh Issue Details
|
dotnet/performance microbenchmarks filtered with System.Drawing.Tests.Perf_Color*: Base = .NET6 Before this changeset No Slower results for the provided threshold = 2% and noise filter = 25ns.
Also comparing this changeset to .NET5: Base = .NET5 No differences found between the benchmark results with threshold 2%. |
What my profiling showed was that Color.IsKnownColorSystem(color) called here was not being inlined anymore due to the extra comparison that was added to the KnownColorToArgb(KnownColor color) method: runtime/src/libraries/Common/src/System/Drawing/KnownColorTable.cs Lines 187 to 199 in 5078dd6
I also saw that there was a lot of time being spent in IsKnownColorSystem(KnownColor knownColor) due to the extra comparisons that were added: runtime/src/libraries/System.Drawing.Primitives/src/System/Drawing/Color.cs Lines 375 to 376 in 5078dd6
To mitigate both causes, I combined the color lookup tables inside KnownColorTable.cs, indexed by their enum value in KnownColor.cs. I also augmented this table to include the color category which leads to no base bounds comparisons necessary to check if a color is categorized as system or non-system (web). Regarding the base bounds comparisons: the existing implementation categorized the color based on its location inside the KnownColor enum table. As this table became more fragmented, more comparisons would be needed to categorize a color. For example: Since modifications to the KnownColor enum table are breaking changes, new colors are added to the end and the existing implementation checks if a given known color is a system color by checking if it's inside the enum value bounds of (SystemColor1 to SystemColor3 | SystemColor4 to SystemColor6| SystemColor7) in this example. As this becomes more fragmented, the comparisons grow linearly. As we already have a unique lookup value for each color (their enum value) we can use a secondary table as it already exists in two parts (one for web and another for system) in KnownColorTable, combine it, and also include the category labels for comparison free checking of a known color's category. This brings performance back on par with .NET5. |
0xFFF5F5F5, // WhiteSmoke | ||
0xFFFFFF00, // Yellow | ||
0xFF9ACD32, // YellowGreen | ||
Unknown, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we instead introduce 3 constants?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @safern , yes I did consider that initially but chose the enum route in favor of constants due to easier readability and maybe simpler when using the library? I believe enum compiles to a constant so I went with that currently. Also by using enums we can expand the categories in the future to hold more values other than unknown, system, and web. But you're right, if we use constants directly there will be less casting involved. I will follow your recommendation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I doubt having more categories. If we end up in the need to using more categories we can go back to an enum.
Yeah, enums are handy cause they compile to a constant, but we are doing a lot of castings for this scenario, so it might just be better to store them as uint constants, or even byte constants given that we don't need 32 bits for the current categories?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, sounds good. I will change the enum categories to byte constants. Just a quick question, I had originally made them uint so I could put them in the same uint array:
public static readonly uint[,] s_colorTable = new uint[knownColorCount, 2]
but it turned out I still needed to cast the enums when placing them inside this array. By using byte constants, I will still need to cast to place them inside right? Or do you recommend something like an array of structs to hold the category byte constant and color value so we don't need to cast when creating the array?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could have an array of byte, uint tiple. But let's leave it as you have it now using uint for both values. I'm not sure what would be better.
@tannergooding insight might help.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, I've updated it with the requested changes, keeping constant uint categories as suggested for now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @safern , anything else I can do to enhance this PR? Thanks for the help.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I missed the ping and Santi just pinged me offline.
My recommendation is to split this into a byte[]
(for the kind) and uint[]
(for the value). The JIT and C# compiler can optimize this scenario and it will save us some space.
The byte[]
for the kind could even be further compressed if space was a concern, since its basically a bitmap (0 or 1, per entry)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @L2, I just spoke with @tannergooding and we think the best approach for this would be to have 2 separate arrays.
One byte[]
for the color kind and one uint[]
for the color itself. Having a multidimensional array might affect startup perf and memory perf vs two separate arrays, plus since these are primitives, the JIT will optimize these 2 arrays.
EDIT: @tannergooding beat me to reply and didn't get the comment until I posted it 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you, @tannergooding and @safern . I've updated it with the recommended changes. Looks to have a nice bump in speed with the casting removed and split array.
dotnet/performance microbenchmarks filtered with System.Drawing.Tests.Perf_Color*:
summary:
better: 3, geomean: 1.480
total diff: 3
No Slower results for the provided threshold = 2% and noise filter = 25ns.
Faster | base/diff | Base Median (ns) | Diff Median (ns) | Modality |
---|---|---|---|---|
System.Drawing.Tests.Perf_Color.GetBrightness | 1.55 | 1542.74 | 996.36 | |
System.Drawing.Tests.Perf_Color.GetSaturation | 1.50 | 1597.70 | 1064.63 | |
System.Drawing.Tests.Perf_Color.GetHue | 1.40 | 1757.32 | 1258.50 |
* As recommended by: tannergooding and safern
internal static Color ArgbToKnownColor(uint argb) | ||
{ | ||
// Should be fully opaque (and as such we can skip the first entry | ||
// which is transparent). | ||
Debug.Assert((argb & Color.ARGBAlphaMask) == Color.ARGBAlphaMask); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe add a Debug.Assert here to assert that the two tables are the same length?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, updated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you very much!
Improvemnts in System.Drawing.Tests.Perf_Color
We are seeing these results in the lab, thanks for tracking down and fixing this issue. |
with .NET5
colors (indexed by the corresponding KnownColor enum value).
with the KnownColor enum value.
color is simplified to a single lookup instead of multiple base
bounds comparisons inside the fragmented KnownColor enum table.
enum table had more colors added to it, mixed between system and
non-system. Now no comparisons are needed.