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

Optimise Guard.IsBitwiseEqual #3325

Merged
merged 22 commits into from Jun 5, 2020

Conversation

john-h-k
Copy link
Contributor

@john-h-k john-h-k commented Jun 4, 2020

Optimisation (identical behavior)

What is the current behavior?

Slow path is taken for any type which isn't 1, 2, 34, or 8 byte size

What is the new behavior?

Fast path is also taken for 16 byte types, and other sized types use Span<T>.SequenceEqual, which is well optimised, rather than the naive for-loop approach

Extensively codegen checked. All is inlined as expected. The only possible improvement is making the parameters passed by readonly reference (in), but it also has downsides in common scenarios

PR Checklist

  • Contains NO breaking changes

@ghost
Copy link

ghost commented Jun 4, 2020

Thanks john-h-k 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 🙌

@ghost ghost assigned Kyaa-dost Jun 4, 2020
@john-h-k
Copy link
Contributor Author

john-h-k commented Jun 4, 2020

cc @Sergio0694

@azchohfi
Copy link
Contributor

azchohfi commented Jun 4, 2020

@john-h-k would you mind just resolving the conflicts before we review this?

@john-h-k
Copy link
Contributor Author

john-h-k commented Jun 4, 2020

Done! @azchohfi

Copy link
Member

@Sergio0694 Sergio0694 left a comment

Choose a reason for hiding this comment

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

Looks good! Just a couple really minor questions 😄

Co-authored-by: Sergio Pedri <sergio0694@live.com>
@ghost
Copy link

ghost commented Jun 4, 2020

This PR has been marked as "needs attention 👋" and awaiting a response from the team.

john-h-k and others added 3 commits June 5, 2020 00:55
Co-authored-by: Sergio Pedri <sergio0694@live.com>
Apologies for multiple commits I'm.on a phone
@michael-hawker
Copy link
Member

@azchohfi thoughts here? I think my main question is around the unsafe addition to the signature. I'd also like to see another unit test added/updated for the new 16 length case.

Otherwise, if you and @Sergio0694 approve, I'm good, as long as it'd be in not too late Friday.

@ghost ghost removed the needs attention 👋 label Jun 5, 2020
@Sergio0694
Copy link
Member

Sergio0694 commented Jun 5, 2020

@john-h-k A couple notes related to this PR, then it looks great!

  1. This is the test method with positive result. Please add:
  • A comparison between two Guid values
  • A comparison between two values of a custom unmanaged struct that you can define right above that method, which needs to be of size > 16. Just throw in together a bunch of Guid, int, float, etc. fields to have a test with a general user-defined complex struct.

https://github.com/windows-toolkit/WindowsCommunityToolkit/blob/9b15f6ab571ead581a969dba471e73807f4fa175/UnitTests/UnitTests.Shared/Diagnostics/Test_Guard.cs#L156-L162

  1. Rename this to Size8Fail for consistency:

https://github.com/windows-toolkit/WindowsCommunityToolkit/blob/9b15f6ab571ead581a969dba471e73807f4fa175/UnitTests/UnitTests.Shared/Diagnostics/Test_Guard.cs#L167

  1. Move this test to the previous Size8Fail method, since DateTime actually has a size of 8:

https://github.com/windows-toolkit/WindowsCommunityToolkit/blob/9b15f6ab571ead581a969dba471e73807f4fa175/UnitTests/UnitTests.Shared/Diagnostics/Test_Guard.cs#L177

  1. Add a new Size16Fail, and add a failing test with any struct you want that has a size of 16.

  2. Rename this to SizeGreaterThan16Fail, and then add a failing case with the custom struct you defined for point 1):

https://github.com/windows-toolkit/WindowsCommunityToolkit/blob/9b15f6ab571ead581a969dba471e73807f4fa175/UnitTests/UnitTests.Shared/Diagnostics/Test_Guard.cs#L175

@michael-hawker The unsafe keyword there isn't actually part of the method signature, it's merely an implementation detail. Just like the async keyword, that won't show up in the method signature at all, it just describes how the method works and lets you use specific features (eg. await). In this case, the unsafe keyword there is just to indicate that the whole body of the method is an unsafe context, where raw pointers are allowed. But there isn't any functional difference than before, since we were already doing the same by using the Unsafe class, which doesn't require the unsafe context but lets you do equally dangerous hacks anyway 😄

@john-h-k
Copy link
Contributor Author

john-h-k commented Jun 5, 2020

I think my main question is around the unsafe addition to the signature. I'd also like to see another unit test added/updated for the new 16 length case.

If anything, I strongly dislike the current avoidal of unsafe. The code in the methods are very, very unsafe, and have zero language validation (unsafe code has minimal validation - can't take address of managed types or unpinned variables, where Unsafe has literally none), and just tries to hide the fact it is unsafe when it really, really should be unsafe

System.Runtime.CompilerServices is wayy more dangerous and, imo, and I think the official consensus is, should only be used when unsafe physically can't. This entire method could be unsafe and it would arguably be better. E.g, if it was &value not Unsafe.AsPointer(ref value), then the compiler would correctly error and say "you can't do this" if someone changed the parameter to ref or something, whereas it would just silently break with Unsafe

@Sergio0694
Copy link
Member

@john-h-k On that last point, I have no objections if we want to use &value and &target here:

https://github.com/windows-toolkit/WindowsCommunityToolkit/blob/e47334a050394d32c14a7135f60f101126d74d82/Microsoft.Toolkit/Diagnostics/Guard.Comparable.Generic.cs#L181-L182

If nothing else, that'd also make the code more consistent, since you did use &value in the two previous cases. So feel free to make that change, sure 👍

Copy link
Member

@Sergio0694 Sergio0694 left a comment

Choose a reason for hiding this comment

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

Looking great! 🚀

Just one last small detail for the sake of future maintenance/readability.

UnitTests/UnitTests.Shared/Diagnostics/Test_Guard.cs Outdated Show resolved Hide resolved
@ghost
Copy link

ghost commented Jun 5, 2020

This PR has been marked as "needs attention 👋" and awaiting a response from the team.

@Sergio0694
Copy link
Member

@john-h-k Code looks fine now but the CI is failing.
Can you build fine locally in both Debug and Release mode?

@ghost ghost removed the needs attention 👋 label Jun 5, 2020
@michael-hawker
Copy link
Member

This is the error from the build log, but I'm not sure what that means:

"D:\a\1\s\Windows Community Toolkit.sln" (Build target) (1) ->
       "D:\a\1\s\UnitTests\UnitTests.UWP\UnitTests.UWP.csproj" (default target) (2) ->
       (BuildNativePackage target) -> 
         C:\Users\VssAdministrator\.nuget\packages\microsoft.net.native.compiler\2.2.7-rel-27913-00\tools\Microsoft.NetNative.targets(801,5): error : ILT0005: 'C:\Users\VssAdministrator\.nuget\packages\runtime.win10-x86.microsoft.net.native.compiler\2.2.7-rel-27913-00\tools\x86\ilc\Tools\nutc_driver.exe @"D:\a\1\s\UnitTests\UnitTests.UWP\obj\x86\Release\ilc\intermediate\MDIL\UnitTests.UWP.rsp"' returned exit code -1073741819 [D:\a\1\s\UnitTests\UnitTests.UWP\UnitTests.UWP.csproj]

Co-authored-by: Sergio Pedri <sergio0694@live.com>
@ghost
Copy link

ghost commented Jun 5, 2020

This PR has been marked as "needs attention 👋" and awaiting a response from the team.

@ghost ghost added the needs attention 👋 label Jun 5, 2020
@Sergio0694
Copy link
Member

@john-h-k Found a workaround to make .NET Native happy.
For some reason those pointers were causing the Release UWP build to fail.
Please replace these two methods with this code:

/// <summary>
/// Asserts that the input value must be a bitwise match with a specified value.
/// </summary>
/// <typeparam name="T">The type of input values to compare.</typeparam>
/// <param name="value">The input <typeparamref name="T"/> value to test.</param>
/// <param name="target">The target <typeparamref name="T"/> value to test for.</param>
/// <param name="name">The name of the input parameter being tested.</param>
/// <exception cref="ArgumentException">Thrown if <paramref name="value"/> is not a bitwise match for <paramref name="target"/>.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static unsafe void IsBitwiseEqualTo<T>(T value, T target, string name)
    where T : unmanaged
{
    // Include some fast paths if the input type is of size 1, 2, 4, 8, or 16.
    // In those cases, just reinterpret the bytes as values of an integer type,
    // and compare them directly, which is much faster than having a loop over each byte.
    // The conditional branches below are known at compile time by the JIT compiler,
    // so that only the right one will actually be translated into native code.
    if (sizeof(T) == 1)
    {
        byte valueByte = Unsafe.As<T, byte>(ref value);
        byte targetByte = Unsafe.As<T, byte>(ref target);

        if (valueByte == targetByte)
        {
            return;
        }

        ThrowHelper.ThrowArgumentExceptionForsBitwiseEqualTo(value, target, name);
    }
    else if (sizeof(T) == 2)
    {
        ushort valueUShort = Unsafe.As<T, ushort>(ref value);
        ushort targetUShort = Unsafe.As<T, ushort>(ref target);

        if (valueUShort == targetUShort)
        {
            return;
        }

        ThrowHelper.ThrowArgumentExceptionForsBitwiseEqualTo(value, target, name);
    }
    else if (sizeof(T) == 4)
    {
        uint valueUInt = Unsafe.As<T, uint>(ref value);
        uint targetUInt = Unsafe.As<T, uint>(ref target);

        if (valueUInt == targetUInt)
        {
            return;
        }

        ThrowHelper.ThrowArgumentExceptionForsBitwiseEqualTo(value, target, name);
    }
    else if (sizeof(T) == 8)
    {
        ulong valueULong = Unsafe.As<T, ulong>(ref value);
        ulong targetULong = Unsafe.As<T, ulong>(ref target);

        if (Bit64Compare(ref valueULong, ref targetULong))
        {
            return;
        }

        ThrowHelper.ThrowArgumentExceptionForsBitwiseEqualTo(value, target, name);
    }
    else if (sizeof(T) == 16)
    {
        ulong valueULong0 = Unsafe.As<T, ulong>(ref value);
        ulong targetULong0 = Unsafe.As<T, ulong>(ref target);

        if (Bit64Compare(ref valueULong0, ref targetULong0))
        {
            ulong valueULong1 = Unsafe.Add(ref Unsafe.As<T, ulong>(ref value), 1);
            ulong targetULong1 = Unsafe.Add(ref Unsafe.As<T, ulong>(ref target), 1);

            if (Bit64Compare(ref valueULong1, ref targetULong1))
            {
                return;
            }
        }

        ThrowHelper.ThrowArgumentExceptionForsBitwiseEqualTo(value, target, name);
    }
    else
    {
        Span<byte> valueBytes = new Span<byte>(Unsafe.AsPointer(ref value), sizeof(T));
        Span<byte> targetBytes = new Span<byte>(Unsafe.AsPointer(ref target), sizeof(T));

        if (valueBytes.SequenceEqual(targetBytes))
        {
            return;
        }

        ThrowHelper.ThrowArgumentExceptionForsBitwiseEqualTo(value, target, name);
    }
}

// Compares 64 bits of data from two given memory locations for bitwise equality
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static unsafe bool Bit64Compare(ref ulong left, ref ulong right)
{
    // Handles 32 bit case, because using ulong is inefficient
    if (sizeof(IntPtr) == 4)
    {
        ref int r0 = ref Unsafe.As<ulong, int>(ref left);
        ref int r1 = ref Unsafe.As<ulong, int>(ref right);

        return r0 == r1 &&
               Unsafe.Add(ref r0, 1) == Unsafe.Add(ref r1, 1);
    }

    return left == right;
}

@ghost ghost removed the needs attention 👋 label Jun 5, 2020
@Sergio0694
Copy link
Member

Sergio0694 commented Jun 5, 2020

It worked! 🎉🎉🎉

FYI @MichalStrehovsky, @tommcdon looks like .NET Native (x64) fails to build when using unsafe code and abundant pointers in this specific case. Commit 5754bcd fixed the issue, so you should be able to see from there the before/after to possibly find out what was causing this.

The specific error was:

Error ILT0005: 'C:\Users\Sergio.nuget\packages\runtime.win10-x64.microsoft.net.native.compiler\2.2.7-rel-27913-00\tools\x64\ilc\Tools\nutc_driver.exe @"C:\Users\Sergio\Documents\GitHub\MS_WindowsCommunityToolkit\UnitTests\UnitTests.UWP\obj\x64\Release\ilc\intermediate\MDIL\UnitTests.UWP.rsp"' returned exit code -1073741819

Hope this helps! 😄

Copy link
Member

@Sergio0694 Sergio0694 left a comment

Choose a reason for hiding this comment

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

Looks great! 🚀

Copy link
Member

@michael-hawker michael-hawker left a comment

Choose a reason for hiding this comment

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

Thanks @john-h-k for the submission! 🎉🎉🎉

@michael-hawker michael-hawker merged commit 43c28eb into CommunityToolkit:master Jun 5, 2020
@michael-hawker michael-hawker added this to the 6.1 milestone Jun 5, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants