Запись 32-битного регистра всегда обнуляет верхние 32 полного 64-битного регистра. Вы могли бы сделать этот трюк легче с 16-битными половинами 32-битного регистра или особенно с младшими 8 битами.
( В задаче с кодом-гольфом , у меня когда-то была константа, в которой я нуждался только вне циклов, а ее младшие 8 бит были равны нулю. Я использовал ebx=-1024
вне внутренних циклов и bl
как мой счетчик циклов внутри циклов, оканчивающийся на bl = 0.)
Но обычно лучше просто использовать другой регистр или оставить счетчик внешнего цикла в памяти стека. (Или вывести другое редко используемое значение, особенно если оно в основном для чтения, так что вы можете просто использовать это как операнд источника памяти.)
Как предлагает Шут, тестируйте младшие 32 бита отдельно для состояния внутреннего цикла. (Это стоит 1 дополнительный моп в семействе Intel Sandybridge, где dec/jnz
может макро-слиться. Но 0 дополнительных моп в AMD или другом Intel, где dec/jnz
не может слиться, но test/jnz
может.)
Для внешнего цикла rcgldr уже предложил вращать до / после для замены 32-битных половин. (При неудачном выборе медленная loop
инструкция без веской причины.)
Но мы можем уменьшить это значение до 1 инструкции overhead помимо sub / jcc, который у вас обычно был бы . Если мы рассматриваем внешний счетчик как со знаком 32-бит и проверяем, чтобы он стал отрицательным, мы можем сделать эту проверку одновременно с воссозданием счетчика внутреннего цикла в ECX с тем же sub rcx
, (Это означает, что начальное значение счетчика должно быть на 1 ниже, потому что мы фактически останавливаемся на -1
вместо 0
.)
32-битный немедленный с расширенным знаком недостаточно велик, чтобы sub rcx, 1<<32
, и (если вам не нужна эта константа для чего-то другого), если вы используете 2 регистра, вам гораздо лучше использовать отдельные регистры для отдельные счетчики. Но с 2 вычитаниями, или на самом деле add
из -(2^31)
, мы можем обернуть низкие 32 почти полностью, вычитая 1 из верхней половины и оставляя счет для следующего внутреннего цикла в ECX.
inner_count equ 0x5678
outer_count equ 0x1234
global _start
_start:
xor eax, eax
xor edx, edx ; test counters to prove this loops the right number of times
mov rcx, ((outer_count-1)<<32) + inner_count
.outer:
.inner: ; do {
; ... inner loop body
inc rax ; instrumentation: inner++
dec rcx ; rcx--
test ecx,ecx
jnz .inner ; }while(ecx)
; ecx=0. rcx=outer count << 32
;... outer loop body
inc rdx ; instrumentation: outer++
add rcx, -1<<31 ; largest magnitude 32-bit immediate is INT_MIN, 0xFFFFFFFF8000000
sub rcx, (1<<31) - inner_count ; re-create the inner loop counter from 0 + INT_MIN
jge .outer
.end: ; set a breakpoint on _start.end and look at registers
mov eax, 231
syscall ; Linux sys_exit_group(edi=0)
Конечное состояние: rdx = 0x1234
, rax = 0x6260060 = 0x1234 * 0x5678
, поэтому эти циклы выполнялись правильное число раз.
В семействе Sandybridge sub
/ jge
может слиться в одну инструкцию. Несмотря на это, я думаю, что у него худший размер кода (2x sub r64, imm32), а ror rcx,32
- инструкция с одним битом для семейства Sandybridge и AMD. (https://agner.org/optimize/). Если ваш внешний счетчик был в формате RAX, краткая форма кодирования без байта ModRM могла бы помочь.
Это работает для любого внутреннего счетчика без знака, от 1 до 2^32 - 1
, и для любого внешнего счетчика со знаком, от 1 до 2^31 - 1
.
Внутренний счет не может быть 0 = 2 ^ 32, потому что это потребует 2x add rcx, 0xFFFFFFFF80000000
, чтобы обернуть все вокруг. С одной из инструкций, равной sub rcx,imm32
, наибольшее положительное число (без установки старших битов), которое мы можем вычесть, составляет 0x7fffffff
.
Это также может работать с jnc
, если мы используем обертывание верхней половины в качестве условия выхода из цикла, позволяя полный диапазон 2 ^ 32-1 для верхнего счетчика.
30-битный счетчик внизу RCX, 34-битный счетчик вверху
Тест внутреннего цикла становится
dec rcx
test ecx, (1<<31)-1 ; test the low 30 bits for non-zero
jnz .inner
Преимущество здесь в том, что один sub imm32
может обернуть внутренний счетчик туда, где он нам нужен:
sub rcx, (1<<31) - inner_count ; outer-- and re-create the inner loop counter
jnc .outer
Мы все еще не можем использовать jnz
, потому что повторное создание внутреннего счетчика одновременно означает, что весь регистр не будет равен нулю. Таким образом, мы должны разветвляться на это, становясь отрицательным или имея неподписанный перенос.