32-разрядный машинный код x86 (32-разрядные целые числа): 17 байт.
(также см. другие версии ниже, включая 16 байтов для 32-битной или 64-битной версии с соглашением о вызовах DF = 1.)
Вызывающая сторона передает аргументы в регистрах, включая указатель на конец выходного буфера (как мой ответ C ; см. Его_itoa
для обоснования и объяснения алгоритма.) Это делает внутренняя часть glibc , так что это не просто придумано для code-golf. Регистры передачи аргументов близки к x86-64 System V, за исключением того, что у нас есть аргумент в EAX вместо EDX.
По возвращении EDI указывает на первый байт строки C с нулем в конце в выходном буфере. Обычный регистр возвращаемых значений - EAX / RAX, но на языке ассемблера вы можете использовать любое удобное для вызова соглашение о вызовах. ( xchg eax,edi
в конце добавил бы 1 байт).
Вызывающий может вычислить явную длину, если он хочет, из buffer_end - edi
. Но я не думаю, что мы можем оправдать исключение терминатора, если функция не возвращает оба указателя start + end или указатель + length. Это сэкономило бы 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-разрядным выходным указателем нет, но nostring
теперь 18B ( ) связывается.
- 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 для получения остатка в / из AL до / после XLATB (поиск таблицы с R / EBX в качестве базы) и STOSB ( *output-- = al
).
При нормальном DF = 0 на входной / выходной конвенции, / / версия 18 байт для 32 и 64-битного кода, и является 64-битным (работает с выходным указателем 64-битной).std
cld
scasb
Обратите внимание, что входные аргументы находятся в разных регистрах, включая 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
производит остаток для последующих итераций.
2-й байт idiv ebp
- FD
это код операции для std
инструкции, необходимой для работы этой функции. (Это было счастливое открытие. Я смотрел на это div
ранее, что отличается от idiv
использования /r
битов в ModRM. 2-й байт 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, кстати).
Но здесь мы используем 3 байта, чтобы скопировать регистр и перейти в цикл. Обычно это занимает 2 + 2 (mov + jmp) и позволяет нам перейти в цикл прямо перед STOS, а не перед XLATB. Но тогда нам понадобится отдельная ЗППП, и это будет не очень интересно.
Попробуйте онлайн! (с _start
вызывающим абонентом, который использует sys_write
результат)
Лучше всего для отладки запускать его strace
или выводить в шестнадцатеричном формате, чтобы вы могли убедиться, что \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
символом, за которым следовали нули.)