32-бітна функція машинного коду x86, 21 байт
Функція машинного коду x86-64, 22 байти
Збереження 1B в 32-бітному режимі вимагає використання роздільника = filler-1, наприклад, fill=0
і sep=/
. У 22-байтовій версії можна використовувати довільний вибір сепаратора та наповнювача.
Це 21-байтова версія з входом-роздільником = \n
(0xa), вихід-наповнювач = 0
, вихід-роздільник = /
= наповнювач-1. Ці константи можна легко змінити.
; see the source for more comments
; RDI points to the output buffer, RSI points to the src string
; EDX holds the base
; This is the 32-bit version.
; The 64-bit version is the same, but the DEC is one byte longer (or we can just mov al,output_separator)
08048080 <str_exp>:
8048080: 6a 01 push 0x1
8048082: 59 pop ecx ; ecx = 1 = base**0
8048083: ac lods al,BYTE PTR ds:[esi] ; skip the first char so we don't do too many multiplies
; read an input row and accumulate base**n as we go.
08048084 <str_exp.read_bar>:
8048084: 0f af ca imul ecx,edx ; accumulate the exponential
8048087: ac lods al,BYTE PTR ds:[esi]
8048088: 3c 0a cmp al,0xa ; input_separator = newline
804808a: 77 f8 ja 8048084 <str_exp.read_bar>
; AL = separator or terminator
; flags = below (CF=1) or equal (ZF=1). Equal also implies CF=0 in this case.
; store the output row
804808c: b0 30 mov al,0x30 ; output_filler
804808e: f3 aa rep stos BYTE PTR es:[edi],al ; ecx bytes of filler
8048090: 48 dec eax ; mov al,output_separator
8048091: aa stos BYTE PTR es:[edi],al ;append delim
; CF still set from the inner loop, even after DEC clobbers the other flags
8048092: 73 ec jnc 8048080 <str_exp> ; new row if this is a separator, not terminator
8048094: c3 ret
08048095 <end_of_function>
; 0x95 - 0x80 = 0x15 = 21 bytes
64-бітна версія на 1 байт довша, використовуючи 2-байтний DEC або a mov al, output_separator
. Крім цього, машинний код однаковий для обох версій, але деякі назви регістрів змінюються (наприклад, rcx
замість ecx
в pop
).
Вибірка вибору з запуску програми тестування (база 3):
$ ./string-exponential $'.\n..\n...\n....' $(seq 3);echo
000/000000000/000000000000000000000000000/000000000000000000000000000000000000000000000000000000000000000000000000000000000/
Алгоритм :
Проведіть петлю над введенням, роблячи exp *= base
для кожного заповнювача. На роздільниках та кінцевому нульовому байті додайте exp
байти наповнювача, а потім роздільник до вихідного рядка та скиньте на exp=1
. Дуже зручно, що вхід гарантовано не закінчується як новою лінією, так і термінатором.
На вході будь-яке значення байта над роздільником (неподписане порівняння) трактується як наповнювач, а будь-яке байтне значення нижче роздільника трактується як маркер кінця рядка. (Явна перевірка нульового байта потребує додаткового test al,al
порівняння з розгалуженням на прапорах, встановлених внутрішнім циклом).
Правила дозволяють розділяти кінцевий роздільник лише тоді, коли це останній новий рядок. Моя реалізація завжди додає роздільник. Щоб отримати збереження 1B в 32-бітному режимі, для цього правила потрібен роздільник = 0xa ( '\n'
ASCII LF = linefeed), наповнювач = 0xb ( '\v'
ASCII VT = вертикальна вкладка). Це не дуже зручно для людини, але задовольняє букву закону. (Ви можете hexdump або
tr $'\v' x
висновок, щоб переконатися, що він працює, або змінити константу, щоб розділювач виводу і наповнювач були надруковані. Я також помітив, що правила, схоже, вимагають, щоб він міг приймати вхід з тим же заповненням / sep, який він використовує для виведення , але я не бачу нічого корисного від порушення цього правила.).
Джерело NASM / YASM Створіть як 32 або 64-бітний код, використовуючи %if
матеріали, що входять до програми тестування, або просто змініть rcx на ecx.
input_separator equ 0xa ; `\n` in NASM syntax, but YASM doesn't do C-style escapes
output_filler equ '0' ; For strict rules-compliance, needs to be input_separator+1
output_separator equ output_filler-1 ; saves 1B in 32-bit vs. an arbitrary choice
;; Using output_filler+1 is also possible, but isn't compatible with using the same filler and separator for input and output.
global str_exp
str_exp: ; void str_exp(char *out /*rdi*/, const char *src /*rsi*/,
; unsigned base /*edx*/);
.new_row:
push 1
pop rcx ; ecx=1 = base**0
lodsb ; Skip the first char, since we multiply for the separator
.read_bar:
imul ecx, edx ; accumulate the exponential
lodsb
cmp al, input_separator
ja .read_bar ; anything > separator is treated as filler
; AL = separator or terminator
; flags = below (CF=1) or equal (ZF=1). Equal also implies CF=0, since x-x doesn't produce carry.
mov al, output_filler
rep stosb ; append ecx bytes of filler to the output string
%if output_separator == output_filler-1
dec eax ; saves 1B in the 32-bit version. Use dec even in 64-bit for easier testing
%else
mov al, output_separator
%endif
stosb ; append the delimiter
; CF is still set from the .read_bar loop, even if DEC clobbered the other flags
; JNC/JNB here is equivalent to JE on the original flags, because we can only be here if the char was below-or-equal the separator
jnc .new_row ; separator means more rows, else it's a terminator
; (f+s)+f+ full-match guarantees that the input doesn't end with separator + terminator
ret
Функція слідує за системою x86-64 SystemV ABI, з підписом.
void str_exp(char *out /*rdi*/, const char *src /*rsi*/, unsigned base /*edx*/);
Вона інформує абонента лише про довжину вихідного рядка, залишаючи вказівник "один минулий кінець" в ньому rdi
, щоб ви могли вважати це значенням повернення в не -стандартний режим виклику.
xchg eax,edi
Повернення кінцевого вказівника в eax або rax коштуватиме 1 або 2 байти ( ). (Якщо використовується x32 ABI, покажчики гарантовано становлять лише 32 біти, інакше ми повинні використовуватись xchg rax,rdi
у випадку, якщо абонент передає вказівник до буфера поза низькими 32 бітами.) Я не включив це у версію I публікація, оскільки є обхідні шляхи rdi
, що може викликати абонент, не отримуючи значення , тому ви можете зателефонувати цьому з C без обгортки.
Ми навіть не скасовуємо нульовий рядок або щось подібне, тому це лише новий рядок. Для виправлення цього знадобиться 2 байти: xchg eax,ecx / stosb
(rcx дорівнює нулю від rep stosb
.)
Способи з'ясувати довжину вихідного рядка:
- rdi вказує на один-останній-кінець рядка при поверненні (тому абонент може зробити len = end-start)
- абонент може просто знати, скільки рядків було на вході, і рахувати нові рядки
- абонент може використовувати великий нульовий буфер і
strlen()
потім.
Вони не дуже чи ефективні (за винятком використання зворотного значення RDI від абонента asm), але якщо ви цього хочете, тоді не викликайте функцій asm з гольфу від C.: P
Обмеження розміру / діапазону
Максимальний розмір вихідного рядка обмежений лише обмеженнями адресного простору віртуальної пам'яті. (Головним чином, поточне обладнання x86-64 підтримує лише 48 значущих бітів у віртуальних адресах, розділених навпіл, оскільки вони підписують-розширюють замість нульового розширення. Див. Схему у зв’язаній відповіді .)
Кожен рядок може містити максимум 2 ** 32 - 1 байта наповнювача, оскільки я накопичую експоненцію в 32-бітному регістрі.
Функція працює правильно для баз від 0 до 2 ** 32 - 1. (Правильно для бази 0 0 0 x x = 0, тобто просто порожні рядки без байтів заповнення. Правильне для бази 1 - 1 ^ x = 1, тому завжди 1 наповнювач на рядок.)
Це також надзвичайно швидко в Intel IvyBridge і пізніше, особливо для великих рядків, записаних у вирівняну пам'ять. rep stosb
є оптимальною реалізацією memset()
для великих рахунків з вирівнюваними покажчиками на процесорах з функцією ERMSB . наприклад, 180 ** 4 становить 0,97 Гб, і на моєму i7-6700k Skylake (з ~ 256 кб м'яких сторінок-помилок) потрібно 0,27 секунди, щоб записати на / dev / null. (У Linux драйвер пристрою для / dev / null нікуди не копіює дані, він просто повертається. Отже, весь час знаходиться у rep stosb
та м'яких сторінках-помилках, які спрацьовують при першому дотику до пам'яті. Це на жаль, не використовувати прозорі величезні сторінки для масиву в BSS. Можливо, madvise()
системний виклик прискорить його.)
Тестова програма :
Побудуйте статичну двійкову і запустіть як ./string-exponential $'#\n##\n###' $(seq 2)
для базової 2. Щоб уникнути впровадження програми atoi
, вона використовує base = argc-2
. (Обмеження довжини командного рядка запобігають тестуванню смішно великих баз.)
Ця обгортка працює для вихідних рядків до 1 ГБ. (Це робить лише єдиний системний виклик write () навіть для гігантських рядків, але Linux підтримує це навіть для запису на труби). Для підрахунку символів або перейдіть до нього, wc -c
або використайте, strace ./foo ... > /dev/null
щоб побачити аргумент до системного виклику запису.
Для цього використовується корисне значення RDI для обчислення довжини рядка як аргументу write()
.
;;; Test program that calls it
;;; Assembles correctly for either x86-64 or i386, using the following %if stuff.
;;; This block of macro-stuff also lets us build the function itself as 32 or 64-bit with no source changes.
%ifidn __OUTPUT_FORMAT__, elf64
%define CPUMODE 64
%define STACKWIDTH 8 ; push / pop 8 bytes
%define PTRWIDTH 8
%elifidn __OUTPUT_FORMAT__, elfx32
%define CPUMODE 64
%define STACKWIDTH 8 ; push / pop 8 bytes
%define PTRWIDTH 4
%else
%define CPUMODE 32
%define STACKWIDTH 4 ; push / pop 4 bytes
%define PTRWIDTH 4
%define rcx ecx ; Use the 32-bit names everywhere, even in addressing modes and push/pop, for 32-bit code
%define rsi esi
%define rdi edi
%define rsp esp
%endif
global _start
_start:
mov rsi, [rsp+PTRWIDTH + PTRWIDTH*1] ; rsi = argv[1]
mov edx, [rsp] ; base = argc
sub edx, 2 ; base = argc-2 (so it's possible to test base=0 and base=1, and so ./foo $'xxx\nxx\nx' $(seq 2) has the actual base in the arg to seq)
mov edi, outbuf ; output buffer. static data is in the low 2G of address space, so 32-bit mov is fine. This part isn't golfed, though
call str_exp ; str_exp(outbuf, argv[1], argc-2)
; leaves RDI pointing to one-past-the-end of the string
mov esi, outbuf
mov edx, edi
sub edx, esi ; length = end - start
%if CPUMODE == 64 ; use the x86-64 ABI
mov edi, 1 ; fd=1 (stdout)
mov eax, 1 ; SYS_write (Linux x86-64 ABI, from /usr/include/asm/unistd_64.h)
syscall ; write(1, outbuf, length);
xor edi,edi
mov eax,231 ; exit_group(0)
syscall
%else ; Use the i386 32-bit ABI (with legacy int 0x80 instead of sysenter for convenience)
mov ebx, 1
mov eax, 4 ; SYS_write (Linux i386 ABI, from /usr/include/asm/unistd_32.h)
mov ecx, esi ; outbuf
; 3rd arg goes in edx for both ABIs, conveniently enough
int 0x80 ; write(1, outbuf, length)
xor ebx,ebx
mov eax, 1
int 0x80 ; 32-bit ABI _exit(0)
%endif
section .bss
align 2*1024*1024 ; hugepage alignment (32-bit uses 4M hugepages, but whatever)
outbuf: resb 1024*1024*1024 * 1
; 2GB of code+data is the limit for the default 64-bit code model.
; But with -m32, a 2GB bss doesn't get mapped, so we segfault. 1GB is plenty anyway.
Це було цікавим викликом, який дуже добре піддався підсилення, особливо x86 string ops . Правила добре розроблені, щоб уникнути необхідності обробляти новий рядок і потім термінатор в кінці вхідного рядка.
Експоненція з повторним множенням подібна до множення з повторним додаванням, і мені потрібно було циклічно рахувати символи у кожному вхідному рядку.
Я розглядав можливість використання одного операнда mul
або imul
замість більш тривалого imul r,r
, але його неявне використання EAX суперечить LODSB.
Я також спробував SCASB замість завантаження та порівняння , але мені було потрібно xchg esi,edi
до і після внутрішнього циклу, оскільки SCASB і STOSB обидва використовують EDI. (Отже, 64-розрядна версія повинна використовувати x32 ABI, щоб уникнути обрізання 64-бітних покажчиків).
Уникати STOSB - це не варіант; нічого іншого немає ніде поруч, як короткий. І половина переваги використання SCASB полягає в тому, що AL = наповнювач після виходу з внутрішнього циклу, тому нам не потрібні настройки для REP STOSB.
SCASB порівнює в іншому напрямку з тим, що я робив, тому мені потрібно було змінити порівняння.
Моя найкраща спроба з xchg та scasb. Працює, але не коротше. ( 32-розрядний код, використовуючи inc
/ dec
трюк, щоб змінити наповнювач на роздільник ).
; SCASB version, 24 bytes. Also experimenting with a different loop structure for the inner loop, but all these ideas are break-even at best
; Using separator = filler+1 instead of filler-1 was necessary to distinguish separator from terminator from just CF.
input_filler equ '.' ; bytes below this -> terminator. Bytes above this -> separator
output_filler equ input_filler ; implicit
output_separator equ input_filler+1 ; ('/') implicit
8048080: 89 d1 mov ecx,edx ; ecx=base**1
8048082: b0 2e mov al,0x2e ; input_filler= .
8048084: 87 fe xchg esi,edi
8048086: ae scas al,BYTE PTR es:[edi]
08048087 <str_exp.read_bar>:
8048087: ae scas al,BYTE PTR es:[edi]
8048088: 75 05 jne 804808f <str_exp.bar_end>
804808a: 0f af ca imul ecx,edx ; exit the loop before multiplying for non-filler
804808d: eb f8 jmp 8048087 <str_exp.read_bar> ; The other loop structure (ending with the conditional) would work with SCASB, too. Just showing this for variety.
0804808f <str_exp.bar_end>:
; flags = below if CF=1 (filler<separator), above if CF=0 (filler<terminator)
; (CF=0 is the AE condition, but we can't be here on equal)
; So CF is enough info to distinguish separator from terminator if we clobber ZF with INC
; AL = input_filler = output_filler
804808f: 87 fe xchg esi,edi
8048091: f3 aa rep stos BYTE PTR es:[edi],al
8048093: 40 inc eax ; output_separator
8048094: aa stos BYTE PTR es:[edi],al
8048095: 72 e9 jc 8048080 <str_exp> ; CF is still set from the inner loop
8048097: c3 ret
Для введення ../.../.
, виробляє ..../......../../
. Я не збираюся турбуватися, показуючи hexdump версії з роздільником = newline.
"" <> "#"~Table~#
на 3 байти коротше, що"#"~StringRepeat~#
, ймовірно, також можна зіграти.