32-бітна функція машинного коду x86, 42 41 байт
Наразі найкоротша відповідь на мові, що не використовується для гольфу, на 1B коротша за q / kdb + @ streetter .
З 0 для правдивої і ненульової для помилки: 41 40 байт. (загалом, економить 1 байт для 32-бітових, 2 байти для 64-розрядних).
З рядками неявної довжини (C-стиль 0-завершено): 45 44 байти
машинний код x86-64 (з 32-бітовими вказівниками, як x32 ABI): 44 43 байт .
x86-64 з рядками неявної довжини, все ще 46 байтами (стратегія зсуву / маски растрової карти є беззбитковою).
Це функція C підпису _Bool dennis_like(size_t ecx, const char *esi)
. Конвенція виклику є дещо нестандартною, близькою до MS vectorcall / fastcall, але з різними аргументами аргументів: рядок у ESI та довжина в ECX. Він лише клобує свої аргументи та EDX. AL утримує повернене значення з високими байтами, що містять сміття (як це дозволяють ABI SysV x86 та x32.
Пояснення алгоритму :
Проведіть петлю над вхідним рядком, фільтруючи та класифікуючи булевий масив у стеку: для кожного байта перевірте, чи є він за алфавітом (якщо ні, перейдіть до наступного символу) та перетворіть його на ціле число від 0-25 (AZ) . Використовуйте це 0-25 ціле число, щоб перевірити растрові зображення голосних = 0 / приголосний = 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
.
- Створення масиву змінного розміру на стеку із натисканням на цикл. (Стандартний для asm, але не те, що ви можете зробити з C для версії рядка неявної довжини). Він використовує 4 байти місця для стеку для кожного вхідного символу, але економить принаймні 1 байт проти оптимального гольфу навколо
stosb
.
- Замість cmp / setne в бульовому масиві, XOR вводить булі разом, щоб безпосередньо отримати значення істини. (
cmp
/ salc
не є варіантом, тому що salc
працює лише для CF, а 0xFF-0 не встановлює CF. sete
3 байти, але це дозволить уникнути inc
зовнішньої петлі, чиста вартість 2 байти (1 у 64-бітному режимі) )) проти xor у циклі та фіксація його з укл.
; 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
Це, мабуть, також одна з найшвидших відповідей, оскільки жоден гольф насправді не шкодить занадто сильно, принаймні, для рядків розміром у декілька тисяч символів, де використання 4x пам’яті не спричиняє багато помилок кешу. (Можливо, це також втратить відповіді, які вимагають раннього відтворення рядків, не схожих на Денніса, перед тим, як переглядати всі символи.) salc
Повільніше, ніж setcc
для багатьох процесорів (наприклад, 3 Uops проти 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-бітну версію на машині без підтримки x32 ABI, ви можете змінити регістри вказівників на 64-бітні. (Тоді просто відніміть з числа кількість префіксів REX.W = 1 (0x48 байт). У цьому випадку для роботи на 64-бітних регістрах для 4 інструкцій потрібні префікси REX. Або просто зателефонуйте за 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
щоб потрапляти в / з МВ, використовуйте shift / маску, щоб ізолювати біт, який ми хочемо від растрової карти.
%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-AL, яке має більшість інструкцій 8086, як я вже робив для sub
і cmp
.)
Він втрачає bt
/ перебуває salc
в 32-бітному режимі, і рядки явної довжини потребують ECX для підрахунку, так що це також не працює.
Але потім я зрозумів, що помилявся: bt edx, eax
досі працює з високим сміттям в еаксі. Це, мабуть, маскує підрахунок зсуву так само, як shr r32, cl
це робить (дивлячись лише на низькі 5 біт кл). Це відрізняється від того bt [mem], reg
, до якого можна отримати доступ поза пам'яттю, на яку посилається режим / розмір адресації, трактуючи це як бітовий рядок. (Божевільний CISC ...)
Інструкція Intel щодо встановлених посилань не містить документації щодо маскування, тому, можливо, 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