Подтвердить что ты не робот

Почему генерируются инструкции AND?

Для кода, такого как:

int res = 0;
for (int i = 0; i < 32; i++)
{
    res += 1 << i;
}

Этот код сгенерирован (режим выпуска, без отладчика, 64 бит):

 xor edx,edx 
 mov r8d,1 
_loop:
 lea ecx,[r8-1] 
 and ecx,1Fh     ; why?
 mov eax,1 
 shl eax,cl 
 add edx,eax 
 mov ecx,r8d 
 and ecx,1Fh     ; why?
 mov eax,1 
 shl eax,cl 
 add edx,eax 
 lea ecx,[r8+1] 
 and ecx,1Fh     ; why?
 mov eax,1 
 shl eax,cl 
 add edx,eax 
 lea ecx,[r8+2] 
 and ecx,1Fh     ; why?
 mov eax,1 
 shl eax,cl 
 add edx,eax 
 add r8d,4 
 cmp r8d,21h 
 jl  _loop

Теперь я вижу там большинство инструкций, но что с инструкциями AND? ecx никогда не будет больше 0x1F в этом коде в любом случае, но я извиняюсь за это, не замечая этого (а также не замечая, что результат является константой), это не передовой компилятор, который может позволить себе потратить много времени по анализу в конце концов. Но что более важно, SHL с 32-битным операндом маскирует cl на 0x1F уже. Поэтому мне кажется, что эти ИО совершенно бесполезны. Почему они генерируются? У них есть какая-то цель, которую я не хватает?

4b9b3361

Ответ 1

and уже присутствует в коде CIL, выпущенном компилятором С#:

    IL_0009: ldc.i4.s 31
    IL_000b: and
    IL_000c: shl

Спецификация для инструкции CIL shl гласит:

Возвращаемое значение не указано, если значение shiftAmount больше или равно размеру значения.

Однако спецификация С# определяет 32-битный сдвиг, чтобы принять значение смещения mod 32:

Когда тип x равен int или uint,, счетчик сдвига задается пятью битами счета младшего порядка. Другими словами, подсчет сдвига вычисляется из count & 0x1F.

В этой ситуации С# компилятор не может действительно сделать намного лучше, чем испускать явную операцию and. Надеемся, что JITTER заметит это и оптимизирует избыточное and, но это требует времени, и скорость JIT очень важна. Поэтому рассмотрим эту цену, заплаченную за систему на основе JIT.

По-моему, реальный вопрос заключается в том, почему CIL задает инструкцию shl таким образом, когда С# и x86 определяют поведение усечения. Этого я не знаю, но я предполагаю, что его важно для спецификации CIL не указывать поведение, которое может привести к чему-то дорогостоящему на некоторых наборах инструкций. В то же время для С# важно иметь как можно меньше undefined поведений, потому что люди неизменно оказываются в таком стиле undefined до следующей версии компилятора/фреймворка/ОС/независимо от их изменения, нарушая код.

Ответ 2

x64 ядра уже применяют 5-битную маску к сумме сдвига. В руководстве Intel Processor, том 2B стр. 4-362:

Операндом назначения может быть регистр или ячейка памяти. Оператор count может быть немедленным значением или регистром CL. Счет маскируется до 5 бит (или 6 бит, если в 64-битном режиме и используется REG.W). Для подсчета 1 предоставляется специальная кодировка кода операции.

Так что машинный код не нужен. К сожалению, компилятор С# не может делать никаких предположений о поведении процессора и должен применять правила языка С#. И сгенерируйте IL, поведение которого указано в спецификации CLI. Ecma-335, Partion III, глава 3.58 говорит об операции SHL:

Команда shl сдвигает значение (int32, int64 или native int), оставшееся от количества бит, заданных shiftAmount. shiftAmount имеет тип int32 или собственный int. Возвращаемое значение не указано, если shiftAmount больше или равно ширине значения.

Unspecified находится здесь. Закрепление заданного поведения поверх неуказанных деталей реализации приводит к ненужному коду. Технически дрожание могло бы оптимизировать опкод. Хотя это сложно, оно не знает правила языка. Любой язык, который не указывает на маскировку, будет нелегко генерировать правильный IL. Вы можете отправить сообщение на сайт connect.microsoft.com, чтобы получить представление команды дрожания по этому вопросу.

Ответ 3

Компилятор С# должен вставлять эти инструкции AND при генерации промежуточного (машинно-независимого) кода, потому что оператор С# с левым сдвигом должен использовать только 5 наименее значимых бит.

При генерации кода x86 оптимизирующий компилятор может удалить эти ненужные инструкции. Но, по-видимому, он пропускает эту оптимизацию (возможно, потому что не может позволить себе потратить много времени на анализ).