нулевое назначение против xor, второе действительно быстрее? - PullRequest
15 голосов
/ 08 октября 2011

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

xor i,i

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

Ответы [ 3 ]

27 голосов
/ 08 октября 2011

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

  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 раза больше, занимает больше времени для загрузки из оперативной памяти и занимает гораздо больше места в кеше. В свое время время загрузки само по себе было убийственным, сегодня можно считать, что время цикла памяти и объем кеша не так заметны, но если ваш компилятор и / или код делают это слишком часто, вы увидите потерю кеша пространство и / или выселения, и более медленные циклы системной памяти.

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

Есть также преимущества в производительности по сравнению с xor по сравнению с mov в некоторых процессорах x86 (особенно Intel), которые не имеют ничего общего с размером кода , поэтому обнуление xor всегда предпочтительнее в сборке 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 вместо 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 всегда дает ноль, но только помогает производительности в сборке x86.


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

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

5 голосов
/ 08 октября 2011

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

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

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

0 голосов
/ 10 октября 2011

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

...