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

Нулевое назначение по сравнению с xor, является вторым быстрее?

кто-то показал мне несколько лет назад следующую команду для нулевой переменной.

xor i,i

Он сказал мне, что это быстрее, чем просто присвоить ему нуль. Это правда? Компиляторы делают оптимизацию, чтобы заставить код выполнять такую ​​вещь?

4b9b3361

Ответ 1

Вы можете попробовать это самостоятельно, чтобы увидеть ответ:

  movl $0,%eax
  xor %eax,%eax

собрать, затем разобрать:

as xor.s -o xor.o
objdump -D xor.o

И получите

   0:   b8 00 00 00 00          mov    $0x0,%eax
   5:   31 c0                   xor    %eax,%eax

команда mov для 32-битного регистра в 2,5 раза больше, занимает больше времени для загрузки из ram и потребляет гораздо больше пространства кеша. Еще в тот же день, когда время загрузки было убийцей, сегодня время цикла памяти и пространство кэша можно утверждать, что это не так заметно, но если ваш компилятор и/или код делают это слишком часто, вы увидите потерю кеша пространства и/или выселения, а также медленных циклов системной памяти.

В современных процессорах больший размер кода также может замедлять декодеры, возможно, препятствуя их расшифровке их максимального количества инструкций x86 за цикл. (например, до 4 инструкций в блоке 16B для некоторых процессоров.)

Есть также преимущества производительности для xor over mov в некоторых x86-процессорах (особенно Intel), которые не имеют никакого отношения к размеру кода, поэтому xor-zeroing всегда предпочтительнее в сборке x86.


Другой набор экспериментов:

void fun1 ( unsigned int *a )
{
    *a=0;
}
unsigned int fun2 ( unsigned int *a, unsigned int *b )
{
    return(*a^*b);
}
unsigned int fun3 ( unsigned int a, unsigned int b )
{
    return(a^b);
}


0000000000000000 <fun1>:
   0:   c7 07 00 00 00 00       movl   $0x0,(%rdi)
   6:   c3                      retq   
   7:   66 0f 1f 84 00 00 00    nopw   0x0(%rax,%rax,1)
   e:   00 00 

0000000000000010 <fun2>:
  10:   8b 06                   mov    (%rsi),%eax
  12:   33 07                   xor    (%rdi),%eax
  14:   c3                      retq   
  15:   66 66 2e 0f 1f 84 00    nopw   %cs:0x0(%rax,%rax,1)
  1c:   00 00 00 00 

0000000000000020 <fun3>:
  20:   89 f0                   mov    %esi,%eax
  22:   31 f8                   xor    %edi,%eax
  24:   c3                      retq   

Удерживает путь, показывающий, что для переменных xor i, i, как может быть в вашем вопросе. Поскольку вы не указали, какой процессор или какой контекст вы ссылаетесь, сложно рисовать всю картину. Если, например, вы говорите о коде C, вы должны понять, что компиляторы делают с этим кодом, и это сильно зависит от кода в самой функции, если во время вашего xor компилятор имеет операнд в регистре и зависит в настройках вашего компилятора вы можете получить xor eax, eax. или компилятор может выбрать изменить это на mov reg, 0 или изменить что-то = 0; к xor reg, рег.

Несколько дополнительных примеров:

если адрес переменной уже находится в регистре:

   7:   c7 07 00 00 00 00       movl   $0x0,(%rdi)

   d:   8b 07                   mov    (%rdi),%eax
   f:   31 c0                   xor    %eax,%eax
  11:   89 07                   mov    %eax,(%rdi)

Компилятор выберет значение mov zero вместо xor. Это то, что вы получили бы, если бы попробовали этот код C:

void funx ( unsigned int *a )
{
    *a=*a^*a;
}

Компилятор заменяет его на нуль. Такое же количество байтов получено, но требуется две памяти, а не один, и регистр сгорел. и три команды для выполнения вместо одного. Таким образом, нулевой ход заметно лучше.

Теперь, если размер байта и регистр:

13: b0 00                   mov    $0x0,%al
15: 30 c0                   xor    %al,%al

нет разницы в размере кода. (Но они все равно выполняются по-разному).


Теперь, если вы говорили о другом процессоре, скажем, ARM

   0:   e3a00000    mov r0, #0
   4:   e0200000    eor r0, r0, r0
   8:   e3a00000    mov r0, #0
   c:   e5810000    str r0, [r1]
  10:   e5910000    ldr r0, [r1]
  14:   e0200000    eor r0, r0, r0
  18:   e5810000    str r0, [r1]

Вы ничего не сохраняете, используя xor (эксклюзивный или eor): одна инструкция - это одна команда, как извлеченная, так и исполняемая. xoring что-то в ram, как и любой процессор, если у вас есть адрес переменной в регистре. Если вам нужно скопировать данные в другой регистр для выполнения xor, вы все равно получите два доступа к памяти и три инструкции. Если у вас есть процессор, который может делать память в памяти, движение нуля дешевле, потому что у вас есть только один доступ к памяти и одна или две команды в зависимости от процессора.

На самом деле это хуже, чем: eor r0, r0, r0 требуется иметь зависимость ввода от r0 (ограничение исполнения вне порядка) из-за правила упорядочения памяти. Xor-zeroing всегда производит ноль, но только помогает в производительности в сборке x86.


Итак, в нижней строке это зависит, если вы говорите регистры в ассемблере в системе x86 где угодно от 8088 до настоящего времени, xor часто быстрее, потому что команда меньше, быстрее получает, занимает меньше кеша, если у вас есть, оставляет больше кеша для другого кода и т.д. Точно так же процессоры с переменной длиной переменной, отличные от x86, которые нуждаются в нуле, которые должны быть закодированы в инструкции, также потребуют более длинную инструкцию, более длительное время выборки, больше потребляемого кеша, если есть кеш, и т.д. xor быстрее (обычно, зависит от того, как он кодируется). Будет намного хуже, если у вас есть условные флаги, и вы хотите, чтобы move/xor установил флаг нуля, вам может потребоваться записать правильную инструкцию (на некоторых процессорах mov не изменяет флаги). У некоторых процессоров есть специальный нулевой регистр, который не является общим назначением, когда вы его используете, вы получаете нуль таким образом, что вы можете кодировать этот очень распространенный случай использования, не сжигая больше пространства инструкций или не сжигая дополнительный цикл команд, загружая нуль немедленно в регистр, Например, msp430, перемещение 0x1234 обойдется вам в двухзначную инструкцию, но переместит 0x0000 или 0x0001, а несколько других констант могут быть закодированы в одном слове инструкции. Все процессоры будут иметь двойной доступ к памяти, если вы говорите о переменной в ram, read-modify-write два цикла памяти, не считая выборки команд, и ухудшается, если чтение вызывает заполнение строки кэша (тогда запись будет очень быстро), но без чтения запись может проходить только кэш-память и выполняться очень быстро, так как процессор может продолжать работать, пока запись происходит параллельно (иногда вы получаете выигрыш в производительности, иногда нет, всегда, если вы настраиваете для этого). X86 и, вероятно, более старые процессоры - причина, по которой вы видите привычку к xoring вместо перемещения нуля. Производительность по-прежнему существует сегодня для этих конкретных оптимизаций, системная память по-прежнему чрезвычайно медленная, и любые дополнительные циклы памяти являются дорогостоящими, так же как и любой кеш, который выброшен, является дорогостоящим. Половинные достойные компиляторы, даже gcc, обнаружат xor i, я как эквивалент я = 0 и выбирают в каждом случае лучшую (в средней системе) последовательность команд.

Получите копию Дзен Ассамблеи Майклом Абрашем. Хорошие, использованные копии доступны по разумной цене (менее 50 долларов США), даже если вы отправляетесь за 80 долларов США, это того стоит. Постарайтесь взглянуть за пределы конкретных 8088 "едоков цикла" и понять общий процесс мышления, который он пытается учить. Затем потратьте столько времени, сколько сможете разобрать свой код, идеально для многих разных процессоров. Примените то, что вы узнали...

Ответ 2

В более старых процессорах (но те, что были после Pentium Pro, согласно комментариям), это имело место, однако в большинстве современных процессоров в эти дни есть специальные горячие пути для нулевого назначения (регистров и хорошо выровненных переменных), которые должны обеспечивают эквивалентную производительность. большинство современных компиляторов будут иметь тенденцию использовать сочетание двух, в зависимости от окружающего кода (более старые компиляторы MSVC всегда будут использовать XOR в оптимизированных сборках, и он по-прежнему использует XOR совсем немного, но также будет использовать MOV reg,0 при определенных обстоятельствах).

Это очень большая микро-оптимизация, поэтому tbh, вы можете просто делать то, что когда-либо вам больше всего подходит, если у вас нет жестких циклов, которые отстают из-за зависимостей регистра. однако следует отметить, что использование XOR занимает большую часть времени, что отлично подходит для встроенных устройств или когда вы пытаетесь выровнять цель ветвления.

это предполагает, что вы в основном ссылаетесь на x86 и его производные, в этой заметке @Pascal дал мне идею поместить в технические ссылки, что для основы для этого. Руководство по оптимизации Intel имеет два раздела, посвященных этому, а именно 2.1.3.1 Dependancy Breaking Idioms и 3.5.1.7 Clearing Registers and Dependancy Breaking Idioms. Эти два раздела основополагающие сторонники используют инструкции на основе XOR для любой формы очистки регистра из-за ее нарушения, нарушающего характер (что устраняет латентность). Но в разделах, где необходимо сохранять коды условий, предпочтительнее MOV ing 0 в регистр.

Ответ 3

Определенно было верно на 8088 (и в меньшей степени 8086) из-за того, что инструкция xor короче, а очередь предварительной выборки - ограничения пропускной способности памяти.