Как пройти строку в сборке, пока я не достигну нуля? (стр ллен 1000 *) - PullRequest
3 голосов
/ 02 марта 2020

Прямо сейчас я просто выясняю, как пройти через строку. Если код не имеет смысла, это потому, что я неправильно интерпретировал некоторую информацию. В худшем случае, я действительно не знаю, что я делаю.

strlen:

pushq %rbx
movq %rsi, %rbx


loop:
    cmp $0x00, (%rdi, %rbx)
    je end
    inc %rbx
    jmp loop

end:
    movq %rbx, %rax
    popq %rbx
    ret

PS: есть причина, по которой мой заголовок выглядит как старик во второй раз на своем компьютере, пытаясь найти "как" to go to google.com "Superrrr noob здесь пытается узнать немного о сборке. Я пытаюсь реализовать функцию strlen для себя.

1 Ответ

3 голосов
/ 02 марта 2020

Вы просто 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. Используйте отладчик для проверки значений регистра во время пошагового выполнения кода.

...