32-бітний машинний код x86 (32-бітні цілі числа): 17 байт.
(також дивіться інші версії нижче, включаючи 16 байт для 32-розрядної або 64-бітової версії, з умовою виклику DF = 1.)
Caller передає аргументи в регістри, включаючи вказівник на кінець вихідного буфера (як моя відповідь C ; див. Це для обґрунтування та пояснення алгоритму.) Внутрішній glibc _itoa
робить це , тому це не просто придумано для коду-гольфу. Регістри передачі аргументу близькі до системи x86-64 System V, за винятком того, що у нас є arg в EAX замість EDX.
Після повернення EDI вказує на перший байт 0-закінченого рядка C у вихідному буфері. Звичайний реєстр зворотних значень - це EAX / RAX, але мовою складання ви можете використовувати будь-яку умову виклику, зручну для функції. ( xchg eax,edi
в кінці додасть 1 байт).
Абонент може вирахувати явну довжину, якщо хоче, від buffer_end - edi
. Але я не думаю, що ми можемо виправдати пропущення термінатора, якщо функція насправді не повертає початкові + кінцеві покажчики або вказівник + довжина. Це дозволило б заощадити 3 байти в цій версії, але я не думаю, що це виправдано.
- EAX = n = число для декодування. (Для
idiv
. Інші аргументи не є неявними операндами.)
- EDI = кінець вихідного буфера (64-розрядна версія все ще використовується
dec edi
, тому повинна бути низькою 4GiB)
- ESI / RSI = таблиця пошуку, відома також як LUT. не клобовані.
- ECX = довжина таблиці = основа. не клобовані.
nasm -felf32 ascii-compress-base.asm -l /dev/stdout | cut -b -30,$((30+10))-
(Відредагована вручну, щоб зменшити коментарі, нумерація рядків дивна.)
32-bit: 17 bytes ; 64-bit: 18 bytes
; same source assembles as 32 or 64-bit
3 %ifidn __OUTPUT_FORMAT__, elf32
5 %define rdi edi
6 address %define rsi esi
11 machine %endif
14 code %define DEF(funcname) funcname: global funcname
16 bytes
22 ;;; returns: pointer in RDI to the start of a 0-terminated string
24 ;;; clobbers:; EDX (tmp remainder)
25 DEF(ascii_compress_nostring)
27 00000000 C60700 mov BYTE [rdi], 0
28 .loop: ; do{
29 00000003 99 cdq ; 1 byte shorter than xor edx,edx / div
30 00000004 F7F9 idiv ecx ; edx=n%B eax=n/B
31
32 00000006 8A1416 mov dl, [rsi + rdx] ; dl = LUT[n%B]
33 00000009 4F dec edi ; --output ; 2B in x86-64
34 0000000A 8817 mov [rdi], dl ; *output = dl
35
36 0000000C 85C0 test eax,eax ; div/idiv don't write flags in practice, and the manual says they're undefined.
37 0000000E 75F3 jnz .loop ; }while(n);
38
39 00000010 C3 ret
0x11 bytes = 17
40 00000011 11 .size: db $ - .start
Дивно, що найпростіша версія з обмеженнями швидкості / розміру є найменшою, але std
/ cld
коштує 2 байти, яку слід використовувати stosb
в порядку зменшення та все-таки дотримуватися загальної схеми виклику DF = 0. (І STOS декременти після зберігання, залишаючи вказівник, що вказує один байт занадто низько на виході з циклу, коштуючи нам додаткових байтів для обходу.)
Версії:
Я придумав 4 суттєво різні хитрості реалізації (використання простого mov
завантаження / зберігання (вище), використання lea
/ movsb
(акуратного, але не оптимального), використання xchg
/ xlatb
/ stosb
/ xchg
, і того, хто входить у цикл із злому перекриття інструкцій. Дивіться код нижче) . Останній потребує контингенту 0
в таблиці пошуку, щоб скопіювати його як термінатор вихідного рядка, тому я рахую це як 1 байт. Залежно від 32/64-бітових (1-байт inc
чи ні) та чи можемо ми вважати, що абонент встановлює DF = 1 ( stosb
низхідний) чи будь-що інше, різні версії є (прив'язаними для) найкоротшими.
DF = 1 для зберігання у порядку зменшення робить виграш xchg / stosb / xchg, але абонент часто цього не хоче; Це здається, що розвантажувати роботу абоненту важко виправданим чином. (На відміну від користувальницьких регістрів передачі аргументів та зворотних значень, які, як правило, не коштують абонента ASM зайвої роботи.) Але в 64-бітовому коді cld
/ scasb
працює як inc rdi
, уникаючи обрізання вихідного вказівника на 32-бітний, тому іноді це незручно зберігати DF = 1 у 64-бітних чистих функціях. . (Покажчики статичного коду / даних є 32-бітними у x86-64 не-PIE-файлах у Linux та завжди в Linux x32 ABI, тому версія x86-64, що використовує 32-бітні покажчики, може використовуватися в деяких випадках.) У будь-якому випадку, ця взаємодія робить цікавим перегляд різних комбінацій вимог.
- IA32 з коефіцієнтом DF = 0 на умові виклику входу / виходу: 17B (
nostring
) .
- IA32: 16B (з умовою DF = 1:
stosb_edx_arg
або skew
) ; або з вхідним DF = dontcare, залишаючи його встановленим: 16 + 1Bstosb_decode_overlap
або 17Bstosb_edx_arg
- x86-64 з 64-бітовими вказівниками та DF = 0 на умові виклику входу / виходу: 17 + 1 байт (
stosb_decode_overlap
) , 18B ( stosb_edx_arg
або skew
)
x86-64 з 64-бітовими вказівниками, інша обробка DF: 16B (DF = 1 skew
) , 17B ( nostring
при DF = 1, використовуючи scasb
замість dec
). 18B ( stosb_edx_arg
збереження DF = 1 з 3-байтовим inc rdi
).
Або якщо ми дозволимо повернути вказівник на 1 байт перед рядком, 15B ( stosb_edx_arg
без inc
кінця). Все готово знову зателефонувати та розгорнути ще одну рядок у буфер з іншою базою / таблицею ... Але це матиме більше сенсу, якби ми також не зберігали закінчення 0
, і ви могли б помістити тіло функції всередину циклу, так що це справді окрема проблема.
x86-64 з 32-бітовим вихідним вказівником, DF = 0 умовою виклику: поліпшення порівняно з 64-бітовим вихідним вказівником, але 18B ( nostring
) пов'язано зараз.
- x86-64 з 32-бітовим вихідним вказівником: поліпшення в порівнянні з найкращими 64-бітовими версіями вказівника, так що 16B (DF = 1
skew
). Або встановити DF = 1 і залишити його, 17B для skew
з, std
але ні cld
. Або 17 + 1B для stosb_decode_overlap
з inc edi
в кінці замість cld
/ scasb
.
З DF = 1 умовою виклику: 16 байт (IA32 або x86-64)
Потрібен DF = 1 на вході, залишає його встановленим. Навряд чи правдоподібно , принаймні на основі функції. Це те ж саме, що і у наведеній версії, але з xchg, щоб отримати залишок в / з АЛ до / після XLATB (пошук таблиці з R / EBX в якості бази) та STOSB ( *output-- = al
).
З нормальним коефіцієнтомstd
cld
scasb
DF = 0 на умові входу / виходу, / / версія має 18 байт для 32 та 64-бітного коду та є 64-бітною чистою (працює з 64-бітовим вихідним вказівником).
Зауважте, що вхідні аргументи знаходяться в різних регістрах, включаючи RBX для таблиці (для xlatb
). Також зауважте, що цей цикл починається із збереження AL, і закінчується останнім символом, який ще не зберігався (звідси і mov
в кінці). Отже цикл "перекошений" відносно інших, звідси і назва.
;DF=1 version. Uncomment std/cld for DF=0
;32-bit and 64-bit: 16B
157 DEF(ascii_compress_skew)
158 ;;; inputs
159 ;; O in RDI = end of output buffer
160 ;; I in RBX = lookup table for xlatb
161 ;; n in EDX = number to decode
162 ;; B in ECX = length of table = modulus
163 ;;; returns: pointer in RDI to the start of a 0-terminated string
164 ;;; clobbers:; EDX=0, EAX=last char
165 .start:
166 ; std
167 00000060 31C0 xor eax,eax
168 .loop: ; do{
169 00000062 AA stosb
170 00000063 92 xchg eax, edx
171
172 00000064 99 cdq ; 1 byte shorter than xor edx,edx / div
173 00000065 F7F9 idiv ecx ; edx=n%B eax=n/B
174
175 00000067 92 xchg eax, edx ; eax=n%B edx=n/B
176 00000068 D7 xlatb ; al = byte [rbx + al]
177
178 00000069 85D2 test edx,edx
179 0000006B 75F5 jnz .loop ; }while(n = n/B);
180
181 0000006D 8807 mov [rdi], al ; stosb would move RDI away
182 ; cld
183 0000006F C3 ret
184 00000070 10 .size: db $ - .start
Аналогічна версія, яка не перекошується, затіняє EDI / RDI, а потім виправляє її.
; 32-bit DF=1: 16B 64-bit: 17B (or 18B for DF=0)
70 DEF(ascii_compress_stosb_edx_arg) ; x86-64 SysV arg passing, but returns in RDI
71 ;; O in RDI = end of output buffer
72 ;; I in RBX = lookup table for xlatb
73 ;; n in EDX = number to decode
74 ;; B in ECX = length of table
75 ;;; clobbers EAX,EDX, preserves DF
76 ; 32-bit mode: a DF=1 convention would save 2B (use inc edi instead of cld/scasb)
77 ; 32-bit mode: call-clobbered DF would save 1B (still need STD, but INC EDI saves 1)
79 .start:
80 00000040 31C0 xor eax,eax
81 ; std
82 00000042 AA stosb
83 .loop:
84 00000043 92 xchg eax, edx
85 00000044 99 cdq
86 00000045 F7F9 idiv ecx ; edx=n%B eax=n/B
87
88 00000047 92 xchg eax, edx ; eax=n%B edx=n/B
89 00000048 D7 xlatb ; al = byte [rbx + al]
90 00000049 AA stosb ; *output-- = al
91
92 0000004A 85D2 test edx,edx
93 0000004C 75F5 jnz .loop
94
95 0000004E 47 inc edi
96 ;; cld
97 ;; scasb ; rdi++
98 0000004F C3 ret
99 00000050 10 .size: db $ - .start
16 bytes for the 32-bit DF=1 version
Я спробував альтернативну версію цього з lea esi, [rbx+rdx]
/ movsb
як внутрішнього корпусу циклу. (RSI скидає кожну ітерацію, але зменшення RDI). Але він не може використовувати xor-zero / stos для термінатора, тому він на 1 байт більший. (І це не 64-бітний чистий для таблиці пошуку без префікса REX на LEA.)
LUT з явною довжиною та 0-термінатором: 16 + 1 байт (32-бітний)
Ця версія встановлює DF = 1 і залишає його таким чином. Я рахую додатковий потрібний байт LUT як частину загальної кількості байтів.
Класна хитрість тут - це те, що однакові байти декодують двома різними способами . Ми потрапляємо в середину циклу з залишком = база та коефіцієнт = вхідний номер, і копіюємо 0 термінатор на місце.
Уперше за допомогою функції перші 3 байти циклу споживаються як високі байти disp32 для LEA. Те, що LEA копіює базу (модуль) в EDX, idiv
видає залишок для пізніших ітерацій.
Другим байтом idiv ebp
є FD
, що є кодом для std
інструкції, яку ця функція потребує для роботи. (Це було щасливим відкриттям. Я розглядав це div
раніше, що відрізняє від idiv
використання /r
бітів в ModRM. Другий байт div epb
декодує як cmc
, що нешкідливо, але не допомагає. Але idiv ebp
ми можемо насправді видалити std
верхню частину функції.)
Зверніть увагу, що вхідні регістри знову є різницею: EBP для бази.
103 DEF(ascii_compress_stosb_decode_overlap)
104 ;;; inputs
105 ;; n in EAX = number to decode
106 ;; O in RDI = end of output buffer
107 ;; I in RBX = lookup table, 0-terminated. (first iter copies LUT[base] as output terminator)
108 ;; B in EBP = base = length of table
109 ;;; returns: pointer in RDI to the start of a 0-terminated string
110 ;;; clobbers: EDX (=0), EAX, DF
111 ;; Or a DF=1 convention allows idiv ecx (STC). Or we could put xchg after stos and not run IDIV's modRM
112 .start:
117 ;2nd byte of div ebx = repz. edx=repnz.
118 ; div ebp = cmc. ecx=int1 = icebp (hardware-debug trap)
119 ;2nd byte of idiv ebp = std = 0xfd. ecx=stc
125
126 ;lea edx, [dword 0 + ebp]
127 00000040 8D9500 db 0x8d, 0x95, 0 ; opcode, modrm, 0 for lea edx, [rbp+disp32]. low byte = 0 so DL = BPL+0 = base
128 ; skips xchg, cdq, and idiv.
129 ; decode starts with the 2nd byte of idiv ebp, which decodes as the STD we need
130 .loop:
131 00000043 92 xchg eax, edx
132 00000044 99 cdq
133 00000045 F7FD idiv ebp ; edx=n%B eax=n/B;
134 ;; on loop entry, 2nd byte of idiv ebp runs as STD. n in EAX, like after idiv. base in edx (fake remainder)
135
136 00000047 92 xchg eax, edx ; eax=n%B edx=n/B
137 00000048 D7 xlatb ; al = byte [rbx + al]
138 .do_stos:
139 00000049 AA stosb ; *output-- = al
140
141 0000004A 85D2 test edx,edx
142 0000004C 75F5 jnz .loop
143
144 %ifidn __OUTPUT_FORMAT__, elf32
145 0000004E 47 inc edi ; saves a byte in 32-bit. Makes DF call-clobbered instead of normal DF=0
146 %else
147 cld
148 scasb ; rdi++
149 %endif
150
151 0000004F C3 ret
152 00000050 10 .size: db $ - .start
153 00000051 01 db 1 ; +1 because we require an extra LUT byte
# 16+1 bytes for a 32-bit version.
# 17+1 bytes for a 64-bit version that ends with DF=0
Цей прийом декодування, що перекривається, також може бути використаний cmp eax, imm32
: для ефективного переходу вперед 4 байти потрібно лише 1 байт, лише прапори, що клобують. (Це жахливо для продуктивності на процесорах, які позначають межі інструкцій у кеші L1i, BTW.)
Але тут ми використовуємо 3 байти, щоб скопіювати регістр і перейти в цикл. Зазвичай це займе 2 + 2 (mov + jmp), і ми дозволимо нам стрибнути у цикл прямо перед STOS, а не перед XLATB. Але тоді нам знадобиться окрема ЗПСШ, і це було б не дуже цікаво.
Спробуйте в Інтернеті! (з _start
абонентом, який використовує sys_write
результат)
Найкраще для налагодження запустити його під strace
або hexdump виводу, так що ви можете переконатися, чи є \0
термінатор у потрібному місці тощо. Але ви можете бачити, що це справді працює і виробляти AAAAAACHOO
на вклад
num equ 698911
table: db "CHAO"
%endif
tablen equ $ - table
db 0 ; "terminator" needed by ascii_compress_stosb_decode_overlap
(Власне xxAAAAAACHOO\0x\0\0...
, тому, що ми скидаємо з 2-х байт раніше в буфер на фіксовану довжину. Отже, ми можемо побачити, що функція написала байти, які вона повинна була, і не наступила на жодні байти, яких вона не повинна мати. Стартовий вказівник, переданий функції, був другим x
символом, який супроводжувався нулями.)