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?
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
isint
oruint
, 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
islong
orulong
, the shift count is given by the low-order six bits of count. In other words, the shift count is computed fromcount & 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
.