x86 32ビットマシンコード(32ビット整数):17バイト。
(32ビットまたは64ビットの16バイトを含む、DF = 1呼び出し規約を含む、以下の他のバージョンも参照してください。)
発信者はへのポインタを含むレジスタ内の引数渡し、エンド出力バッファの(のような私のCの答えを、正当化し、アルゴリズムの説明については、それを参照してください。) のglibcの内部_itoa
これを行い、それが単にコードゴルフのために考案していないので、。引数渡しレジスタは、EDXの代わりにEAXに引数があることを除いて、x86-64 System Vに近いです。
戻り時に、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は保存後に減少し、ポインターがループ終了時に1バイト低くなりすぎて、回避するために余分なバイトがかかります。)
バージョン:
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ビットに切り捨てないため、 64ビットクリーン関数でDF = 1を保持するのは不便です。。(静的コード/データへのポインターは、Linux上のx86-64非PIE実行可能ファイルでは32ビットであり、常にLinux x32 ABIであるため、32ビットポインターを使用するx86-64バージョンが使用できる場合があります。)この相互作用により、要件のさまざまな組み合わせを見ることができます。
- エントリー/出口呼び出し規約でDF = 0を使用したIA32:17B(
nostring
)。
- IA32:16B(DF = 1の規則:
stosb_edx_arg
またはskew
) ; または、着信DF = dontcareを使用して、16 + 1Bstosb_decode_overlap
または17Bを設定したままにしますstosb_edx_arg
- 64ビットポインターを使用するx86-64、およびエントリ/出口呼び出し規約のDF =
stosb_decode_overlap
0:17 + 1バイト()、18B(stosb_edx_arg
またはskew
)
64ビットポインターを備えたx86-64、その他のDF処理:16B(DF = 1 skew
)、17B(nostring
DF = 1で、scasb
代わりに使用dec
)。18B(stosb_edx_arg
3バイトでDF = 1を保持inc rdi
)。
または、文字列の1バイト前のポインターを返すことを許可する場合、15B(末尾stosb_edx_arg
なしinc
)。 すべてを再度呼び出して、別の文字列を異なるベース/テーブルのバッファに展開するように設定します... しかし、終端0
も保存しない場合、それはより理にかなっており、ループ内に関数本体を置くことができます。別の問題。
32ビット出力ポインターを備えたx86-64、DF = 0呼び出し規約:64ビット出力ポインターよりも改善はありませんが、18B(nostring
)になりました。
- 32ビット出力ポインターを備えたx86-64:最高の64ビットポインターバージョンと比べて改善がないため、16B(DF = 1
skew
)。または、DF = 1を設定してそのままにしておくには、17Bでskew
withでstd
、notではありません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
)の前後にALの残りを取得または削除します。
エントリ/出口規則の通常のDF = 0では、std
/ cld
/ scasb
バージョンは32および64ビットコードで18バイトであり、64ビットクリーンです(64ビット出力ポインターで動作します)。
入力引数は、テーブルのRBX(for 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バイト大きくなります。(また、LEAにREXプレフィックスがないルックアップテーブルでは、64ビットクリーンではありません。)
明示的な長さと 0ターミネータを持つLUT :16 + 1バイト(32ビット)
このバージョンはDF = 1に設定し、そのままにします。合計バイト数の一部として必要な追加のLUTバイトをカウントしています。
ここでのクールなトリックは、同じバイトで2つの異なる方法をデコードすることです。ループの真ん中に残り= baseとquotient =入力番号を入れて、0ターミネーターを所定の位置にコピーします。
関数を初めて使用すると、ループの最初の3バイトがLEAのdisp32の上位バイトとして消費されます。その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
ます。1バイトだけで4バイトを効果的に前方にジャンプし、フラグを壊します。(これは、L1iキャッシュ(BTW)の命令境界をマークするCPUのパフォーマンスにとってはひどいです。)
しかし、ここでは、レジスタをコピーしてループにジャンプするために3バイトを使用しています。通常は2 + 2(mov + jmp)を必要とし、XLATBの前ではなく、STOSの直前にループにジャンプできます。しかし、その後、別のSTDが必要になり、あまり面白くないでしょう。
オンラインでお試しください!(結果_start
で使用する呼び出し元とsys_write
)
デバッグを行うにstrace
は、出力の下で実行するか、出力を16進ダンプするのが最善です\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バイト前から固定長にダンプしているため、関数が想定されたバイトを書き込み、必要のないバイトを踏まなかったことがわかります。関数に渡される開始ポインターは最後から2番目のx
文字で、その後にゼロが続きました。)