Оператор И + сложение быстрее, чем вычитание

Я измерил время выполнения следующих кодов:

volatile int r = 768;
r -= 511;

volatile int r = 768;
r = (r & ~512) + 1;

сборка:

mov     eax, DWORD PTR [rbp-4]
sub     eax, 511
mov     DWORD PTR [rbp-4], eax

mov     eax, DWORD PTR [rbp-4]
and     ah, 253
add     eax, 1
mov     DWORD PTR [rbp-4], eax

результаты, достижения:

Subtraction time: 141ns   
AND + addition: 53ns

Я запускал фрагмент кода несколько раз, и результаты были стабильными.
Может ли кто-нибудь объяснить мне, почему это так, несмотря на то, что для версии AND + добавлена ​​еще одна строка сборки?


person Quest    schedule 19.03.2017    source источник
comment
Потому что вычитание дороже сложения ^^   -  person invisal    schedule 19.03.2017
comment
@invisal хорошо, но почему?   -  person Quest    schedule 19.03.2017
comment
Погрешность измерения. Я предполагаю, что разная длина кода повлияла на выравнивание в других частях кода (вы использовали цикл, верно?)   -  person Jester    schedule 19.03.2017
comment
Потому что вычитание сложнее. Вы можете проверить electronics-tutorials.ws/combination/comb_7.html и electronics-tutorials.ws/combination/binary-subtractor.html   -  person invisal    schedule 19.03.2017
comment
И как вы рассчитали это время? Скорее всего, вы неправильно выбираете время.   -  person Cornstalks    schedule 19.03.2017
comment
@invisal не имеет значения. В любом случае они проходят через одно и то же ALU, а не через сумматор, который каким-то образом на неполный цикл быстрее, чем вычитатель. Это не реально работать.   -  person harold    schedule 19.03.2017
comment
@invisal Там нет ничего, что говорило бы о том, что это сложнее или занимает больше времени.   -  person user207421    schedule 19.03.2017


Ответы (2)


Ваше утверждение, что один фрагмент быстрее другого, ошибочно.
Если вы посмотрите на код:

mov     eax, DWORD PTR [rbp-4]
....
mov     DWORD PTR [rbp-4], eax

Вы увидите, что во времени выполнения преобладает загрузка/сохранение в память.
Даже на Skylake это займет минимум 2+2 = 4 цикла.
1 цикл, который sub или 3 *) циклы, которые занимает and bytereg/add full reg, просто исчезают во времени доступа к памяти.
На более старых процессорах, таких как Core2, требуется минимум 5 циклов, чтобы выполнить пару загрузки/сохранения по одному и тому же адресу.

Такие короткие последовательности кода сложно рассчитать по времени, поэтому следует позаботиться о том, чтобы у вас была правильная методология.
Также необходимо помнить, что rdstc не является точным на процессорах Intel и не работает по порядку при загрузке.

Если вы используете правильный код синхронизации, например:

.... x 100,000    //stress the cpu using integercode in a 100,000 x loop to ensure it's running at 100%
cpuid             //serialize instruction to make sure rdtscp does not run early.
rdstcp            //use the serializing version to ensure it does not run late   
push eax
push edx
mov reg1,1000*1000   //time a minimum of 1,000,000 runs to ensure accuracy
loop:
...                  //insert code to time here
sub reg1,1           //don't use dec, it causes a partial register stall on the flags.
jnz loop             //loop
//kernel mode only!
//mov eax,cr0          //reading and writing to cr0 serializes as well.
//mov cr0,eax
cpuid                //serialization in user mode.
rdstcp               //make sure to use the 'p' version of rdstc.
push eax
push edx
pop 4x               //retrieve the start and end times from the stack.

Запустите временной код в 100 раз и возьмите наименьшее число циклов.
Теперь вы получите точный счет с точностью до 1 или 2 циклов.
Вам нужно замерить время пустого цикла. а также и вычтите время для этого, чтобы вы могли видеть чистое время, затраченное на выполнение интересующих инструкций.

Если вы сделаете это, вы обнаружите, что add и sub работают с одинаковой скоростью, точно так же, как это происходит в каждом процессоре x86/x64, начиная с 8086.
Это, конечно, также то, что Agner Fog, руководства по ЦП Intel, руководства по ЦП AMD и почти скажем, любой другой доступный источник.

*) and ah,value занимает 1 цикл, затем ЦП останавливается на 1 цикл из-за частичной записи в регистр, а add eax,value занимает еще один цикл.

Оптимизированный код

sub     DWORD PTR [rbp-4],511

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

person Johan    schedule 19.03.2017

Полный машинный код

8b 45 fc                mov    eax,DWORD PTR [rbp-0x4]
2d ff 01 00 00          sub    eax,0x1ff
89 45 fc                mov    DWORD PTR [rbp-0x4],eax

vs

8b 45 fc                mov    eax,DWORD PTR [rbp-0x4]
80 e4 fd                and    ah,0xfd
83 c0 01                add    eax,0x1
89 45 fc                mov    DWORD PTR [rbp-0x4],eax 

Это означает, что код для второй операции на самом деле длиннее всего на один байт (11 против 12). Скорее всего, ЦП извлекает код большими единицами в байтах, поэтому выборка не намного медленнее. Также он может декодировать несколько инструкций одновременно, так что и здесь первый образец не имеет преимущества. Выполнение одного add, and или sub занимает один проход ALU, поэтому все они занимают только один такт на одном исполнительном блоке. Это преимущество в 1 нс для вас на процессоре с тактовой частотой 1 ГГц.

Так что в основном обе операции более или менее одинаковы. Разница может быть связана с некоторыми другими факторами. Возможно, ячейка памяти rbp-0x4 все еще находится в кеше L1 до того, как вы запустите второй фрагмент кода. Либо инструкции для первого фрагмента расположены в памяти хуже достижимо. Или ЦП смог спекулятивно запустить второй фрагмент до того, как вы начали измерять и т. Д., Вам нужно знать, как вы измеряли скорость и т. Д., Чтобы решить это.

person sannaj    schedule 19.03.2017
comment
Извлечение не имеет значения, ЦП извлекает 32 байта за раз и запускает повторяющийся код из кеша uop, который не нужно ни извлекать, ни декодировать. Ваше утверждение, что sub против and bytereg+add wholereg занимает одинаковое время, неверно. первая занимает 1 такт, вторая 3. ОоО тоже нет, потому что все инструкции находятся в цепочке зависимостей. - person Johan; 19.03.2017
comment
Да, я на самом деле сказал, что ЦП извлекает код большими единицами, чем байты. Я не говорил, что они занимают одинаковое время, только более или менее одинаковое по сравнению с другими факторами. Что касается OoO, я не говорил, что инструкции могут выполняться не по порядку, только то, что они уже могут выполняться вместе с тем, что когда-либо было сделано до этого фрагмента. (Ну, честно говоря, это маловероятно). Кроме того, вы уверены насчет 3 циклов? Разве ЦП просто не переименует другой регистр в eax? - person sannaj; 19.03.2017
comment
ЦП не может переименовываться, потому что есть цепочка зависимостей. Он переименовывается только в том случае, если вы используете тот же регистр, но не в цепочке зависимостей. Инструкции в середине не могут быть выполнены раньше (т.е. OoO), потому что они зависят от результата первого хода. И, как указано, Intel не извлекает код в цикле, этот код уже находится в кэше uop, полностью извлечен и декодирован. Код вне цикла почти никогда не критичен по времени. Это сложно, вы можете прочитать все об этом на: agner.org/optimize/microarchitecture.pdf - person Johan; 19.03.2017