Search code examples
c#.net-coreundefined-behavior

Is logical shift left by word size undefined behavior in C#?


I'm writing .NET 8 code to generate bit masks of various sizes. These masks are generated on the "hot path" in my code, so performance is important. I have the following method:

public static ulong Mask(int bitsize)
{
    return (1ul << bitsize) - 1ul;
}

used in the following way:

using System;

var r = new ulong[3];
test(r);

foreach (var u in r)
{
    Console.WriteLine(u);
}

void test(ulong [] result) {
    result[0] = mask(32);
    result[1] = mask(63); 
    result[2] = mask(64);
}
  
static ulong mask(int bitsize)
{
    return (1ul << bitsize) - 1u;
}

My expectation was that I should be able to create a mask of "all ones" by calling mask(64). This would shift in 64 zero bits, from which 1 is subtracted. Instead, I'm seeing the JITter generate "all zeros". Wrapping the returned value in unchecked makes no difference.

This reminds me of undefined behavior in C++: if you do <forbidden-thing>, anything can happen in the program. The subtraction after the shift seems to have been completely lost.

Naturally, I can pre-calculate the mask values for bitsize ranging from 0 to 64, and do a table lookup, but a table lookup is going to be bigger/slower than simply:

mov eax, 1
shlx rax, rax, rcx
dec rax
ret

which the JITter can aggressively inline for great success.

So, is 1ul << 64 undefined behavior in C#? Is there another way to generate bitmasks dynamically without introducing branches in the code?


Solution

  • So, is 1ul << 64 undefined behavior in C#?

    Nope. From section 12.11 of the draft C# 8 spec:

    For the predefined operators, the number of bits to shift is computed as follows:

    • When the type of x is int or uint, the shift count is given by the low-order five bits of count. In other words, the shift count is computed from count & 0x1F.
    • When the type of x is long or ulong, the shift count is given by the low-order six bits of count. In other words, the shift count is computed from count & 0x3F.

    If the resulting shift count is zero, the shift operators simply return the value of x.

    The bottom 6 bits of 64 are all zero, so the result is just x.