32-битная функция машинного кода x86, 21 байт
Функция машинного кода x86-64, 22 байта
Сохранение 1B в 32-битном режиме требует использования separator = 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
для каждого заполнителя char. На разделителях и завершающем нулевом байте добавьте exp
байты заполнителя, а затем разделитель к выходной строке и сбросьте на exp=1
. Очень удобно, что ввод гарантированно не заканчивается как новой строкой, так и разделителем.
На входе любое значение байта выше разделителя (сравнение без знака) обрабатывается как заполнитель, а любое значение байта ниже разделителя обрабатывается как маркер конца строки. (При явной проверке нулевого байта потребуется дополнительная test al,al
ветвь против ветвления для флагов, установленных внутренним циклом).
Правила допускают только конечный разделитель, когда это завершающий перевод строки. Моя реализация всегда добавляет разделитель. Для сохранения 1B в 32-битном режиме это правило требует разделитель = 0xa ( '\n'
ASCII LF = перевод строки), filler = 0xb ( '\v'
ASCII VT = вертикальная табуляция). Это не очень удобно для человека, но удовлетворяет букве закона. (Вы можете использовать hexdump или
tr $'\v' x
выходные данные, чтобы убедиться, что он работает, или изменить константу, чтобы разделитель и заполнитель выходных данных были пригодны для печати. Я также заметил, что правила, по-видимому, требуют, чтобы он мог принимать входные данные с той же заливкой / разделением, которую он использует для вывода. , но я не вижу никакой выгоды от нарушения этого правила.)
Источник 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-битных.) Я не включил это в версию, которую я отправка сообщений, потому что существуют обходные пути, которые может использовать вызывающая сторона без получения значения rdi
, поэтому вы можете вызывать ее из C без оболочки.
Мы даже не заканчиваем нулем выходную строку или что-то еще, так что это только символ новой строки. Чтобы исправить это, потребуется 2 байта: xchg eax,ecx / stosb
(rcx - ноль с rep stosb
.)
Способы определения длины выходной строки:
- rdi указывает на один конец строки при возврате (так что вызывающая сторона может сделать len = end-start)
- вызывающая сторона может просто знать, сколько строк было на входе, и считать новые строки.
- вызывающая сторона может использовать большой обнуленный буфер и
strlen()
впоследствии.
Они не симпатичны и не эффективны (за исключением использования возвращаемого значения RDI от вызывающей стороны asm), но если вы хотите, то не вызывайте asm-функции golfed из C.: P
Ограничения размера / диапазона
Максимальный размер выходной строки ограничен только адресным пространством виртуальной памяти. (Главным образом, текущее оборудование x86-64 поддерживает только 48 значащих бит в виртуальных адресах, разделенных пополам, потому что они расширяются по знаку вместо нулевого. См. Схему в связанном ответе .)
Каждая строка может иметь максимум 2 ** 32 - 1 байта-заполнителя, поскольку я накапливаю экспоненту в 32-битном регистре.
Функция работает правильно для базисов от 0 до 2 ** 32 - 1. (Правильно для базы 0 - 0 ^ x = 0, то есть просто пустые строки без байтов-заполнителей. Правильно для базы 1 - 1 ^ x = 1, поэтому всегда 1 наполнитель на строку.)
Это также невероятно быстро для Intel IvyBridge и более поздних версий, особенно для больших строк, записываемых в выровненную память. rep stosb
является оптимальной реализацией memset()
для больших подсчетов с выровненными указателями на процессорах с функцией ERMSB . например, 180 ** 4 равен 0,97 ГБ и занимает 0,27 секунды на моем i7-6700k Skylake (с ~ 256k мягких ошибок страниц) для записи в / 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
чтобы увидеть arg для системного вызова write.
При этом используется возвращаемое значение 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.
Это было забавное испытание, которое очень хорошо подействовало на asm, особенно на 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
trick для изменения заполнителя на разделитель ).
; 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~#
, вероятно, также пригоден для игры в гольф.