2D System.Memory-like primitives (Span2D<T>, Memory2D<T>) #3353
Conversation
|
Thanks Sergio0694 for opening a Pull Request! The reviewers will test the PR and highlight if there is any conflict or changes required. If the PR is approved we will proceed to merge the pull request |
Microsoft.Toolkit.HighPerformance/Enumerables/ReadOnlyRefEnumerable{T}.cs
Show resolved
Hide resolved
Microsoft.Toolkit.HighPerformance/Extensions/ArrayExtensions.1D.cs
Outdated
Show resolved
Hide resolved
...ft.Toolkit.HighPerformance/Helpers/ParallelHelper.ForEach.IInAction2D.cs
Outdated
Show resolved
Hide resolved
...ft.Toolkit.HighPerformance/Helpers/ParallelHelper.ForEach.IInAction2D.cs
Outdated
Show resolved
Hide resolved
...ft.Toolkit.HighPerformance/Helpers/ParallelHelper.ForEach.IInAction2D.cs
Outdated
Show resolved
Hide resolved
...ft.Toolkit.HighPerformance/Helpers/ParallelHelper.ForEach.IInAction2D.cs
Show resolved
Hide resolved
...t.Toolkit.HighPerformance/Helpers/ParallelHelper.ForEach.IRefAction2D.cs
Outdated
Show resolved
Hide resolved
|
This PR has been marked as "needs attention |
This reverts commit 78a0266. Done to avoid conflicts, since these two enumerable types are already being overhauled in windows-toolkit#3353 anyway.
|
Going to wait until #3351 gets merged. |
Microsoft.Toolkit.HighPerformance/Enumerables/ReadOnlySpanEnumerable{T}.cs
Outdated
Show resolved
Hide resolved
|
I looked at most of this code, and while I'm not 100% sure, I think it all looks good to me.
|
|
Hey @HurricanKai, thanks for the review! As for your questions:
|
|
@Sergio0694 this has some conflicts to resolve now? |
|
@michael-hawker Yup, was just waiting on #3378 to be merged first since that'll edit some of the same files anyway, so that here I'll just have to resolve conflicts once when pulling changes from |
|
@Sergio0694 #3378 is merged now, can you resolve conflicts and clean-up this PR so we can re-review and get it merged as well? Thanks! |
|
Few small comments on the unit tests, looking good! |
| @@ -23,12 +23,7 @@ public static class HashCodeExtensions | |||
| /// <param name="span">The input <see cref="ReadOnlySpan{T}"/> instance.</param> | |||
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |||
| public static void Add<T>(ref this HashCode hashCode, ReadOnlySpan<T> span) | |||
| #if SPAN_RUNTIME_SUPPORT | |||
michael-hawker
Nov 3, 2020
Member
This has changed now I guess from the runtime side?
This has changed now I guess from the runtime side?
Sergio0694
Nov 3, 2020
Author
Member
I've removed that restriction because I've now added polyfills for the RuntimeHelpers.IsReferenceOrContainsReferences<T>() API on .NET Standard 2.0 and below (I needed this for other code in the Memory2D<T> and Span2D<T> types, so I used it here too while I was at it), I use that here:
Now that older targets can test this too at runtime, we no longer have to add that extra type constraint, which was basically just a precaution due to the lack of this API at runtime. You can see how we switch the polyfill on here:
So now on all targets the API will just automatically pick the right implementation for each input type T 😄
I've removed that restriction because I've now added polyfills for the RuntimeHelpers.IsReferenceOrContainsReferences<T>() API on .NET Standard 2.0 and below (I needed this for other code in the Memory2D<T> and Span2D<T> types, so I used it here too while I was at it), I use that here:
Now that older targets can test this too at runtime, we no longer have to add that extra type constraint, which was basically just a precaution due to the lack of this API at runtime. You can see how we switch the polyfill on here:
So now on all targets the API will just automatically pick the right implementation for each input type T
|
|
||
| Assert.ThrowsException<ArgumentOutOfRangeException>(() => array.GetColumn(-1)); | ||
|
|
||
| Assert.ThrowsException<ArgumentOutOfRangeException>(() => array.GetColumn(20)); |
michael-hawker
Nov 3, 2020
Member
We should also be testing the 'off-by-1' index of '3', eh? (here and where-ever)
We should also be testing the 'off-by-1' index of '3', eh? (here and where-ever)
Sergio0694
Nov 3, 2020
Author
Member
Right! I had added those tests when getting rows/columns from spans, but not from 2D arrays directly!
Fixed in 7a65a29, for both GetRow and GetColumn in this file 😄
Right! I had added those tests when getting rows/columns from spans, but not from 2D arrays directly!
Fixed in 7a65a29, for both GetRow and GetColumn in this file
|
|
||
| // Try to copy to a valid row and an invalid column (too short for the source span) | ||
| bool shouldBeTrue = values.TryCopyTo(array1.GetRow(2)); | ||
| bool shouldBeFalse = values.TryCopyTo(array1.GetColumn(1)); |
michael-hawker
Nov 3, 2020
Member
We're not checking here that no mutation occurred, eh? As the line 281 above would have copied the same values? (I mean we know it failed as '20' is still in the 3rd row, but may be better to use a different column like '3' here to be safe to guard against this case too.)
We're not checking here that no mutation occurred, eh? As the line 281 above would have copied the same values? (I mean we know it failed as '20' is still in the 3rd row, but may be better to use a different column like '3' here to be safe to guard against this case too.)
Sergio0694
Nov 3, 2020
Author
Member
Good catch! Also added one extra check for the entire target array for good measure 😄
Done in 6bb9e5a, including the same changes for the comment below.
Good catch! Also added one extra check for the entire target array for good measure
Done in 6bb9e5a, including the same changes for the comment below.
...Tests/UnitTests.HighPerformance.Shared/Extensions/Test_SpanExtensions.cs
Outdated
Show resolved
Hide resolved
| @@ -135,8 +135,7 @@ public static partial class ParallelHelper | |||
| /// Processes the batch of actions at a specified index | |||
| /// </summary> | |||
| /// <param name="i">The index of the batch to process</param> | |||
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |||
michael-hawker
Nov 3, 2020
Member
Expected for the inlining to disappear?
Expected for the inlining to disappear?
Sergio0694
Nov 3, 2020
Author
Member
Yeah this was just a leftover, it's not there in the other implementations either. This method is just called once per thread anyway and does all the job. It's the actual delegates within each invocation of this method that need to be inlined, as they're invoked in a tight loop, not this one. As in, this loop here:
Yeah this was just a leftover, it's not there in the other implementations either. This method is just called once per thread anyway and does all the job. It's the actual delegates within each invocation of this method that need to be inlined, as they're invoked in a tight loop, not this one. As in, this loop here:
| // of CPU cores (which is an int), and the height of each batch is necessarily | ||
| // smaller than or equal than int.MaxValue, as it can't be greater than the | ||
| // number of total batches, which again is capped at the number of CPU cores. | ||
| nint |
michael-hawker
Nov 3, 2020
Member
So if this is native to the platform, how do we unit test the code for both possible sizes?
So if this is native to the platform, how do we unit test the code for both possible sizes?
| @@ -2,7 +2,7 @@ | |||
|
|
|||
| <PropertyGroup> | |||
| <TargetFrameworks>netstandard1.4;netstandard2.0;netstandard2.1;netcoreapp2.1;netcoreapp3.1</TargetFrameworks> | |||
| <LangVersion>8.0</LangVersion> | |||
| <LangVersion>9.0</LangVersion> | |||
michael-hawker
Nov 3, 2020
Member
Does this need anything new for our build pipeline/tooling requirements for folks?
Does this need anything new for our build pipeline/tooling requirements for folks?
Sergio0694
Nov 3, 2020
Author
Member
Commenting here about the same comments from the stream, for future reference. Right now this works on the latest versions of VS stable (not the preview preview), because it uses the same Roslyn codebase as VS Preview, just from an older branch. As such, some C# 9 features are buggy/outdated (eg. function pointers), but since here we're really only using the new nint type, that bit in particular mostly works. I only had to add an extra explicit cast here to ensure the correct codegen due to a small compiler bug that's fixed in VS Preview, for instance:
As in, this PR should work fine when compiled with VS stable, but especially due to all the other C# 9 changes coming in other PRs, as well as the .NET 5 support itself, we should definitely update the CI build of VS to the new release as soon as it drops hopefully next week when .NET 5 is announced at .NET Conf 😄
Commenting here about the same comments from the stream, for future reference. Right now this works on the latest versions of VS stable (not the preview preview), because it uses the same Roslyn codebase as VS Preview, just from an older branch. As such, some C# 9 features are buggy/outdated (eg. function pointers), but since here we're really only using the new nint type, that bit in particular mostly works. I only had to add an extra explicit cast here to ensure the correct codegen due to a small compiler bug that's fixed in VS Preview, for instance:
As in, this PR should work fine when compiled with VS stable, but especially due to all the other C# 9 changes coming in other PRs, as well as the .NET 5 support itself, we should definitely update the CI build of VS to the new release as soon as it drops hopefully next week when .NET 5 is announced at .NET Conf
|
|
||
| // Copy a span to a target row and column with valid lengths | ||
| values.CopyTo(array1.GetRow(0)); | ||
| values.Slice(0, 4).CopyTo(array1.GetColumn(1)); |
michael-hawker
Nov 3, 2020
Member
Should we check the intermediary results? I've noticed these tests are chaining quite a few operations together, so it may be better to help find problems in a particular part if we're validating the first part before moving on? (Or split them as separate tests?)
Should we check the intermediary results? I've noticed these tests are chaining quite a few operations together, so it may be better to help find problems in a particular part if we're validating the first part before moving on? (Or split them as separate tests?)
|
|
||
| // A couple of tests for invalid parameters, ie. layers out of range | ||
| Assert.ThrowsException<ArgumentOutOfRangeException>(() => new Memory2D<int>(array, -1)); | ||
| Assert.ThrowsException<ArgumentOutOfRangeException>(() => new Memory2D<int>(array, 20)); |
michael-hawker
Nov 3, 2020
Member
same here for off-by-one
same here for off-by-one
|
Hello @michael-hawker! Because this pull request has the 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 (
|
|
Other than cleaning up some no longer needed |
| [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1312", Justification = "Dummy loop variable")] | ||
| [SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1501", Justification = "Empty test loop")] |
RosarioPulella
Nov 5, 2020
Contributor
There a few SuppressMessageAttributes left on methods that are no longer needed. It would be good to remove them from methods that no longer need it.
There a few SuppressMessageAttributes left on methods that are no longer needed. It would be good to remove them from methods that no longer need it.
Sergio0694
Nov 5, 2020
Author
Member
Oh, good catch, forgot about those! 😄
Fixed in 7e67a9c, and also fixed some other leftover warnings in the tests.
Oh, good catch, forgot about those!
Fixed in 7e67a9c, and also fixed some other leftover warnings in the tests.
|
Removing the |
The🦙 (@michael-hawker) asked:
And this is the answer🚀
PR Type
What kind of change does this PR introduce?
What is the current behavior?
There is no proper way to pass around a 2D memory area in a format that's easy to handle and somewhat similar to how the other
System.MemoryAPIs work (ie.Span<T>,Memory<T>). This is regardless of whether or not the wrapped memory region is contiguous in memory or not.What is the new behavior?
This PR adds a number of new types and extensions to solve this issue, specifically:
Span2D<T>ReadOnlySpan2D<T>Memory2D<T>ReadOnlyMemory2D<T>And a series of new extensions for 3D arrays as well.
Note that all these 2D types can be used with arrays of any type, eg. they can both be used to wrap a 2D memory region from a 2D array, a 2D slice for a 3D array, or even just to represent a 1D array as a 2D memory region to make it easier to properly access elements by their spatial coordinates.
API surface (summary)
These two types are also mirrored as
ReadOnlyMemory2D<T>andReadOnlySpan2D<T>.PR Checklist
Please check if your PR fulfills the following requirements:
Sample in sample app has been added / updated (for bug fixes / features)Icon has been created (if new sample) following the Thumbnail Style Guide and templatesAdditional notes
The breaking change is the removal of the
Array2DRowEnumerable<T>andArray2DColumnEnumerable<T>types, which were used in theGetRowandGetColumnAPIs. This approach had two problems:GetRowAPI was a breaking change when upgrading, as the return type was justSpan<T>there.The solution, as discussed with @john-h-k and @tannergooding, was to introduce a new, shared
RefEnumerable<T>type that can be used to traverse both rows and columns, and to use that one for both APIs. Next to them, a new APIGetRowSpanwas also added only on supported runtimes (so not a breaking change), which is equivalent toGetRowbut returning aSpan<T>value.