Я знаю о rep movsb, но задача состояла в том, чтобы сделать его в цикле байт за байтом, я не знаю, можно ли это сделать лучше.
Если у вас есть для зацикливания 1 байта за раз, вот как это сделать эффективно.Стоит упомянуть, потому что эффективный цикл полезен и для других случаев, кроме memcpy
!
Прежде всего, вы знаете, что ваше тело цикла должно выполняться хотя бы один раз, поэтому вы можете использовать нормальную структуру цикла сусловная ветвь внизу.( Почему циклы всегда компилируются в стиле "do ... while" (прыжок в хвост)? )
Во-вторых, если вы вообще не собираетесь разворачиваться, вам следует использоватьИндексированный режим адресации, чтобы избежать увеличения обоих указателей.(Но на самом деле было бы лучше развернуть).
И не используйте 16-битные регистры, если вам не нужно.Предпочитают 32-битный размер операнда (ECX);запись 32-битного регистра неявно расширяется до 64-битного, поэтому можно безопасно использовать индекс как часть режима адресации.
Вы можете использовать индексированную загрузку, но неиндексированное хранилище, так чтоваш моп-адрес магазина может все еще работать на порту 7, что делает его более дружественным к гиперпоточности на Haswell / Skylake.И избегать без ламинирования на Sandybridge.Очевидно, что копирование 1 байта за раз является полным мусором для производительности , но иногда вы действительно хотите зацикливаться, и на самом деле делает что-то с каждым байтом, пока он находится в регистре, и вы не можетевручную векторизовать его с помощью SSE2 (чтобы сделать 16 байтов за раз).
Вы можете сделать это путем индексации src относительно dst.
Или другой трюк состоит в подсчете отрицательного индексадо нуля, поэтому вы избегаете лишних cmp
.Давайте сначала это сделаем:
default rel ; use RIP-relative addressing modes by default
ARR_SIZE equ 100
section .data
a: times ARR_SIZE db 1
section .bss
b: resb ARR_SIZE ;reserve n bytes of space in the BSS
;section _start ; do *not* use custom section names unless you have a good reason
; they might get linked with unexpected read/write/exec permission
section .text
global _start
_start:
lea rsi, [a+ARR_SIZE] ; pointers to one-past-the-end of the arrays
lea rdi, [b+ARR_SIZE] ; RIP-relative LEA is better than mov r64, imm64
mov rcx, -ARR_SIZE
.copy_loop: ; do {
movzx eax, byte [rsi+rcx] ; load without a false dependency on the old value of RAX
mov [rdi+rcx], al
inc rcx
jnz .copy_loop ; }while(++idx != 0);
.end:
mov eax, 60
xor edi, edi
syscall ; sys_exit(0)
В позиционно-зависимом коде, таком как статический (или другой не-PIE) исполняемый файл Linux, mov edi, b+ARR_SIZE
является наиболее эффективным способом помещения статического адреса в регистр.
Не используйте _
для всех названий ваших этикеток._start
назван таким образом, потому что имена символов C, начинающиеся с _
, зарезервированы для использования реализацией.Это не то, что вы должны скопировать;на самом деле все наоборот.
Используйте .foo
для локального имени метки внутри функции.Например, .foo:
является сокращением для _start.foo:
, если вы используете его после _start
.
Индексирование src относительно dst:
Обычно ваш ввод ивыходные данные не находятся в статическом хранилище, поэтому вам нужно sub
адресов во время выполнения.Здесь , если мы поместим их обоих в один и тот же раздел, как вы делали изначально, mov rcx, a-b
фактически соберется.Но если нет, NASM отказывается.
На самом деле вместо режима адресации с двумя регистрами я мог бы просто делать [rdi + (a-b)]
или просто [rdi - ARR_SIZE]
, потому что я знаю, что они смежные.
_start:
lea rdi, [b] ; RIP-relative LEA is better than mov r64, imm64
mov rcx, a-b ; distance between arrays so [rdi+rcx] = [a]
;;; for a-b to assemble, I had to move b back to the .data section.
lea rdx, [rdi+ARR_SIZE] ; end_dst pointer
.copy_loop: ; do {
movzx eax, byte [rdi + rcx] ; src = dst+(src-dst)
mov [rdi], al
inc rdi
cmp rdi, rdx
jbe .copy_loop ; }while(dst < end_dst);
Указатель конца массива - это то же самое, что вы делали бы в C ++ с foo.end()
, чтобы получить указатель / итератор в один конец.
Это необходимоINC + CMP / JCC как издержки цикла.В процессорах AMD CMP / JCC могут макросжаться в 1 моп, а INC / JCC - нет, поэтому дополнительная CMP по сравнению с индексированием с конца в основном бесплатна.(За исключением размера кода).
На Intel это позволяет избежать индексированного хранилища.В этом случае нагрузка является чистой нагрузкой, так что в любом случае это один моп без необходимости микроплавления с меру ALU.Intel может макрособъединить inc/jcc
, так что это потребует дополнительных потерь в цикле.
Этот способ зацикливания хорош при развертывании, если вам не нужно избегать режима индексированной адресации длягрузы.Но если вы используете источник памяти для инструкции ALU, такой как vaddps ymm0, ymm1, [rdi]
, то да, вы должны увеличивать оба указателя по отдельности, чтобы вы могли использовать неиндексированные режимы адресации как для загрузки, так и для хранения, потому что процессоры Intel более эффективны в этом отношении.(Порт AGU хранилища 7 обрабатывает только неиндексированные данные, и некоторые микроплавкие нагрузки не ламинируются в режиме индексированной адресации. Режимы микросинтеза и адресации )