Зачем использовать только младшие пять бит операнда сдвига при сдвиге 32-битного значения? (например, (UInt32) 1 << 33 == 2)
Рассмотрим следующий код:
UInt32 val = 1;
UInt32 shift31 = val << 31; // shift31 == 0x80000000
UInt32 shift32 = val << 32; // shift32 == 0x00000001
UInt32 shift33 = val << 33; // shift33 == 0x00000002
UInt32 shift33a = (UInt32)((UInt64)val << 33); // shift33a == 0x00000000
Он не генерирует предупреждение (об использовании сдвига больше 32), поэтому оно должно быть ожидаемым.
Код, который фактически выводится на сгенерированную сборку (или, по крайней мере, рефлекторную интерпретацию кода),
uint val = 1;
uint shift31 = val << 0x1f;
uint shift32 = val;
uint shift33 = val << 1;
uint shift33a = val << 0x21;
IL (опять же, используя Reflector)
L_0000: nop
L_0001: ldc.i4.1
L_0002: stloc.0
L_0003: ldloc.0
L_0004: ldc.i4.s 0x1f
L_0006: shl
L_0007: stloc.1
L_0008: ldloc.0
L_0009: stloc.2
L_000a: ldloc.0
L_000b: ldc.i4.1
L_000c: shl
L_000d: stloc.3
L_000e: ldloc.0
L_000f: conv.u8
L_0010: ldc.i4.s 0x21
L_0012: shl
L_0013: conv.u4
L_0014: stloc.s shift33a
Я понимаю , что происходит (он описан в MSDN); когда код скомпилирован, используются только младшие 5 бит при смещении 32-битного значения... Мне любопытно, как это сделать почему.
(Вывод shift33a
также заставляет меня думать, что с Reflector что-то не совсем верно, так как их С# -презентация IL будет компилироваться на что-то другое)
Вопрос (ы):
- Почему являются только более низкими 5 битами значения "значение для переключения"?
- Если "не имеет смысла сдвигать более 31 бит", почему нет предупреждений?
- Является ли это обратной совместимостью (т.е. как программисты "ожидают" )?
- Правильно ли, что базовый IL может выполнять сдвиги более 31 бит (как в
L_0010: ldc.i4.s 0x21
), но компилятор обрезает значения?
Ответы
Ответ 1
В основном это сводится к тому, как x86 обрабатывает коды операций арифметического сдвига: он использует только нижние 5 бит числа сдвигов. Например, смотрите 80386 руководство по программированию. В C/С++ технически undefined поведение выполняет сдвиг бит более чем на 31 бит (для 32-разрядного целого), идя с философией C: "вы не платите за то, что вам не нужно", Из раздела 6.5.7, пункт 3 стандарта C99:
Целочисленные рекламные акции выполняются для каждого из операндов. Тип результата - это продвинутый левый операнд. Если значение правого операнда отрицательное или больше или равно ширине продвинутого левого операнда, поведение undefined.
Это позволяет компиляторам опустить одну команду переключения на x86 для сдвигов. 64-битные сдвиги не могут выполняться в одной инструкции на x86. Они используют SHLD/SHRD инструкции плюс некоторая дополнительная логика. В x86_64 64-битные сдвиги могут выполняться в одной инструкции.
Например, gcc 3.4.4 испускает следующую сборку для 64-битного сдвига влево на произвольную сумму (скомпилированную с -O3 -fomit-frame-pointer
):
uint64_t lshift(uint64_t x, int r)
{
return x << r;
}
_lshift:
movl 12(%esp), %ecx
movl 4(%esp), %eax
movl 8(%esp), %edx
shldl %cl,%eax, %edx
sall %cl, %eax
testb $32, %cl
je L5
movl %eax, %edx
xorl %eax, %eax
L5:
ret
Теперь я не очень хорошо знаком с С#, но я предполагаю, что он имеет аналогичную философию - создайте язык, чтобы он мог быть реализован как можно эффективнее. Указав, что операции сдвига используют только нижние 5/6 бит сдвига, он позволяет компилятору JIT скомпилировать сдвиги максимально оптимально. 32-разрядные сдвиги, а также 64-битные сдвиги в 64-битных системах могут получить JIT, скомпилированный в один код операции.
Если С# были перенесены на платформу с другим поведением для своих собственных кодов операций сдвига, то это фактически наложило бы дополнительный удар производительности - компилятор JIT должен был убедиться, что стандарт соблюден, поэтому ему нужно будет добавить дополнительная логика, чтобы убедиться, что были использованы только нижние 5/6 бит числа сдвигов.
Ответ 2
Unit32 переполняет 32 бита, что определено в спецификации. Что вы ожидаете?
CLR не определяет сдвиг влево с оператором обнаружения переполнения (1). Если вам нужен такой объект, вам нужно убедиться сами.
(1) Компилятор С# может отложить его до конца, но я не уверен.
Ответ 3
Я написал этот простой тест в C (gcc, linux) и получил аналогичные результаты. Интересно, однако, то, что константа определяет для более сдвига, превратилась в нуль вместо обертывания. Он действительно предупреждал об этом, поэтому, по крайней мере, было некоторое признание, что это было "неправильное" дело.
#include <stdio.h>
unsigned int is0 = 1 << 31;
unsigned int is1 = 1 << 32;
unsigned int is2 = 1 << 33;
int main()
{
unsigned int loopy = 0;
int x = 0;
printf("0x%08X\n", is0);
printf("0x%08X\n", is1);
printf("0x%08X\n", is2);
for (x = 0; x < 35; ++x)
{
loopy = 1 << x;
printf("%02d 0x%08X\n", x,loopy);
}
return 0;
}
Вот результаты:
0x80000000
0x00000000
0x00000000
00 0x00000001
01 0x00000002
02 0x00000004
03 0x00000008
04 0x00000010
05 0x00000020
06 0x00000040
07 0x00000080
08 0x00000100
09 0x00000200
10 0x00000400
11 0x00000800
12 0x00001000
13 0x00002000
14 0x00004000
15 0x00008000
16 0x00010000
17 0x00020000
18 0x00040000
19 0x00080000
20 0x00100000
21 0x00200000
22 0x00400000
23 0x00800000
24 0x01000000
25 0x02000000
26 0x04000000
27 0x08000000
28 0x10000000
29 0x20000000
30 0x40000000
31 0x80000000
32 0x00000001
33 0x00000002
34 0x00000004