x86 32ビットマシンコード関数、21バイト
x86-64マシンコード関数、22バイト
32ビットモードで1Bを保存するには、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またはを使用し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
するには、内部ループで設定されたフラグで余分な分岐が必要になります)。
規則では、末尾の改行の場合にのみ末尾の区切り文字を使用できます。私の実装では、常にセパレータが追加されます。 32ビットモードで1Bを節約するには、このルールに区切り文字= 0xa('\n'
ASCII LF =改行)、フィラー= 0xb('\v'
ASCII VT =垂直タブ)が必要です。 それはあまり人間に優しいものではありませんが、法律の文字を満たします。(hexdumpまたは
tr $'\v' x
出力を使用して、動作を確認するか、出力セパレーターとフィラーが印刷可能になるように定数を変更できます。また、ルールは、出力に使用するのと同じfill / sepで入力を受け入れる必要があるように見えます) 、しかし、私はその規則を破ることから得られるものは見当たらない。)
NASM / YASMソース。%if
テストプログラムに含まれているものを使用して32または64ビットコードとしてビルドするか、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から呼び出すことができるためです。
出力文字列などをnullで終了することもしないので、改行のみで終了します。それを修正するには2バイトかかりますxchg eax,ecx / stosb
(rcxは0からrep stosb
です)。
出力文字列の長さを調べる方法は次のとおりです。
- rdiは、戻り時に文字列の末尾の1つを指します(したがって、呼び出し元はlen = end-startを実行できます)
- 呼び出し側は、入力に含まれる行数を知ることができ、改行をカウントできます。
- 呼び出し元は大きなゼロバッファーを使用でき、
strlen()
その後も使用できます。
それらはきれいでも効率的でもありません(asm呼び出し元からRDI戻り値を使用する場合を除く)が、それが必要な場合は、Cからゴルフasm関数を呼び出さないでください。
サイズ/範囲の制限
最大出力文字列サイズは、仮想メモリのアドレス空間の制限によってのみ制限されます。(主に現在のx86-64ハードウェアは仮想アドレスで48ビットのみをサポートし、ゼロ拡張ではなく符号拡張のため半分に分割されます。リンクされた回答の図を参照してください。)
32ビットのレジスタに指数を蓄積するため、各行には最大2 ** 32-1フィラーバイトしか含めることができません。
この関数は、0〜2 ** 32-1の基数に対して正しく機能します(基数0の修正は0 ^ x = 0、つまり、フィラーバイトのない空白行です。基数1の修正は1 ^ x = 1なので、常に1行に1つのフィラー。)
Intel IvyBridge以降では、特にアラインされたメモリに書き込まれる大きな行の場合、非常に高速です。 ERMSB機能を備えたCPU上に位置合わせされたポインターを持つ大数rep stosb
のmemset()
ための最適な実装です。たとえば、180 ** 4は0.97GBで、i7-6700k Skylake(〜256kのソフトページフォールト)で/ dev / nullに書き込むには0.27秒かかります。(Linuxでは、/ dev / nullのデバイスドライバーはデータをどこにもコピーせず、単に戻ります。そのため、すべての時間はrep stosb
、メモリに初めて触れるときにトリガーされるソフトページフォールトにあります。残念ながら、BSSの配列に透過的なhugepagesを使用していません。おそらく、madvise()
システムコールがそれを高速化するでしょう。
テストプログラム:
静的バイナリをビルドし、./string-exponential $'#\n##\n###' $(seq 2)
ベース2の場合と同じように実行atoi
しますbase = argc-2
。(コマンドラインの長さの制限により、途方もなく大きなベースのテストができなくなります。)
このラッパーは、最大1 GBの出力文字列に対して機能します。(巨大な文字列に対しても単一のwrite()システムコールのみを行いますが、Linuxはパイプへの書き込みに対してもこれをサポートしています)。文字をカウントwc -c
するstrace ./foo ... > /dev/null
には、パイプを使用するか、書き込みsyscallの引数を確認するために使用します。
これは、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に非常に適した楽しい挑戦でした。ルールは、入力文字列の最後で改行とターミネータを処理する必要がないようにうまく設計されています。
繰り返される乗算を伴う指数関数は、繰り返される加算を伴う乗算に似ています。とにかく各入力行の文字をカウントするためにループする必要がありました。
私が使用して考えられて1をオペランドmul
またはimul
代わりに長いのimul r,r
が、EAXの暗黙的な使用はLODSBと競合します。
loadとcompareの代わりにSCASBも試しましたがxchg esi,edi
、SCASBとSTOSBの両方がEDIを使用するため、内部ループの前後に必要でした。(したがって、64ビットバージョンでは、64ビットポインターの切り捨てを回避するためにx32 ABIを使用する必要があります)。
STOSBを避けることはオプションではありません。これほど短い場所は他にありません。また、SCASBを使用する利点の半分は、内側のループを抜けた後にAL = fillerであるため、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
の入力に対して../.../.
、を生成し..../......../../
ます。separator = newlineを使用したバージョンの16進ダンプを表示することはありません。
"" <> "#"~Table~#
は"#"~StringRepeat~#
、よりも3バイト短く、おそらくさらにゴルフ可能です。