Skip to content
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

Use Span<T>.Fill implementation for Array.Fill #52590

Merged
merged 16 commits into from
Jun 30, 2021

Conversation

xtqqczze
Copy link
Contributor

@xtqqczze xtqqczze commented May 11, 2021

Fix #7049

Follow-up #51365

cc: @GrabYourPitchforks

@ghost
Copy link

ghost commented May 11, 2021

Tagging subscribers to this area: @tannergooding
See info in area-owners.md if you want to be subscribed.

Issue Details

Fix #7049

Follow-up #51365

Author: xtqqczze
Assignees: -
Labels:

area-System.Runtime

Milestone: -

Copy link
Member

@GrabYourPitchforks GrabYourPitchforks left a comment

Choose a reason for hiding this comment

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

Creating a Span<T> wrapped around a T[] in this manner is illegal. (It doesn't account for array variance and could lead to type-safety violations.) You need to have logic similar to what's already in the Span<T> ctor:

if (!typeof(T).IsValueType && array.GetType() != typeof(T[]))
ThrowHelper.ThrowArrayTypeMismatchException();

Basically, if the span ctor would've succeeded, then turn it into a span and call fill. Otherwise loop and set each array element individually.

@xtqqczze
Copy link
Contributor Author

Creating a Span<T> wrapped around a T[] in this manner is illegal.

I will make the suggested changes. However. I see the same pattern used elsewhere, without the type check, e.g.:

var span = new Span<T>(ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(array), index), length);

@xtqqczze
Copy link
Contributor Author

@gfoidl Could we combine conditions and just throw ArgumentOutOfRangeException, like:

#if TARGET_64BIT
// Since start and length are both 32-bit, their sum can be computed across a 64-bit domain
// without loss of fidelity. The cast to uint before the cast to ulong ensures that the
// extension from 32- to 64-bit is zero-extending rather than sign-extending. The end result
// of this is that if either input is negative or if the input sum overflows past Int32.MaxValue,
// that information is captured correctly in the comparison against the backing _length field.
// We don't use this same mechanism in a 32-bit process due to the overhead of 64-bit arithmetic.
if ((ulong)(uint)start + (ulong)(uint)length > (ulong)(uint)_length)
ThrowHelper.ThrowArgumentOutOfRangeException();
#else
if ((uint)start > (uint)_length || (uint)length > (uint)(_length - start))
ThrowHelper.ThrowArgumentOutOfRangeException();
#endif

@gfoidl
Copy link
Member

gfoidl commented May 12, 2021

Could we combine conditions and just throw ArgumentOutOfRangeException, like:

I like the idea, but I fear this a breaking change as a different exception will be thrown. In the span-code it's just the ArgumentOutOfRangeException, whilst the array-code has a distinction between index and count.
I'd go with your idea, and to be "symmetric" with the span-code, but I'm not in the position to tell if this is tolerable or not. Someone from the team should decide here.

@GrabYourPitchforks
Copy link
Member

Correct - it's generally not considered an acceptable breaking change to change or combine exceptions like that. The reason we could get away with it in Span<T> is because those were completely new code paths with no expectation of previous compat.

@GrabYourPitchforks
Copy link
Member

We should add extra unit tests for this:

  • If I have a Bar[] that has been downcast to an object[], I can call Array.Fill<object>(..., new Bar()), and the call will succeed.
  • If I have a Bar[] that has been downcast to an object[], I can call Array.Fill<object>(..., new Foo()), the call will fail with ArrayTypeMismatchException (or whatever the exception is), and the array will retain its original contents.
  • If I have an int[] that has been sidecast to a uint[], I can call Array.Fill<uint>(..., 42), and the call will succeed.

{
array[i] = value;
var span = new Span<T>(ref MemoryMarshal.GetArrayDataReference(array), array.Length);
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't need this; should be able to do new Span<T>(array).Fill(value);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right, we've already checked array for null so the null check in Span(T[]? array) should be eliminated by the jit. I don't think we can do similar in same Fill<T>(T[] array, T value, int startIndex, int count) because the argument validation is more complex.

src/libraries/System.Private.CoreLib/src/System/Array.cs Outdated Show resolved Hide resolved
public static void Fill_Casting()
{
Bar[] barArray = new Bar[2];
Array.Fill<object>(barArray, new Bar());
Copy link
Member

Choose a reason for hiding this comment

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

Please validate that Array.Fill actually populated the array. Something like:

public class Bar
{
    public string Value;
}

Bar[] barArray = new Bar[]
{
    new Bar() { Value = "1" },
    new Bar() { Value = "2" },
};

Array.Fill<object>(barArray, new Bar() { Value = "x" });

Assert.Equal("x", barArray[0].Value);
Assert.Equal("x", barArray[1].Value);

This method should also test the other Fill overload. For instance: new Bar[5], with Fill(theArray, 1, 2), ensuring that only theArray[1] and theArray[2] were changed. There's no need to unit test that the bounds check is correct; I think there's already a unit test for that.

And we should have a test between uint[] <-> int[] as well, to ensure that we didn't inadvertently break that logic. For instance:

int[] ints = new int[] { 10, 20, 30, 40, 50 };
uint[] uints = (uint[])(object)ints;
// do work via Array.Fill(uints, ...);
Assert.Equal(new int[] { /* whatever the expected sequence is */ }, ints);

[Fact]
public static void Fill_Casting_Invalid()
{
Bar[] barArray = new Bar[2];
Copy link
Member

Choose a reason for hiding this comment

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

Similar to above, but after the call to Assert.Throws this unit test should ensure that the contents of the array remain unchanged.

}

[Fact]
public static void Fill_Casting()
Copy link
Member

Choose a reason for hiding this comment

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

Add similar tests coverage for the other Array.Fill overload. If I am reading the code correctly, it has a bug that you should be able to find by adding the tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

bug fixedup in 07dd088

@jeffhandley
Copy link
Member

@xtqqczze Do you think you'll be able to address the remaining feedback?

@xtqqczze
Copy link
Contributor Author

@xtqqczze Do you think you'll be able to address the remaining feedback?

I'll be able to continue working on this in the w/c 2021-06-20.

@jeffhandley
Copy link
Member

Cool; thanks! If it's merged in by July 12, it'll make .NET 6.0.

@xtqqczze xtqqczze marked this pull request as draft June 18, 2021 16:56
Assert.Throws<ArrayTypeMismatchException>(() => Array.Fill<object>(barArray1, new Foo()));

Bar[] barArray2 = CreateBarArray();
try
Copy link
Member

Choose a reason for hiding this comment

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

Don't use try / catch in these tests, as it won't properly handle the failure condition. Instead, use:

Assert.Throws<ArrayTypeMismatchException>(() => ...);
Assert.Equal(...);

These two lines will check that the callback throws, and it will also check that the array has not been mutated, as you are doing here.

See near the top of this file for some other examples of using Assert.Throws.

{
Bar[] barArray1 = CreateBarArray();
Array.Fill<object>(barArray1, new Bar() { Value = "x" });
Assert.Equal(Enumerable.Repeat("x", barArray1.Length), barArray1.Select(e => e.Value));
Copy link
Member

Choose a reason for hiding this comment

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

Consider using new string[] { 'x', 'x', 'x', 'x' } here to match the pattern you have in line 4285 below. Removing the call to Enumerable.Repeat uncomplicates the test.

(Same comment for line 4289.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was originally considering parameterised the tests, this would require the second Fill_Casting test to be written something like:

T[] before = source.Take(startIndex).ToArray();
T[] after = source.Skip(startIndex + count).ToArray();
Array.Fill(array, value, startIndex, count);
Assert.Equal(before, array.Take(startIndex));
Assert.Equal(Enumerable.Repeat(value, count), array.Skip(startIndex).Take(count));
Assert.Equal(after, array.Skip(startIndex + count));

But this seemed unnecessarily complicated 😋

@xtqqczze xtqqczze marked this pull request as ready for review June 22, 2021 12:50
@xtqqczze
Copy link
Contributor Author

runtime failed: TF400898: An Internal Error Occurred. Activity Id: d86bb713-ad27-498f-8159-716f1fb2f1f9.

@GrabYourPitchforks
Copy link
Member

FYI - there's no need to force push. In fact, doing so makes the PR more difficult for us to review.
Everything will be properly squashed anyway into a single commit when it's eventually merged.

@GrabYourPitchforks
Copy link
Member

Also, the PR as of commit c48c69c seemed fine. Why change it to df45239e19ff03b43175bc24ebc0fbfc543682ce? Seems like that change introduced compilation errors and reverted some of the test cleanup you did.

@xtqqczze
Copy link
Contributor Author

Also, the PR as of commit c48c69c seemed fine. Why change it to df45239? Seems like that change introduced compilation errors and reverted some of the test cleanup you did.

To clarify here, df45239e19ff03b43175bc24ebc0fbfc543682ce was the commit you approved (which contained compilation errors), c48c69c is the current revision (which was force pushed).

Sorry for the confusion, in future, I shall avoid force push. If it helps I can rebase back onto df45239e19ff03b43175bc24ebc0fbfc543682ce to present a linear history.

@GrabYourPitchforks
Copy link
Member

Derp, swapping the two was my mistake. But it's a good case study of how force pushing makes reviewing more difficult: because we need to fetch the two commits individually and perform a manual diff against them offline, which presents opportunity for reviewers to err (as I had done here). In the future, if you stick to simple pushes, it'll mean that we can review using just the web UI here, which makes looking at diffs much simpler. Thanks! :)

@GrabYourPitchforks
Copy link
Member

CI failures are caused by https://status.dev.azure.com/_event/246897938. We'll sit tight and wait for this to resolve itself.

@xtqqczze xtqqczze closed this Jun 22, 2021
@xtqqczze xtqqczze reopened this Jun 22, 2021
@xtqqczze xtqqczze closed this Jun 24, 2021
@xtqqczze xtqqczze reopened this Jun 24, 2021
@GrabYourPitchforks
Copy link
Member

@xtqqczze Looks like 2 categories of errors: The first is that c48c69c should be reverted, and those lines should instead read Assert.Equal<object>(new int[] { /* ... */ }, uintArray). This is just a weird consequence of how xunit performs equality checking in the version we pull in. The code you wrote would work in the newest version of xunit, but we don't yet pull it in to the repo.

The second is that I think you need to implement Bar.Equals on your test class. Otherwise xunit doesn't know how to compare those Bar objects for equality.

I think we're getting close here! :)

@xtqqczze
Copy link
Contributor Author

c48c69c should be reverted, and those lines should instead read Assert.Equal<object>(new int[] { /* ... */ }, uintArray).

I think we need Assert.Equal(new int[] { /* ... */ }, (int[])(object)uintArray) instead.

Otherwise, I see error CS1503 because the Equal(String, String) overload is selected.

@GrabYourPitchforks
Copy link
Member

PeriodTimer failures are a known issue (also tracked via #54543).

@xtqqczze
Copy link
Contributor Author

Test failures:

  • System.Net.Http.Functional.Tests.SocketsHttpHandlerTest_Cookies_Http3_Mock.GetAsyncWithBasicAuth_ReceiveSetCookie_CookieSent
  • System.Net.Http.Functional.Tests.SyncHttpHandler_HttpProtocolTests_Dribble.GetAsync_ResponseHasNormalLineEndings_Success

These appear to be tracked by #54778.

@GrabYourPitchforks
Copy link
Member

@GrabYourPitchforks
Copy link
Member

Remaining test failures are #28949, #54042

@GrabYourPitchforks GrabYourPitchforks merged commit 99f7bae into dotnet:main Jun 30, 2021
@GrabYourPitchforks
Copy link
Member

Thanks @xtqqczze!

@xtqqczze
Copy link
Contributor Author

xtqqczze commented Jul 1, 2021

@GrabYourPitchforks We got there in the end! Thanks for all your help with this.

@xtqqczze xtqqczze deleted the array-fill branch July 1, 2021 16:08
@ghost ghost locked as resolved and limited conversation to collaborators Jul 31, 2021
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.

Optimize implementation of Array.Fill.
5 participants