32-разрядная функция машинного кода x86, 42 41 байт
В настоящее время самый короткий ответ на языке без игры в гольф, на 1В короче, чем у @ Streetter's q / kdb + .
С 0 для правдивых и ненулевых для ложных: 41 40 байт. (в общем случае сохраняет 1 байт для 32-разрядных, 2 байта для 64-разрядных).
Со строками неявной длины (в стиле C в стиле 0): 45 44 байта
Машинный код x86-64 (с 32-битными указателями, как x32 ABI): 44 43 байта .
x86-64 со строками неявной длины, по-прежнему 46 байтов (стратегия битовых карт shift / mask теперь безубыточна).
Это функция C подписи _Bool dennis_like(size_t ecx, const char *esi)
. Соглашение о вызовах немного нестандартно, близко к MS vectorcall / fastcall, но с другими регистрами arg: строка в ESI и длина в ECX. Это только забивает его arg-regs и EDX. AL содержит возвращаемое значение, а старшие байты содержат мусор (как это разрешено ABI SysV x86 и x32. IDK, что ABI MS говорят о высоком мусоре при возврате bool или узких целых чисел.)
Объяснение алгоритма :
Зацикливание входной строки, фильтрация и классификация в логический массив в стеке: для каждого байта проверьте, является ли он буквенным символом (если нет, переходите к следующему символу), и преобразуйте его в целое число от 0-25 (AZ) , Используйте это 0-25 целое число, чтобы проверить битовую карту vowel = 0 / consonant = 1. (Растровое изображение загружается в регистр как 32-битная непосредственная константа). Вставьте 0 или 0xFF в стек в соответствии с результатом растрового изображения (фактически в младшем байте 32-битного элемента, который может содержать мусор в верхних 3 байтах).
Первый цикл создает массив 0 или 0xFF (в элементах dword, заполненных мусором). Выполните обычную проверку палиндрома со вторым циклом, который останавливается, когда указатели пересекаются в середине (или когда они оба указывают на один и тот же элемент, если было нечетное количество буквенных символов). Восходящий указатель является указателем стека, и мы используем POP для загрузки + приращения. Вместо сравнения / setcc в этом цикле мы можем просто использовать XOR для определения того же / другого, поскольку существует только два возможных значения. Мы могли бы накапливать (с помощью ИЛИ), нашли ли мы какие-либо несоответствующие элементы, но ранняя ветвь на флагах, установленных XOR, по крайней мере, так же хороша.
Обратите внимание, что второй цикл использует byte
размер операнда, поэтому ему все равно, какой мусор первый цикл оставляет вне младшего байта каждого элемента массива.
Он использует недокументированную salc
инструкцию для установки AL из CF таким же образом, как это sbb al,al
было бы. Он поддерживается на любом процессоре Intel (кроме 64-битного режима), даже в Knight's Landing! Agner Fog перечисляет время для этого на всех процессорах AMD (включая Ryzen), поэтому, если производители x86 настаивают на том, чтобы связать этот байт пространства кода операции с 8086 года, мы могли бы также воспользоваться этим.
Интересные трюки:
- трюк сравнения без знака для комбинированных функций isalpha () и toupper (), а также нулевое расширение байта для заполнения eax, настроенное для:
- немедленное растровое изображение в регистре для
bt
, вдохновленное некоторыми хорошими выходными данными компилятора дляswitch
.
- Создание массива переменного размера в стеке с циклом push in. (Стандартно для asm, но не то, что вы можете сделать с C для строковой версии неявной длины). Он использует 4 байта стекового пространства для каждого входного символа, но экономит по крайней мере 1 байт против оптимальной игры в гольф
stosb
.
- Вместо cmp / setne в логическом массиве XOR объединяет логические значения для непосредственного получения значения истинности. (
cmp
/ salc
не вариант, потому что salc
работает только для CF, а 0xFF-0 не устанавливает CF. sete
составляет 3 байта, но позволит избежать inc
внешнего цикла, за чистую стоимость 2 байта (1 в 64-битном режиме )) против xor в цикле и исправления с помощью inc.
; explicit-length version: input string in ESI, byte count in ECX
08048060 <dennis_like>:
8048060: 55 push ebp
8048061: 89 e5 mov ebp,esp ; a stack frame lets us restore esp with LEAVE (1B)
8048063: ba ee be ef 03 mov edx,0x3efbeee ; consonant bitmap
08048068 <dennis_like.filter_loop>:
8048068: ac lods al,BYTE PTR ds:[esi]
8048069: 24 5f and al,0x5f ; uppercase
804806b: 2c 41 sub al,0x41 ; range-shift to 0..25
804806d: 3c 19 cmp al,0x19 ; reject non-letters
804806f: 77 05 ja 8048076 <dennis_like.non_alpha>
8048071: 0f a3 c2 bt edx,eax # AL = 0..25 = position in alphabet
8048074: d6 SALC ; set AL=0 or 0xFF from carry. Undocumented insn, but widely supported
8048075: 50 push eax
08048076 <dennis_like.non_alpha>:
8048076: e2 f0 loop 8048068 <dennis_like.filter_loop> # ecx = remaining string bytes
; end of first loop
8048078: 89 ee mov esi,ebp ; ebp = one-past-the-top of the bool array
0804807a <dennis_like.palindrome_loop>:
804807a: 58 pop eax ; read from the bottom
804807b: 83 ee 04 sub esi,0x4
804807e: 32 06 xor al,BYTE PTR [esi]
8048080: 75 04 jne 8048086 <dennis_like.non_palindrome>
8048082: 39 e6 cmp esi,esp ; until the pointers meet or cross in the middle
8048084: 77 f4 ja 804807a <dennis_like.palindrome_loop>
08048086 <dennis_like.non_palindrome>:
; jump or fall-through to here with al holding an inverted boolean
8048086: 40 inc eax
8048087: c9 leave
8048088: c3 ret
;; 0x89 - 0x60 = 41 bytes
Вероятно, это также один из самых быстрых ответов, поскольку ни один из вариантов игры в гольф не причиняет слишком большого вреда, по крайней мере, для строк длиной до нескольких тысяч символов, когда использование памяти в 4 раза не приводит к большим потерям кэша. (Это также может привести к тому, что ответы, в которых раньше не было строк, подобных Деннису, перед циклическим перебором по всем символам, имеют ранний выход.) Это salc
происходит медленнее, чем setcc
на многих процессорах (например, 3 мопа против 1 на Skylake), но проверка битовой карты с bt/salc
все еще быстрее, чем поиск строки или регулярное выражение. И нет никаких накладных расходов на запуск, поэтому это очень дешево для коротких строк.
Выполнение этого за один проход на лету означало бы повторение классификационного кода для направлений вверх и вниз. Это было бы быстрее, но с большим размером кода. (Конечно, если вы хотите быстро, вы можете сделать 16 или 32 символа одновременно с SSE2 или AVX2, все еще используя трюк сравнения, сдвигая диапазон к нижней части подписанного диапазона).
Тестовая программа (для ia32 или x32 Linux) для вызова этой функции с помощью аргумента cmdline и выхода из состояния = возвращаемое значение. strlen
реализация от int80h.org .
; build with the same %define macros as the source below (so this uses 32-bit regs in 32-bit mode)
global _start
_start:
;%define PTRSIZE 4 ; true for x32 and 32-bit mode.
mov esi, [rsp+4 + 4*1] ; esi = argv[1]
;mov rsi, [rsp+8 + 8*1] ; rsi = argv[1] ; For regular x86-64 (not x32)
%if IMPLICIT_LENGTH == 0
; strlen(esi)
mov rdi, rsi
mov rcx, -1
xor eax, eax
repne scasb ; rcx = -strlen - 2
not rcx
dec rcx
%endif
mov eax, 0xFFFFAEBB ; make sure the function works with garbage in EAX
call dennis_like
;; use the 32-bit ABI _exit syscall, even in x32 code for simplicity
mov ebx, eax
mov eax, 1
int 0x80 ; _exit( dennis_like(argv[1]) )
;; movzx edi, al ; actually mov edi,eax is fine here, too
;; mov eax,231 ; 64-bit ABI exit_group( same thing )
;; syscall
64-битная версия этой функции может использовать sbb eax,eax
, что составляет всего 2 байта вместо 3 для setc al
. Это также потребовало бы дополнительного байта для dec
или not
в конце (потому что только 32-битный имеет 1 байт inc / dec r32). Используя x32 ABI (32-битные указатели в длинном режиме), мы все еще можем избежать префиксов REX, даже если мы копируем и сравниваем указатели.
setc [rdi]
Можно записывать напрямую в память, но резервирование байтов стека в ECX стоит больше кода, чем экономит. (И нам нужно перемещаться по выходному массиву. [rdi+rcx]
Для режима адресации требуется один дополнительный байт, но на самом деле нам нужен счетчик, который не обновляется для отфильтрованных символов, поэтому он будет хуже, чем этот.)
Это источник YASM / NASM с %if
условными обозначениями . Он может быть построен с -felf32
(32-битным кодом) или -felfx32
(64-битным кодом с x32 ABI) и с неявной или явной длиной . Я протестировал все 4 версии. Смотрите этот ответ для скрипта для создания статического двоичного файла из источника NASM / YASM.
Чтобы протестировать 64-битную версию на машине без поддержки ABI x32, вы можете изменить регистры указателя на 64-битные. (Затем просто вычтите количество префиксов REX.W = 1 (0x48 байт) из числа. В этом случае 4 инструкции нуждаются в префиксах REX для работы с 64-битными регистрами). Или просто вызовите его с rsp
помощью указателя и в низком 4G адресного пространства.
%define IMPLICIT_LENGTH 0
; This source can be built as x32, or as plain old 32-bit mode
; x32 needs to push 64-bit regs, and using them in addressing modes avoids address-size prefixes
; 32-bit code needs to use the 32-bit names everywhere
;%if __BITS__ != 32 ; NASM-only
%ifidn __OUTPUT_FORMAT__, elfx32
%define CPUMODE 64
%define STACKWIDTH 8 ; push / pop 8 bytes
%else
%define CPUMODE 32
%define STACKWIDTH 4 ; push / pop 4 bytes
%define rax eax
%define rcx ecx
%define rsi esi
%define rdi edi
%define rbp ebp
%define rsp esp
%endif
; A regular x86-64 version needs 4 REX prefixes to handle 64-bit pointers
; I haven't cluttered the source with that, but I guess stuff like %define ebp rbp would do the trick.
;; Calling convention similar to SysV x32, or to MS vectorcall, but with different arg regs
;; _Bool dennis_like_implicit(const char *esi)
;; _Bool dennis_like_explicit(size_t ecx, const char *esi)
global dennis_like
dennis_like:
; We want to restore esp later, so make a stack frame for LEAVE
push rbp
mov ebp, esp ; enter 0,0 is 4 bytes. Only saves bytes if we had a fixed-size allocation to do.
; ZYXWVUTSRQPONMLKJIHGFEDCBA
mov edx, 11111011111011111011101110b ; consonant/vowel bitmap for use with bt
;;; assume that len >= 1
%if IMPLICIT_LENGTH
lodsb ; pipelining the loop is 1B shorter than jmp .non_alpha
.filter_loop:
%else
.filter_loop:
lodsb
%endif
and al, 0x7F ^ 0x20 ; force ASCII to uppercase.
sub al, 'A' ; range-shift to 'A' = 0
cmp al, 'Z'-'A' ; if al was less than 'A', it will be a large unsigned number
ja .non_alpha
;; AL = position in alphabet (0-25)
bt edx, eax ; 3B
%if CPUMODE == 32
salc ; 1B only sets AL = 0 or 0xFF. Not available in 64-bit mode
%else
sbb eax, eax ; 2B eax = 0 or -1, according to CF.
%endif
push rax
.non_alpha:
%if IMPLICIT_LENGTH
lodsb
test al,al
jnz .filter_loop
%else
loop .filter_loop
%endif
; al = potentially garbage if the last char was non-alpha
; esp = bottom of bool array
mov esi, ebp ; ebp = one-past-the-top of the bool array
.palindrome_loop:
pop rax
sub esi, STACKWIDTH
xor al, [rsi] ; al = (arr[up] != arr[--down]). 8-bit operand-size so flags are set from the non-garbage
jnz .non_palindrome
cmp esi, esp
ja .palindrome_loop
.non_palindrome: ; we jump here with al=1 if we found a difference, or drop out of the loop with al=0 for no diff
inc eax ;; AL transforms 0 -> 1 or 0xFF -> 0.
leave
ret ; return value in AL. high bytes of EAX are allowed to contain garbage.
Я смотрел на возни с DF (флаг направления, который контролирует lodsd
/ scasd
и так далее), но это просто не было победой. Обычные ABI требуют, чтобы DF очищался при входе и выходе из функции. Предполагая, что очищено при входе, но оставлено на выходе, это будет обманом, IMO. Было бы неплохо использовать LODSD / SCASD, чтобы избежать 3-х байтов sub esi, 4
, особенно в случае, когда нет высокого мусора.
Альтернативная растровая стратегия (для строк неявной длины x86-64)
Оказывается, это не спасает байты, потому что bt r32,r32
все еще работает с большим количеством мусора в битовом индексе. Это просто не задокументировано так, как shr
есть.
Вместо того bt / sbb
чтобы вводить бит в / из CF, используйте shift / mask, чтобы выделить нужный бит из битовой карты.
%if IMPLICIT_LENGTH && CPUMODE == 64
; incompatible with LOOP for explicit-length, both need ECX. In that case, bt/sbb is best
xchg eax, ecx
mov eax, 11111011111011111011101110b ; not hoisted out of the loop
shr eax, cl
and al, 1
%else
bt edx, eax
sbb eax, eax
%endif
push rax
Так как это производит 0/1 в AL в конце (вместо 0 / 0xFF), мы можем сделать необходимое инвертирование возвращаемого значения в конце функции с помощью xor al, 1
(2B) вместо dec eax
(также 2B в x86-64) для по-прежнему производят правильное bool
/_Bool
возвращаемое значение.
Это использовалось для сохранения 1B для x86-64 со строками неявной длины, избегая необходимости обнулять старшие байты EAX. (Я использовал and eax, 0x7F ^ 0x20
для преобразования в верхний регистр и обнуления оставшуюся часть eax с помощью 3-байтового кода and r32,imm8
. Но теперь я использую 2-байтовую кодировку немедленного с AL, которую имеет большинство инструкций 8086, как я уже делал для sub
и cmp
.)
Он проигрывает в bt
/ salc
в 32-битном режиме, и для строк с явной длиной требуется ECX для подсчета, поэтому это тоже не работает.
Но потом я понял, что был неправ: bt edx, eax
все еще работает с большим мусором в EAX. Это , по- видимому маскирует сдвиг рассчитывать так же , как shr r32, cl
делает ( если смотреть только на низких 5 битов ХЛ). Это отличается от того bt [mem], reg
, к которому можно обращаться за пределами памяти, на которую ссылается режим / размер адресации, рассматривая ее как цепочку битов. (Сумасшедший CISC ...)
Руководство Intel по установке insn set не документирует маскировку, поэтому, возможно, это недокументированное поведение, которое Intel сохраняет на данный момент. (Подобные вещи не редкость. bsf dst, src
С src = 0 всегда оставляет dst неизмененным, даже если в этом случае задокументировано, что dst содержит неопределенное значение. AMD фактически документирует поведение src = 0.) Я тестировал на Skylake и Core2, и bt
версия работает с ненулевым мусором в EAX за пределами AL.
Опрятный трюк здесь использует xchg eax,ecx
(1 байт), чтобы получить счетчик в CL. К сожалению, BMI2 shrx eax, edx, eax
составляет 5 байтов, по сравнению только с 2 байтами для shr eax, cl
. Для использования bextr
требуется 2 байта mov ah,1
(для количества битов для извлечения), так что это опять 5 + 2 байта, как SHRX + AND.
Исходный код стал довольно грязным после добавления %if
условных выражений . Вот разборка строк неявной длины x32 (с использованием альтернативной стратегии для растрового изображения, так что это все еще 46 байтов).
Основное отличие от версии с явной длиной в первом цикле. Обратите внимание на то, как есть lods
перед ним и внизу, вместо одного в верхней части цикла.
; 64-bit implicit-length version using the alternate bitmap strategy
00400060 <dennis_like>:
400060: 55 push rbp
400061: 89 e5 mov ebp,esp
400063: ac lods al,BYTE PTR ds:[rsi]
00400064 <dennis_like.filter_loop>:
400064: 24 5f and al,0x5f
400066: 2c 41 sub al,0x41
400068: 3c 19 cmp al,0x19
40006a: 77 0b ja 400077 <dennis_like.non_alpha>
40006c: 91 xchg ecx,eax
40006d: b8 ee be ef 03 mov eax,0x3efbeee ; inside the loop since SHR destroys it
400072: d3 e8 shr eax,cl
400074: 24 01 and al,0x1
400076: 50 push rax
00400077 <dennis_like.non_alpha>:
400077: ac lods al,BYTE PTR ds:[rsi]
400078: 84 c0 test al,al
40007a: 75 e8 jne 400064 <dennis_like.filter_loop>
40007c: 89 ee mov esi,ebp
0040007e <dennis_like.palindrome_loop>:
40007e: 58 pop rax
40007f: 83 ee 08 sub esi,0x8
400082: 32 06 xor al,BYTE PTR [rsi]
400084: 75 04 jne 40008a <dennis_like.non_palindrome>
400086: 39 e6 cmp esi,esp
400088: 77 f4 ja 40007e <dennis_like.palindrome_loop>
0040008a <dennis_like.non_palindrome>:
40008a: ff c8 dec eax ; invert the 0 / non-zero status of AL. xor al,1 works too, and produces a proper bool.
40008c: c9 leave
40008d: c3 ret
0x8e - 0x60 = 0x2e = 46 bytes