Вы просто inc %rbx
увеличиваете значение указателя. (%rbx)
разыменовывает этот регистр, используя его значение в качестве адреса памяти. В x86 каждый байт имеет свой собственный адрес (это свойство называется «адресуемым байтом»), а адреса - это просто целые числа, которые помещаются в регистр.
Все символы в строке ASCII имеют ширину 1 байт, поэтому при увеличении указателя на 1 выполняется переход к следующему символу в строке ASCII. (Это неверно в общем случае UTF-8 с символами вне диапазона кодовых точек 1..127, но ASCII является подмножеством UTF-8.)
Терминология: код ASCII 0
называется NUL (один L), а не NULL. В C NULL - это понятие указателя. Строки неявной длины в стиле C могут быть описаны как оканчивающиеся нулем или нулем, но термин «оканчивающийся нулем» неправильно использует терминологию.
Вы должны выбрать другой регистр (тот, который заблокирован вызовом), чтобы вам не нужно было нажимать / вставлять его вокруг вашей функции. Ваш код не выполняет какие-либо вызовы функций, поэтому нет необходимости хранить индукционную переменную в регистре с сохранением вызовов.
Я не нашел хорошего простого примера в других вопросах и ответах SO. У них либо есть 2 ветви внутри цикла (включая один безусловный jmp), как тот, который я связал в комментариях, либо они тратят инструкции на увеличение указателя и счетчика. Использование режима индексированной адресации внутри цикла не страшно, но менее эффективно на некоторых процессорах, поэтому я бы по-прежнему рекомендовал делать приращение указателя -> вычитать конец-начало после цикла.
Вот как я бы написал минимальный strlen, который проверяет только 1 байт за раз (медленно и просто). Я сохранил сам цикл маленьким, и это ИМО - разумный пример хорошего способа написания циклов в целом. Часто сохранение компактности кода упрощает понимание функции в asm. (Дайте ему другое имя, отличное от strlen
, чтобы вы могли протестировать его без необходимости gcc -fno-builtin-strlen
или чего-то еще.)
.globl simple_strlen
simple_strlen:
lea -1(%rdi), %rax # p = start-1 to counteract the first inc
.Lloop: # do {
inc %rax # ++p
cmpb $0, (%rax)
jne .Lloop # }while(*p != 0);
# RAX points at the terminating 0 byte = one-past-end of the real data
sub %rdi, %rax # return length = end - start
ret
Возвращаемое значение strlen
- это индекс массива из 0
байта = длина данных без, включая терминатор.
Если бы вы вставляли это вручную (потому что это всего лишь цикл с тремя инструкциями), вам часто просто нужен указатель на терминатор 0, чтобы вы не беспокоились о вспомогательной ерунде, просто используйте RAX в конце цикла.
Чтобы избежать смещения инструкций LEA / INC перед первой загрузкой (которая стоит 2 цикла задержки перед первым cmp), можно выполнить очистку первой итерации или jmp
для входа в цикл на cmp / jne, после inc. Почему циклы всегда компилируются в do ... а стиль (прыжок хвостом)?.
Увеличение указателя с помощью LEA между cmp / jcc (например, cmp
; lea 1(%rax), %rax
; jne
) может быть хуже, потому что оно побеждает макрослияние cmp / jcc в один uop. (На самом деле макро-слияние cmp $imm, (%reg)
/ jcc в любом случае не происходит на процессорах Intel, таких как Skylake. cmp
хотя и выполняет микроплавление операнда памяти. Может быть, AMD объединяет cmp / jcc.) Кроме того, вы оставите цикл с RAX На 1 выше, чем вы хотите.
Таким образом, было бы столь же эффективно (в семействе Intel Sandybridge) movzx
(иначе movzbl
) загружать и расширять ноль байта до %ecx
и test %ecx, %ecx
/ jnz
в качестве условия цикла. Но больший размер кода.
Большинство процессоров будут запускать мой цикл с частотой 1 итерация за такт. Возможно, мы могли бы приблизиться к 2 байтам за цикл (при этом проверяя только каждый байт отдельно) с некоторым развертыванием цикла.
Проверка 1 байта за раз для больших строк примерно в 16 раз медленнее, чем при использовании SSE2. Если вы не стремитесь к минимальному размеру кода и простоте, см. Почему этот код в 6,5 раза медленнее при включенной оптимизации? для простой SSE2 strlen, который использует регистр XMM. SSE2 является базовым для x86-64, поэтому вы всегда должны использовать его, когда он дает ускорение, для вещей, которые стоит писать вручную в asm.
Re: ваш обновленный вопрос с ошибочным портом реализации из Почему rax и rdi работают одинаково в этой ситуации?
RDI и RBX содержат указатели. Их сложение не дает действительного адреса! В коде, который вы пытались перенести, RCX (индекс) инициализируется нулем перед циклом. Но вместо xor %ebx, %ebx
вы сделали mov %rdi, %rbx
. Используйте отладчик для проверки значений регистров, пока вы выполняете пошаговый код.
person
Peter Cordes
schedule
02.03.2020
inc %rbx
, но использование RAX позволит вам делать то же самое без необходимости сохранять / восстанавливать RBX. И нет, вы не хотите использовать 32-битные регистры для указателей. - person Peter Cordes   schedule 02.03.2020