Может ли x86’s MOV быть «свободным»? Почему я не могу воспроизвести это вообще?
Я постоянно вижу, что люди утверждают, что инструкция MOV может быть бесплатной в x86 из-за переименования регистра.
Для моей жизни я не могу проверить это в одном тестовом случае. Каждый тестовый пример, который я пытаюсь развенчать.
Например, вот код, который я компилирую с Visual C ++:
- Почему GCC, скомпилированный программой C, нуждается в разделе .eh_frame?
- Сколько байтов вводит push-команду в стек, если я не укажу размер операнда?
- Ошибка сегментации сборки после выполнения системного вызова, в конце моего кода
- Изменение целевых настроек процессора в Visual Studio 2010 Express
- Почему SSE скалярный sqrt (x) медленнее, чем rsqrt (x) * x?
#include #include #include int main(void) { unsigned int k, l, j; clock_t tstart = clock(); for (k = 0, j = 0, l = 0; j < UINT_MAX; ++j) { ++k; k = j; // <-- comment out this line to remove the MOV instruction l += j; } fprintf(stderr, "%d ms\n", (int)((clock() - tstart) * 1000 / CLOCKS_PER_SEC)); fflush(stderr); return (int)(k + j + l); }
Это создает следующий код сборки для цикла (не стесняйтесь делать это, как вы хотите, вам явно не нужен Visual C ++):
LOOP: add edi,esi mov ebx,esi inc esi cmp esi,FFFFFFFFh jc LOOP
Теперь я запускаю эту программу несколько раз, и я наблюдаю довольно последовательную 2-процентную разницу при удалении инструкции MOV:
Without MOV With MOV 1303 ms 1358 ms 1324 ms 1363 ms 1310 ms 1345 ms 1304 ms 1343 ms 1309 ms 1334 ms 1312 ms 1336 ms 1320 ms 1311 ms 1302 ms 1350 ms 1319 ms 1339 ms 1324 ms 1338 ms
Так что же дает? Почему MOV «свободен»? Этот цикл слишком сложный для x86?
Есть ли один пример, который может продемонстрировать, что MOV свободен, как утверждают люди?
Если так, то, что это? И если нет, то почему все продолжают требовать, чтобы MOV был бесплатным?
- Почему EDX должен быть 0, прежде чем использовать инструкцию DIV?
- Проверьте, равен ли регистр нулю с помощью CMP reg, 0 против OR reg, reg?
- Инструкция INC против ADD 1: Это имеет значение?
- Для чего предназначен регистр «FS» / «GS»?
- Получить количество циклов процессора?
- об ассемблере CF (Carry) и OF (Overflow)
- Вызов 32-битного кода из 64-битного процесса
- Может ли x86 переупорядочить узкий магазин с более широкой нагрузкой, которая полностью его содержит?
Пропуск петли в вопросе не зависит от латентности MOV или (от Haswell) преимущества использования блока исполнения.
Цикл по-прежнему остается всего 4-х футов для вывода интерфейса в kernel из-за порядка. ( mov
прежнему должен отслеживаться kernelм вне порядка, даже если ему не нужен блок исполнения, а макро-предохранители cmp/jc
– в один uop).
Процессоры Intel, так как Core2 имели ширину проблемы 4 мкп за такт, поэтому mov
не останавливает ее от выполнения в (рядом) одного итера за часы на Haswell. Он также работал бы на один за такт в Айвибридже (с отменой mov), но не на Sandybridge (без исключения). На SnB это будет примерно один цикл за 1.333c циклов, узкий по пропускной способности ALU, потому что mov
всегда будет нужен . (SnB / IvB имеет только три порта ALU, а Haswell – четыре).
Обратите внимание, что специальная обработка на этапе переименования была для x87 FXCHG (swap st0
с st1
) намного дольше, чем MOV. Agner Fog показывает FXCHG как 0 латентность на PPro / PII / PIII (kernel P6 первого поколения).
Цикл в вопросе имеет две взаимосвязанные цепи зависимостей ( add edi,esi
зависит от EDI и от счетчика циклов ESI), что делает его более чувствительным к несовершенному планированию. Снижение на 2% по сравнению с теоретическим предсказанием из-за кажущихся несвязанных команд не является необычным, и небольшие изменения в порядке инструкций могут сделать такую разницу. Чтобы работать с точностью до 1 с на каждый, каждый цикл должен запускать INC и ADD. Поскольку все INC и ADD зависят от предыдущей итерации, выполнение вне порядка не может догнать, запустив два за один цикл. Хуже того, ADD зависит от INC в предыдущем цикле, что я подразумевал под «блокировкой», поэтому потеря цикла в цепочке отпечатков INC также останавливает цепочку ADD dep.
Кроме того, предсказанные ветви могут работать только на port6, поэтому любой цикл, когда port6 не выполняет cmp / jc, представляет собой цикл потери пропускной способности . Это происходит каждый раз, когда INC или ADD крадут цикл на порте6 вместо запуска на портах 0, 1 или 5. IDK, если это преступник, или если потерять циклы в самих цепочках отпечатков INC / ADD, или, может быть, некоторые из них.
Добавление дополнительного MOV не добавляет никакого давления на порт выполнения, если оно устранено на 100%, но оно не позволяет переднему концу работать перед kernelм выполнения . (Только 3 из 4-х циклов в цикле нуждаются в исполнительном модуле, и ваш процессор Haswell может запускать INC и ADD на любом из своих 4 портов ALU: 0, 1, 5 и 6. Таким образом, узкие места:
- максимальная пропускная способность интерфейса 4 часа в час. (Цикл без MOV – всего 3 выхода, поэтому интерфейс может работать впереди).
- пропускная способность одного канала за такт.
- цепь зависимостей, включающая
esi
(латентность INC 1 за такт) - цепь зависимостей с использованием
edi
(задержка ADD 1 за такт, а также зависит от INC от предыдущей итерации)
Без MOV интерфейсный модуль может выдавать три выходных сигнала цикла на 4 за такт до тех пор, пока kernel ввода не будет заполнено. (AFAICT, он «разворачивает» крошечные циклы в кольцевом буфере (Loop Stream Detector: LSD), поэтому цикл с ABC-дисками может выдаваться в шаблоне ABCA BCAB CABC … Счетчик perf для lsd.cycles_4_uops
подтверждает, что он в основном проблемы в группах по 4, когда он выдает какие-либо ошибки.)
Процессоры Intel присваивают портам USB по мере их выхода в kernel из-за порядка . Решение основано на счетчиках, которые отслеживают, сколько uops для каждого порта уже находятся в планировщике (aka Reservation Station, RS). Когда в RS ожидает много ошибок, это хорошо работает и обычно должно избегать планирования INC или ADD на порт6. И я предполагаю, что также избегает планирования INC и ADD, так что время теряется из любой из этих цепочек dep. Но если RS пуст или почти пуст, счетчики не остановят ADD или INC от кражи цикла на порту6.
Я думал, что нахожусь на чем-то здесь, но любое субоптимальное планирование должно позволить фронту догнать и сохранить полный конец. Я не думаю, что мы должны ожидать, что интерфейсы вызовут достаточное количество пузырьков в трубопроводе, чтобы объяснить падение на 2% ниже максимальной пропускной способности, поскольку крошечный цикл должен работать от буфера цикла с очень последовательной 4-х тактовой пропускной способностью. Возможно, что-то еще происходит.
Реальный пример преимущества удаления mov
.
Я использовал lea
для построения цикла, который имеет только один mov
за такт, создавая прекрасную демонстрацию, когда MOV-устранение преуспевает на 100%, или 0% времени с mov same,same
чтобы продемонстрировать узкое место задержки.
Поскольку макро-fused dec/jnz
является частью цепочки зависимостей, включающей счетчик циклов, то несовершенное планирование не может отсрочить его. Это отличается от случая, когда cmp/jc
«разветвляется» из цепочки зависимостей критического пути на каждой итерации.
_start: mov ecx, 2000000000 ; each iteration decrements by 2, so this is 1G iters align 16 ; really align 32 makes more sense in case the uop-cache comes into play, but alignment is actually irrelevant for loops that fit in the loop buffer. .loop: mov eax, ecx lea ecx, [rax-1] ; we vary these two instructions dec ecx ; dec/jnz macro-fuses into one uop in the decoders, on Intel jnz .loop .end: xor edi,edi ; edi=0 mov eax,231 ; __NR_exit_group from /usr/include/asm/unistd_64.h syscall ; sys_exit_group(0)
В семействе Intel SnB LEA с одним или двумя компонентами в режиме адресации работает с задержкой 1 с (см. http://agner.org/optimize/ и другие ссылки в вики-теге x86 ).
Я построил и запускал это как статический бинарный файл в Linux, поэтому перфомансы пользовательского пространства для всего процесса измеряют только цикл с незначительными затратами на запуск / завершение работы. ( perf stat
очень легко по сравнению с тем, чтобы перенести счетчики в саму программу)
$ yasm -felf64 -Worphan-labels -gdwarf2 mov-elimination.asm && ld -o mov-elimination mov-elimination.o && objdump -Mintel -drwC mov-elimination && taskset -c 1 ocperf.py stat -etask-clock,context-switches,page-faults,cycles,instructions,branches,uops_issued.any,uops_executed.thread -r2 ./mov-elimination Disassembly of section .text: 00000000004000b0 <_start>: 4000b0: b9 00 94 35 77 mov ecx,0x77359400 4000b5: 66 66 2e 0f 1f 84 00 00 00 00 00 data16 nop WORD PTR cs:[rax+rax*1+0x0] 00000000004000c0 <_start.loop>: 4000c0: 89 c8 mov eax,ecx 4000c2: 8d 48 ff lea ecx,[rax-0x1] 4000c5: ff c9 dec ecx 4000c7: 75 f7 jne 4000c0 <_start.loop> 00000000004000c9 <_start.end>: 4000c9: 31 ff xor edi,edi 4000cb: b8 e7 00 00 00 mov eax,0xe7 4000d0: 0f 05 syscall perf stat -etask-clock,context-switches,page-faults,cycles,instructions,branches,cpu/event=0xe,umask=0x1,name=uops_issued_any/,cpu/event=0xb1,umask=0x1,name=uops_executed_thread/ -r2 ./mov-elimination Performance counter stats for './mov-elimination' (2 runs): 513.242841 task-clock:u (msec) # 1.000 CPUs utilized ( +- 0.05% ) 0 context-switches:u # 0.000 K/sec 1 page-faults:u # 0.002 K/sec 2,000,111,934 cycles:u # 3.897 GHz ( +- 0.00% ) 4,000,000,161 instructions:u # 2.00 insn per cycle ( +- 0.00% ) 1,000,000,157 branches:u # 1948.396 M/sec ( +- 0.00% ) 3,000,058,589 uops_issued_any:u # 5845.300 M/sec ( +- 0.00% ) 2,000,037,900 uops_executed_thread:u # 3896.865 M/sec ( +- 0.00% ) 0.513402352 seconds time elapsed ( +- 0.05% )
Как и ожидалось, цикл работает 1G раз ( branches
~ = 1 миллиард). «Дополнительные» 111k циклов за 2G – это накладные расходы, которые присутствуют и в других тестах, в том числе и без mov
. Это не случайный отказ от удаления mov, но он масштабируется с подсчетом итераций, поэтому это не просто накладные расходы на запуск. Вероятно, это из-за таймерных прерываний, так как IIRC Linux perf
не возится с первыми счетчиками при обработке прерываний и просто позволяет им продолжать подсчет. ( perf
виртуализирует аппаратные счетчики производительности, поэтому вы можете получать подсчеты за каждый процесс, даже когда stream переносится через процессоры.) Кроме того, прерывания таймера в логическом ядре дочернего узла, которые имеют одинаковое физическое kernel, будут немного беспокоить.
Узким местом является цепочка зависимостей, связанная с циклом, с использованием счетчика циклов. Циклы 2G для 1G-iteratorов – 2 такта на итерацию или 1 такт на декрет. Подтверждает, что длина цепочки dep составляет 2 цикла. Это возможно только в том случае, если mov
имеет нулевую задержку . (Я знаю, что это не доказывает, что нет какого-либо другого узкого места. Это действительно только доказывает, что латентность не более 2-х циклов, если вы не верите моему утверждению, что латентность является единственным узким местом. perf counter, но у него не так много вариантов для разрушения, из которого исчерпан ресурс микроархитектуры.)
В этом цикле имеется 3 скомпилированных домена: mov
, lea
и macro-fused dec/jnz
. uops_issued.any
3G uops_issued.any
подтверждает, что: он рассчитывается в объединенном домене, который является всем конвейером от декодеров до выхода на пенсию, за исключением планировщика (RS) и исполнительных блоков. (пары с макросплавными командами остаются везде как единичные. Только для микро-слияния хранилищ или загрузки ALU + 1 uop в плавном домене в ROB отслеживает прогресс двух непроверенных доменов.)
2G uops_executed.thread
( uops_executed.thread
-domain) сообщает нам, что все mov
uops были устранены (т.е. обрабатываются этапом выпуска / переименования и помещены в ROB в уже выполненном состоянии). Они по-прежнему занимают пропускную способность для выхода / выхода на пенсию, а также пространство в кэше uop и размер кода. Они занимают место в ROB, ограничивая размер windows вне порядка. Инструкция mov
никогда не бывает свободной. Существует множество возможных недостатков микроархитектуры, помимо латентных и рабочих портов, причем наиболее важным из них является 4-х уровневая проблема с интерфейсом.
На процессорах Intel отсутствие нулевой задержки часто является крупной сделкой, чем не требуется блок исполнения, особенно в Haswell, а затем, где есть 4 порта ALU. (Но только 3 из них могут обрабатывать векторные вершины, поэтому не устраненные движения векторов будут более узким местом, особенно в коде без большого количества загрузок или хранилищ с пропускной способностью переднего плана (4 сокета с плавным доменом на каждый такт) от ALU uops Кроме того, планирование uops для исполнительных блоков не является совершенным (более похоже на старейшее из первых), поэтому uops, которые не находятся на критическом пути, могут красть циклы с критического пути.)
Если мы поместим в цикл nop
или xor edx,edx
, они также будут выпущены, но не будут выполняться на процессорах Intel SnB-семейства.
Отмена устранения нулевой задержки может быть полезна для нулевого расширения от 32 до 64 бит и от 8 до 64. ( movzx eax, bl
исключается, movzx eax, bx
нет ).
Без mov-elim
mov ecx, ecx # Intel can't eliminate mov same,same lea ecx, [rcx-1] dec ecx jnz .loop 3,000,320,972 cycles:u # 3.898 GHz ( +- 0.00% ) 4,000,000,238 instructions:u # 1.33 insn per cycle ( +- 0.00% ) 1,000,000,234 branches:u # 1299.225 M/sec ( +- 0.00% ) 3,000,084,446 uops_issued_any:u # 3897.783 M/sec ( +- 0.00% ) 3,000,058,661 uops_executed_thread:u # 3897.750 M/sec ( +- 0.00% )
Это требует 3G циклов для итераций 1G, потому что длина цепочки зависимостей теперь составляет 3 цикла.
Счетчик совпадений с плавной областью не изменился, но 3G.
Что изменилось, так это то, что теперь счетчик uop незадействованных доменов совпадает с объединенным доменом. Для всех юотов нужен исполнительный блок; ни одна из команд mov
была устранена, поэтому все они добавили 1c латентность к цепочке отрезков, связанной с циклом.
(Когда есть микро-fused uops, например add eax, [rsi]
, счетчик uops_executed
может быть выше, чем uops_issued
. Но у нас этого нет.)
Без mov
вообще:
lea ecx, [rcx-1] dec ecx jnz .loop 2,000,131,323 cycles:u # 3.896 GHz ( +- 0.00% ) 3,000,000,161 instructions:u # 1.50 insn per cycle 1,000,000,157 branches:u # 1947.876 M/sec 2,000,055,428 uops_issued_any:u # 3895.859 M/sec ( +- 0.00% ) 2,000,039,061 uops_executed_thread:u # 3895.828 M/sec ( +- 0.00% )
Теперь мы возвращаемся к задержке в 2 цикла для цепочки отрезков с циклом.
Ничто не устраняется.
Я тестировал на Skylake 3,7 ГГц i7-6700k. Я получаю одинаковые результаты на Haswell i5-4210U (с точностью до 40 тыс. Из 1 Г) для всех перфомансов. Это примерно такая же ошибка, как и повторная работа в одной и той же системе.
Обратите внимание, что если я запускал perf
как root и считал cycles
вместо cycles:u
(только для пользовательского пространства), он измеряет частоту процессора как точно 3.900 ГГц. (IDK почему Linux только подчиняется bios-настройкам для max turbo сразу после перезагрузки, но затем падает до 3,9 ГГц, если я оставлю его бездействующим в течение пары минут. Asus Z170 Pro Gaming mobo, Arch Linux с kernelм 4.10.11-1-ARCH . Пила то же самое с Ubuntu. Написание balance_performance
для каждого из /sys/devices/system/cpu/cpufreq/policy[0-9]*/energy_performance_preference
из /etc/rc.local
исправляет его, но запись balance_power
заставляет его отбрасывать назад до 3.9 ГГц снова.)
Вы должны получить те же результаты в AMD Ryzen, так как он может исключить целое mov
. Семейство AMD Bulldozer может только уничтожить копии регистра xmm. (Согласно Agner Fog, копии регистра ymm
– это исключенная нижняя половина и ALU op для верхней половины.)
Например, AMD Bulldozer и Intel Ivybridge могут поддерживать пропускную способность 1 на часы для
movaps xmm0, xmm1 movaps xmm2, xmm3 movaps xmm4, xmm5 dec jnz .loop
Но Intel Sandybridge не может устранить ходы, так что это будет узким местом на 4 ALU uops для 3 портов выполнения. Если бы это было pxor xmm0,xmm0
вместо movaps, SnB также мог выдержать одну итерацию за такт. (Но семейство Bulldozer не могло, поскольку xor-zeroing по-прежнему нуждается в исполнительном модуле на AMD, хотя он не зависит от старого значения регистра. И семейство Bulldozer имеет только 0,5c пропускную способность для PXOR.)
Ограничения отмены mov
Две зависимые команды MOV в строке раскрывают разницу между Хасуэлом и Скайлаком.
.loop: mov eax, ecx mov ecx, eax sub ecx, 2 jnz .loop
Хасуэлл: незначительная переменная от run-to-run (от 1,746 до 1,749 c / iter), но это типично:
1,749,102,925 cycles:u # 2.690 GHz 4,000,000,212 instructions:u # 2.29 insn per cycle 1,000,000,208 branches:u # 1538.062 M/sec 3,000,079,561 uops_issued_any:u # 4614.308 M/sec 1,746,698,502 uops_executed_core:u # 2686.531 M/sec 745,676,067 lsd_cycles_4_uops:u # 1146.896 M/sec
Не все инструкции MOV устранены: около 0,75 из 2 за итерацию использовали порт выполнения. Каждое MOV, которое выполняется вместо исключения, добавляет 1c латентности к цепочке uops_executed
cycles
, поэтому не случайно, что uops_executed
и cycles
очень похожи. Все вершины являются частью одной цепочки зависимостей, поэтому параллелизм невозможен. cycles
всегда примерно на 5M выше, чем uops_executed
независимо от изменения run-to-run, поэтому я предполагаю, что в другом месте всего 5M циклов.
Skylake: более стабильны, чем результаты HSW, и больше исключений mov: всего 0,6666 MOV каждого 2 требуется блок исполнения.
1,666,716,605 cycles:u # 3.897 GHz 4,000,000,136 instructions:u # 2.40 insn per cycle 1,000,000,132 branches:u # 2338.050 M/sec 3,000,059,008 uops_issued_any:u # 7014.288 M/sec 1,666,548,206 uops_executed_thread:u # 3896.473 M/sec 666,683,358 lsd_cycles_4_uops:u # 1558.739 M/sec
На Haswell lsd.cycles_4_uops
учитывали все lsd.cycles_4_uops
. (0,745 * 4 ~ = 3). Таким образом, почти в каждом цикле, в котором выпущены любые uops, выдается полная группа из 4 (из буфера цикла. Вероятно, я должен был посмотреть на другой счетчик, который не заботится о том, откуда они пришли, например, uops_issued.stall_cycles
для подсчета циклы, в которых не было выпущено ни одного uops).
Но на SKL 0.66666 * 4 = 2.66664
меньше 3, поэтому в некоторых циклах на интерфейсе было выпущено менее 4 часов. (Обычно он останавливается, пока в ядре из-за порядка нет места для выпуска всей группы из 4, вместо того, чтобы выпускать неполные группы).
Странно, что IDK – то, что является точным микроархитектурным ограничением. Поскольку цикл равен всего 3 мкп, каждая группа проблем из 4-х компьютеров – это больше, чем полная итерация. Таким образом, группа вопросов может содержать до 3 зависимых MOV. Может быть, Skylake спроектирован так, чтобы иногда разрывать это, чтобы позволить больше отменить удаление?
update : на самом деле это нормально для 3-юп-петель на Skylake. uops_issued.stall_cycles
показывает, что HSW и SKL выдают простой цикл 3 uop без исключения mov так же, как они выдают этот. Таким образом, лучшее устранение mov – побочный эффект разделения групп проблем по какой-либо другой причине. (Это не узкое место, потому что принятые ветви не могут выполняться быстрее, чем 1 за такт, независимо от того, насколько быстро они выдают). Я до сих пор не знаю, почему SKL отличается, но я не думаю, что это о чем беспокоиться.
В менее экстремальном случае SKL и HSW одинаковы, причем оба из них не могут устранить 0,3333 из каждых 2 инструкций MOV:
.loop: mov eax, ecx dec eax mov ecx, eax sub ecx, 1 jnz .loop
2,333,434,710 cycles:u # 3.897 GHz 5,000,000,185 instructions:u # 2.14 insn per cycle 1,000,000,181 branches:u # 1669.905 M/sec 4,000,061,152 uops_issued_any:u # 6679.720 M/sec 2,333,374,781 uops_executed_thread:u # 3896.513 M/sec 1,000,000,942 lsd_cycles_4_uops:u # 1669.906 M/sec
Все проблемы с uops в группах по 4. Любая смежная группа из 4-х совпадений будет содержать ровно два MOV-модуля, которые являются кандидатами на элиминацию. Поскольку он явно преуспевает в устранении обоих в некоторых циклах, IDK почему он не всегда может это сделать.
В руководстве по оптимизации Intel говорится, что перезапись результата устранения movzx
как можно раньше освобождает микроархитектурные ресурсы, чтобы он мог преуспеть чаще, по крайней мере, для movzx
. См. Пример 3-25. Повторная упорядочивание последовательности для повышения эффективности инструкций MOV с нулевой задержкой .
Может быть, это отслеживается внутри с таблицей с ограниченным размером ссылок? Что-то должно прекратить запись файла физического регистра, если оно больше не требуется в качестве значения исходного архитектурного регистра, если оно по-прежнему необходимо в качестве значения места назначения mov. Освобождение записей PRF как можно скорее является ключевым, поскольку размер PRF может ограничить окно вне порядка меньшим размера ROB.
Я попробовал примеры на Хасуэлл и Скайлак, и обнаружил, что удаление mov действительно на самом деле значительно больше времени, когда это делается, но что это было фактически немного медленнее в общих циклах, а не быстрее. Пример должен был показать преимущества IvyBridge, которые, вероятно, являются узкими местами на его 3 портах ALU, но HSW / SKL только узкое место в конфликтах ресурсов в цепочках dep и, похоже, не беспокоит необходимость использования порта ALU для большей части инструкции movzx
.
См. Также Почему XCHG reg, reg 3 инструкции по микрооперации на современных архитектурах Intel? для большего количества исследований + догадки о том, как работает отмена mov, и может ли он работать для xchg eax, ecx
. (На практике xchg reg,reg
– это 3 ALU uops на Intel, но 2 устранены uops на Ryzen. Интересно догадаться, мог ли Intel реализовать его более эффективно.)
BTW, в качестве обходного пути для хаустелла, Linux не предоставляет uops_executed.thread
когда включена гиперпоточность, только uops_executed.core
. Другое kernel было абсолютно бездействующим все время, даже прерывания таймера, потому что я взял его в автономном режиме с помощью echo 0 > /sys/devices/system/cpu/cpu3/online
. К сожалению, это невозможно сделать, прежде чем perf
решит, что HT включен, а у моего ноутбука Dell нет опции BIOS для отключения HT. Поэтому я не могу заставить perf
использовать все 8 аппаратных счетчиков PMU сразу в этой системе, только 4.: /
Вот два небольших теста, которые, как я считаю, убедительно показывают доказательства отмены mov:
__loop1: add edx, 1 add edx, 1 add ecx, 1 jnc __loop1
против
__loop2: mov eax, edx add eax, 1 mov edx, eax add edx, 1 add ecx, 1 jnc __loop2
Если mov
добавит цикл в цепочку зависимостей, ожидается, что вторая версия займет около 4 циклов на итерацию. На моем Хасуэле оба принимают около 2 циклов на итерацию, что не может произойти без отмены mov.