For code such as this:
int res = 0;
for (int i = 0; i < 32; i++)
{
res += 1 << i;
}
This code is generated (release mode, no d
x64 cores already apply the 5 bit mask to the shift amount. From the Intel Processor manual, volume 2B page 4-362:
The destination operand can be a register or a memory location. The count operand can be an immediate value or the CL register. The count is masked to 5 bits (or 6 bits if in 64-bit mode and REG.W is used). A special opcode encoding is provided for a count of 1.
So that's machine code that isn't necessary. Unfortunately, the C# compiler cannot make any assumptions about the processor's behavior and must apply C# language rules. And generate IL whose behavior is specified in the CLI specification. Ecma-335, Partion III, chapter 3.58 says this about the SHL opcode:
The shl instruction shifts value (int32, int64 or native int) left by the number of bits specified by shiftAmount. shiftAmount is of type int32 or native int. The return value is unspecified if shiftAmount is greater than or equal to the width of value.
Unspecified is the rub here. Bolting specified behavior on top of unspecified implementation details produces the unnecessary code. Technically the jitter could optimize the opcode away. Although that's tricky, it doesn't know the language rule. Any language that specifies no masking will have a hard time generating proper IL. You can post to connect.microsoft.com to get the jitter team's view on the matter.