Вы просто inc %rbx
увеличиваете значение указателя. (%rbx)
разыменование, которое регистрируется, используя его значение в качестве адреса памяти. В x86 каждый байт имеет свой собственный адрес (это свойство называется «адресуемый байт»), а адреса - это просто целые числа, которые помещаются в регистр.
Все символы в строке ASCII имеют ширину 1 байт, поэтому приращение увеличивается указатель на 1 перемещается к следующему символу в строке ASCII. (Это не так в общем случае UTF-8 с символами за пределами диапазона кодов 1..127, но ASCII является подмножеством UTF-8.)
Терминология: код ASCII 0
называется NUL (один L), а не NULL. В C NULL является концепцией указателя. Строки неявной длины C могут быть описаны как 0-завершенные или NUL-завершенные, но "null-terminated" неправильно использует терминологию.
Вы должны выбрать другой регистр (тот, который call-clobbered), поэтому вам не нужно нажимать / вставлять его вокруг вашей функции. Ваш код не вызывает какие-либо вызовы функций, поэтому нет необходимости сохранять переменную индукции в регистре с сохранением вызовов.
Я не нашел хорошего Простой пример в других вопросах и ответах. У них либо 2 ветви внутри l oop (включая один безусловный jmp), аналогичный тому, который я связал в комментариях, либо они теряют инструкции, увеличивающие указатель и счетчик. Использование режима индексированной адресации внутри l oop не страшно, но менее эффективно на некоторых процессорах, поэтому я все равно рекомендую делать приращение указателя -> вычитать end-start после l oop.
Вот так я бы написал минимальный strlen, который проверяет только 1 байт за раз (медленно и просто) . Я сохранил размер l oop небольшим, и это, по-моему, разумный пример хорошего способа записи циклов в целом. Зачастую компактность вашего кода облегчает понимание функции в 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
= длина данных не , включая терминатор.
Если вы вставляете это вручную (потому что это всего лишь 3 инструкции l oop), вам часто нужен просто указатель до терминатора 0, чтобы вы не беспокоились о дополнительном дерьме, просто используйте RAX в конце l oop.
Избегайте смещающих инструкций LEA / IN C до первой загрузки (которая стоимость 2 цикла задержки до первого cmp) может быть выполнена путем очистки первой итерации или с помощью jmp
для ввода l oop в cmp / jne после in c. Почему циклы всегда компилируются в стиле "do ... while" (прыжок в хвост)? .
Увеличение указателя с LEA между cmp / j cc (например, cmp
; lea 1(%rax), %rax
; jne
) может быть хуже, потому что он побеждает макрослияние cmp / j cc в один моп. (На самом деле, слияние макросов cmp $imm, (%reg)
/ j cc не происходит на процессорах Intel, таких как Skylake, в любом случае. cmp
микросжигает операнд памяти, хотя. Возможно, AMD сливает cmp / j cc.) Кроме того, вы бы оставили l oop с RAX 1 выше, чем вы хотите.
Таким образом, было бы столь же эффективно (на семействе Intel Sandybridge) загрузить movzx
(он же movzbl
) и продлить ноль байта до %ecx
и test %ecx, %ecx
/ jnz
в качестве условия l oop. Но больший размер кода.
Большинство процессоров будут запускать мою l oop с 1 итерацией за такт. Возможно, мы могли бы получить около 2 байтов за цикл (при этом все еще проверяя каждый байт отдельно) с некоторым развертыванием l oop.
Проверка 1 байта за раз примерно на 16 раз медленнее для больших строк, чем мы могли бы go с SSE2. Если вы не стремитесь к минимальному размеру и простоте кода, см. Почему этот код работает в 6,5 раз медленнее с включенной оптимизацией? для простого SSE2, который использует XMM зарегистрироваться. SSE2 является базовой для x86-64, поэтому вы всегда должны использовать его, когда он ускоряется, для вещей, которые стоит писать вручную в asm.
Re: ваш обновленный вопрос с ошибочным портом реализации из Почему rax и rdi работают одинаково в этой ситуации?
RDI и RBX оба держите указатели. Добавление их вместе не делает действительный адрес! В коде, который вы пытались портировать, RCX (индекс) инициализируется нулем до l oop. Но вместо xor %ebx, %ebx
вы сделали mov %rdi, %rbx
. Используйте отладчик для проверки значений регистра во время пошагового выполнения кода.