Минимальный размер кода операции x86-64 strlen реализация

Я исследую реализацию минимального размера кода операции x86-64 strlen для моего исполняемого кода / исполняемого двоичного кода, размер которого не должен превышать некоторый размер (для простоты рассмотрим демосцену).
Общая идея исходит отсюда, идеи оптимизации размера здесь и здесь.

Адрес входной строки находится в rdi максимальная длина не должна превышать Int32

xor   eax,eax ; 2 bytes
or    ecx,-1  ; 3 bytes
repne scasb   ; 2 bytes
not   ecx     ; 2 bytes
dec   ecx     ; 2 bytes

Конечный результат в ecx всего 11 байтов.

Вопрос по настройке ecx в -1

Вариант 1 уже заявлен

or ecx,-1 ; 3 bytes

Вариант 2

lea ecx,[rax-1] ; 3 bytes 

Вариант 3

stc         ; 1 byte
sbb ecx,ecx ; 2 bytes

Вариант 4, вероятно, самый медленный

push -1 ; 2 bytes
pop rcx ; 1 byte

Я это понимаю:
Вариант 1 зависит от предыдущего ecx значение
Вариант 2 зависит от предыдущего rax значение
Вариант 3 Я не уверен, имеет ли он зависимость от предыдущего ecx значение?
Вариант 4 самый медленный?

Здесь есть явный победитель?
Критерий заключается в том, чтобы размер кода операции был как можно меньшим, и выбирайте наилучший вариант с точки зрения производительности.
Я полностью осознаю, что есть реализации, использующие современные инструкции процессора, но этот устаревший подход кажется самым маленьким.

2 ответа

Решение

Для взломанной достаточно хорошей версии мы знаем rdi имеет действительный адрес. Очень вероятно, что edi не маленькое целое число, таким образом, 2 байта mov ecx, edi, Убедитесь, что это безопасно для всех call-сайтов перед использованием!

Это здорово, если вы просто хотите, чтобы rdi указывал на окончание 0 байт, вместо того, чтобы фактически нуждаться в подсчете. Или, если у вас есть стартовый указатель в другом регистре, так что вы можете сделать sub edi, edx или что-то и получить длину таким образом, вместо обработки rcx результат. (Если вы знаете, что результат умещается в 32 бита, вам не нужно sub rdi, rdx потому что вы знаете, что старшие биты в любом случае будут равны нулю. И высокие входные биты не влияют на низкие выходные биты для add/sub; нести распространяется слева направо.)

Для строк размером менее 255 байт можно использовать mov cl, -1 (2 байта). Что делает rcx по крайней мере 0xFF, и выше, в зависимости от того, какой высокий мусор был оставлен в нем. (Это приводит к частичной остановке на Nehalem и более ранних версиях при чтении RCX, в противном случае это просто зависимость от старого RCX). Во всяком случае, тогда mov al, -2 / sub al, cl чтобы получить длину в виде 8-битного целого числа. Это может или не может быть полезным.

В зависимости от абонента, rcx возможно, уже содержал значение указателя, и в этом случае вы могли бы оставить его нетронутым, если вы можете использовать вычитание указателя.


Из предложенных вами вариантов

lea ecx,[rax-1] это очень хорошо, потому что вы просто обнулены eax и это дешевая инструкция на 1 моп с задержкой в ​​1 цикл, которая может выполняться на нескольких портах исполнения на всех основных процессорах.

Если у вас уже есть другой регистр с известным постоянным значением, особенно с нулевым значением, 3 байта lea почти всегда самый эффективный 3-байтовый способ создания константы, если она работает. (См. Установка всех битов в регистре ЦП на 1 эффективно).


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

Да, repne scasb очень компактный Его загрузка при запуске может составлять примерно 15 циклов на типичном процессоре Intel, и, согласно Agner Fog, он выдает>=6n uops с пропускной способностью>= 2n циклов, где n это количество (т. е. 2 цикла на байт, которое сравнивается для длинных сравнений, где скрыты накладные расходы при запуске), так что это уменьшает стоимость lea,

Что-то с ложной зависимостью от ecx может отложить запуск, так что вы определенно хотите lea,

repne scasb вероятно достаточно быстро для того, что вы делаете, но это медленнее, чем pcmpeqb / pmovmsbk / cmp, Для коротких строк фиксированной длины, целое число cmp / jne очень хорошо, когда длина составляет 4 или 8 байтов (включая завершающий 0), при условии, что вы можете безопасно перечитать свои строки, то есть вам не нужно беспокоиться о "" в конце страницы. Этот метод имеет накладные расходы, которые масштабируются с длиной строки. Например, для длины строки =7 вы можете сделать 4, 2 и 1 размера операнда, или вы можете сделать два сравнения dword, перекрывающихся на 1 байт. лайк cmp dword [rdi], first_4_bytes / jne; cmp dword [rdi+3], last_4_bytes / jne,


Подробнее о LEA

На процессоре семейства Sandybridge lea может быть отправлен в исполнительный блок в том же цикле, что и xor -зеро были введены в ядро ​​процессора вышедшего из строя. xor Обнуление обрабатывается на этапе выдачи / переименования, поэтому моп входит в ROB в "уже выполненном" состоянии. Для инструкции невозможно когда-либо ждать RAX. (Если не произойдет прерывание между XOR и lea, но даже тогда я думаю, что будет инструкция по сериализации после восстановления RAX и до lea мог выполнить, поэтому он не мог застрять в ожидании.)

просто lea может работать на port0 или port1 на SnB, или port1 / port5 на Skylake (2 на тактовую пропускную способность, но иногда разные порты на разных CPU семейства SnB). Это задержка 1 цикла, поэтому трудно сделать намного лучше.

Вряд ли вы увидите какое-либо ускорение от использования mov ecx, -1 (5 байт), который может работать на любом порту ALU.

На AMD Ризен, lea r32, [m] в 64-битном режиме рассматривается как "медленный" LEA, который может работать только на 2 портах, и имеет задержку 2 c вместо 1. Хуже того, Ryzen не устраняет обнуление xor.


Тест микробенчмарка, который вы провели, измеряет пропускную способность только для версий без ложных зависимостей, без задержки. Это часто полезная мера, и вы случайно получили правильный ответ, который lea это лучший выбор.

Вопрос о том, точно ли чистая пропускная способность отражает что-либо в вашем реальном случае, - это другой вопрос. На самом деле вы можете зависеть от задержки, а не от пропускной способности, если сравнение строк находится на критическом пути как часть длинной или переносимой циклом цепочки зависимостей данных, не нарушенной jcc чтобы дать вам предсказание ветвления + умозрительное исполнение. (Но код без ответвлений часто больше, поэтому это маловероятно).

stc / sbb ecx,ecx интересно, но лечат только процессоры AMD sbb как нарушение зависимости (только в зависимости от CF, а не целочисленного регистра). На Intel Haswell и ранее, sbb это инструкция 2 uop (потому что она имеет 3 входа: 2 целых числа GP + флаги). У него задержка 2с, поэтому он так плохо работает. (Задержка представляет собой депонированную цепочку.)


Укорочение других частей последовательности

В зависимости от того, что вы делаете, вы можете использовать strlen+2 точно так же, но смещая другую константу или что-то. dec ecx только 32 байта в коде, но x86-64 не имеет краткой формы inc/dec инструкции. Так что не / dec не так круто в 64-битном коде.

После repne scas, у тебя есть ecx = -len - 2 (если вы начали с ecx = -1), and не gives you -x-1 (i.e. + лен + 2 - 1`).

 ; eax = 0
 ; ecx = -1
repne scasb      ; ecx = -len - 2
sub   eax, ecx   ; eax = +len + 2

Я провел несколько тестов на Intel Core i7 4850HQ Haswell 2,3 ГГц, выпуск сборки не включал отладчик. В каждом цикле я измеряю 1000 последовательностей инструкций asm и повторяю их 10 миллионов раз, чтобы получить средний результат.

Я сделал макросы для повторения ассемблерных инструкций 100 раз.

#define lea100 asm{xor   eax,eax};asm { lea ecx,[rax-1] }; // <== Copy pasted 100times
#define or100 asm{xor   eax,eax};asm { or ecx,-1 }; // <== Copy pasted 100times
#define sbb100 asm{xor   eax,eax};asm { stc };asm{sbb ecx,ecx}; // <== Copy pasted 100times
#define stack100 asm ("xor %eax,%eax;.byte 0x6A; .byte 0xFF ;pop %rcx;"); // <== Copy pasted 100times

Тестирование кода C с помощью встроенного ассемблера для MacOS

#include <stdio.h>
#include <CoreServices/CoreServices.h>
#include <mach/mach.h>
#include <mach/mach_time.h>
int main(int argc, const char * argv[]) {
    uint64_t        start;
    uint64_t        end;
    uint64_t        elapsed;
    Nanoseconds     elapsedNano;

    uint64_t sum = 0;
    for (int i = 0; i < 10000000 ; i++) {

// this will become
// call       imp___stubs__mach_absolute_time  
// mov        r14, rax
    start = mach_absolute_time();

//10x lea100 for example for total 1000 

// call       imp___stubs__mach_absolute_time
// sub        rax, r14
    end = mach_absolute_time();

    elapsed = end - start;
    elapsedNano = AbsoluteToNanoseconds( *(AbsoluteTime *) &elapsed );
    uint64_t nano = * (uint64_t *) &elapsedNano;
        sum += nano;
    }
    printf("%f\n",sum/10000000.0);
    return 0;
}

Результаты

xor eax,eax
lea ecx,[rax-1]

205-216 нс

xor eax,eax
or ecx,-1

321-355 нс

xor eax,eax
push -1 
pop rcx 

322-359 нс

xor eax,eax
stc     
sbb ecx,ecx

612-692 нс

Другие вопросы по тегам