Skip to content

Mask count in shift operations on native integers#61341

Merged
jcouv merged 9 commits intodotnet:mainfrom
jcouv:mask-shift
Jun 1, 2022
Merged

Mask count in shift operations on native integers#61341
jcouv merged 9 commits intodotnet:mainfrom
jcouv:mask-shift

Conversation

@jcouv
Copy link
Copy Markdown
Member

@jcouv jcouv commented May 16, 2022

Fixes #43347

@jcouv jcouv self-assigned this May 16, 2022
@jcouv jcouv force-pushed the mask-shift branch 2 times, most recently from 0aa2160 to 09ae707 Compare May 16, 2022 09:06
@tannergooding
Copy link
Copy Markdown
Member

tannergooding commented May 17, 2022

Does this need a corresponding "breaking change" doc?

Notably x86/x64 performs "masked shifting" already and so nint << x was already effectively being handled as int << (x & 0x1F) on 32-bit and long << (x & 0x3F) on 64-bit. It looks like Arm64 does the same and so I would expect that next to no customers are actually impacted by this change.

However, some platform supported by another runtime might see differing behavior when compiled with C# 11 vs when compiled with a prior version since nint isn't emitting the appropriate masking required for determinism today.

@jcouv jcouv force-pushed the mask-shift branch 2 times, most recently from 2633235 to 0873c0a Compare May 17, 2022 02:51
@jcouv
Copy link
Copy Markdown
Member Author

jcouv commented May 17, 2022

@tannergooding I was not planning to add a breaking change entry for this fix. But we can discuss it during code review.

@jcouv jcouv marked this pull request as ready for review May 25, 2022 05:50
@jcouv jcouv requested a review from a team as a code owner May 25, 2022 05:50
// - When the type of `x` is `nint` or `nuint`, the shift count is given by
// the low-order five bits of `count` on a 32 bits platform, or
// the lower-order six bits of `count` on a 64 bits platform.
// The shift count is computed from `count & (sizeof(nint) * 8 - 1)` (or `nuint`),
Copy link
Copy Markdown
Contributor

@AlekseyTs AlekseyTs May 25, 2022

Choose a reason for hiding this comment

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

(or nuint)

I am not sure what this part is supposed to convey. #Closed

// - When the type of `x` is `nint` or `nuint`, the shift count is given by
// the low-order five bits of `count` on a 32 bits platform, or
// the lower-order six bits of `count` on a 64 bits platform.
// The shift count is computed from `count & (sizeof(nint) * 8 - 1)` (or `nuint`),
Copy link
Copy Markdown
Contributor

@AlekseyTs AlekseyTs May 25, 2022

Choose a reason for hiding this comment

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

computed from

"computed as" rather than "computed from"? #Closed

TypeSymbol rightType = loweredRight.Type;
Debug.Assert(rightType.SpecialType == SpecialType.System_Int32);

if (rightConstantValue != null && rightConstantValue.IsIntegral && rightConstantValue.Int32Value <= 0x1F)
Copy link
Copy Markdown
Contributor

@AlekseyTs AlekseyTs May 25, 2022

Choose a reason for hiding this comment

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

IsIntegral

IsIntegral actually checks for all integral types. Perhaps we should check Discriminator == ConstantValueTypeDiscriminator.Int32 instead. #Closed

}

return oldNode == null
? _factory.Binary(operatorKind, type, loweredLeft, loweredRight)
Copy link
Copy Markdown
Contributor

@AlekseyTs AlekseyTs May 25, 2022

Choose a reason for hiding this comment

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

_factory.Binary(operatorKind, type, loweredLeft, loweredRight)

Consider using syntax from the context as RewriteBuiltInShiftOperation does. #Closed

}

[Fact, WorkItem(43347, "https://github.com/dotnet/roslyn/issues/43347")]
public void MaskShiftCount()
Copy link
Copy Markdown
Contributor

@AlekseyTs AlekseyTs May 25, 2022

Choose a reason for hiding this comment

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

MaskShiftCount

Is this a dupe of a test added to NumericIntPtrTests.cs? #Closed

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

We have tests for old native integers (ie. using a runtime without feature flag) and new native integers (on new runtime).
This file is for the old native integers (fix applies to them too).

@AlekseyTs
Copy link
Copy Markdown
Contributor

Done with review pass (commit 3)

// For the predefined operators, the number of bits to shift in `x << count`, `x >> count` or `x >>> count` is computed as follows:
// [...]
// - When the type of `x` is `nint` or `nuint`, the shift count is given by
// the low-order five bits of `count` on a 32 bits platform, or
Copy link
Copy Markdown
Contributor

@cston cston May 26, 2022

Choose a reason for hiding this comment

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

32 bits platform

Consider using "... bit platform" (singular "bit") throughout this comment to match the spec. #Closed

TypeSymbol rightType = loweredRight.Type;
Debug.Assert(rightType.SpecialType == SpecialType.System_Int32);

if (rightConstantValue != null && rightConstantValue.IsIntegral && rightConstantValue.Int32Value <= 0x1F)
Copy link
Copy Markdown
Contributor

@cston cston May 26, 2022

Choose a reason for hiding this comment

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

rightConstantValue.Int32Value <= 0x1F

Are we checking rightConstantValue >= 0? #Closed

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Thanks. Added

validate("nuint", "NUintMaxValue", ">>> 63", "0x0000_0001", "0x0000_0000_0000_0001", nuint_shr_un(63));
validate("nuint", "NUintMaxValue", ">>> 64", "0xFFFF_FFFF", "0xFFFF_FFFF_FFFF_FFFF", nuint_shr_un(64));

// lifted value (sampler)
Copy link
Copy Markdown
Contributor

@cston cston May 26, 2022

Choose a reason for hiding this comment

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

(sampler)

What does "sampler" mean here and below? #Closed

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

It was meant as "not as exhaustive" (just picking a few interesting permutations). I'll remove

// return x op count;
Diagnostic(ErrorCode.ERR_BadBinaryOps, $"x {op} count").WithArguments(op, type, "int").WithLocation(5, 16)
);
}
Copy link
Copy Markdown
Contributor

@cston cston May 26, 2022

Choose a reason for hiding this comment

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

}

Are we masking the shift amount in the following?

class Program
{
    static void Main()
    {
        System.Console.WriteLine(ShiftRight(255));
    }
    static nint ShiftRight(nint x) => x >> (-62);
}
``` #Resolved


if (rightConstantValue != null
&& rightConstantValue.Discriminator == ConstantValueTypeDiscriminator.Int32
&& rightConstantValue.Int32Value is >= 0 and <= 0x1F)
Copy link
Copy Markdown
Contributor

@AlekseyTs AlekseyTs May 27, 2022

Choose a reason for hiding this comment

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

rightConstantValue.Int32Value is >= 0 and <= 0x1F

Perhaps we will be able to handle some negative numbers as well with the following logic?

int originalValue = rightConstantValue.Int32Value;
int maskedValue = originalValue & 0x1F;
bool useMaskedValue = (originalValue & ~((1 << 31) | maskedValue )) == 0;

#Resolved

@AlekseyTs
Copy link
Copy Markdown
Contributor

Done with review pass (commit 5)

@jcouv jcouv requested a review from AlekseyTs May 27, 2022 22:33
&& rightConstantValue.Discriminator == ConstantValueTypeDiscriminator.Int32
&& (rightConstantValue.Int32Value & 0b100_000) == 0)
{
// For cases where count doesn't have the sixth bit set, we statically know the masked count.
Copy link
Copy Markdown
Contributor

@AlekseyTs AlekseyTs May 27, 2022

Choose a reason for hiding this comment

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

// For cases where count doesn't have the sixth bit set, we statically know the masked count.

It is not clear to me why this is the case. What about shifting by 64 on 128bit platform? #Closed

Copy link
Copy Markdown
Member

@tannergooding tannergooding May 27, 2022

Choose a reason for hiding this comment

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

nint becoming 128-bits would require revisions to the PE metadata format, the general IL support, and a number of other things. At this point, such a feature would break much existing .NET IL code (particularly given the prevalence of code like if (IntPtr.Size == 8) { } else { /* assume 32-bit */ } at some of the lowest layers)

Supporting a 128-bit platform, if it ever happens, is likely many years out. It wouldn't be unreasonable for the current compiler to just pretend it can't happen given the likelihood that it would require a break to the existing metadata format.

It also wouldn't be unreasonable to just guard against "too big shifts" or even emit a warning wave in such scenarios to help tell the user "this might not do what you think it does" (many users think that overshifting results in 0, like it does for byte, sbyte, short, and ushort when the mask is less than 31).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I can appreciate the trouble that runtime would have to go through, but I do not see a good reason for the compiler to do so as well (I guess apart from supporting new metadata format, if that would be even needed). We can easily have correct implementation that doesn't depend on the assumption that 64bit is the biggest size we will ever encounter. I do not see this assumption "helping" compiler today or in the long run.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Reverted to simpler logic (only optimize if count is >= 0 and <= 0x1F). This all isn't worth the effort to optimize for odd cases of negative counts.

Julien Couvreur added 2 commits May 27, 2022 16:40
@jcouv jcouv requested a review from AlekseyTs June 1, 2022 07:47
@jcouv jcouv requested a review from cston June 1, 2022 16:39
Copy link
Copy Markdown
Contributor

@AlekseyTs AlekseyTs left a comment

Choose a reason for hiding this comment

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

LGTM (commit 9)

@jcouv jcouv merged commit 9c99ebe into dotnet:main Jun 1, 2022
@ghost ghost added this to the Next milestone Jun 1, 2022
@jcouv jcouv deleted the mask-shift branch June 1, 2022 18:39
@RikkiGibson RikkiGibson modified the milestones: Next, 17.3 P3 Jun 28, 2022
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.

Use shift mask for shift operations with native integers

5 participants