x86 kode mesin 32-bit (bilangan bulat 32-bit): 17 byte.
(juga lihat versi lain di bawah ini, termasuk 16 byte untuk 32-bit atau 64-bit, dengan konvensi pemanggilan DF = 1.)
Caller meneruskan args dalam register, termasuk pointer ke akhir buffer output (seperti jawaban C saya ; lihat untuk pembenaran dan penjelasan algoritme.) Internal glibc _itoa
melakukan ini , jadi tidak hanya dibuat untuk kode-golf. Register arg-passing dekat dengan Sistem V86 x86-64, kecuali kami memiliki arg di EAX bukan EDX.
Saat dikembalikan, EDI menunjuk ke byte pertama dari string C yang diakhiri 0 dalam buffer output. Register nilai pengembalian yang biasa adalah EAX / RAX, tetapi dalam bahasa assembly Anda dapat menggunakan konvensi panggilan apa pun yang nyaman untuk suatu fungsi. ( xchg eax,edi
pada akhirnya akan menambah 1 byte).
Penelepon dapat menghitung panjang eksplisit jika diinginkan, dari buffer_end - edi
. Tapi saya tidak berpikir kita bisa membenarkan menghilangkan terminator kecuali fungsinya benar-benar mengembalikan pointer awal + akhir atau panjang pointer. Itu akan menghemat 3 byte dalam versi ini, tapi saya rasa itu tidak bisa dibenarkan.
- EAX = n = angka untuk memecahkan kode. (Untuk
idiv
. Arg lainnya tidak operan implisit.)
- EDI = akhir buffer output (versi 64-bit masih menggunakan
dec edi
, jadi harus dalam 4GiB rendah)
- ESI / RSI = tabel pencarian, alias LUT. tidak musnah.
- ECX = panjang tabel = basis. tidak musnah.
nasm -felf32 ascii-compress-base.asm -l /dev/stdout | cut -b -30,$((30+10))-
(Diedit tangan untuk mengecilkan komentar, penomoran baris itu aneh.)
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
Mengejutkan bahwa versi yang paling sederhana tanpa dasarnya pengorbanan kecepatan / ukuran adalah yang terkecil, tetapi std
/ cld
biaya 2 byte untuk digunakan stosb
dalam urutan menurun dan masih mengikuti konvensi pemanggilan DF = 0 yang umum. (Dan pengurangan STOS setelah penyimpanan, membiarkan pointer menunjuk satu byte terlalu rendah pada loop keluar, dikenakan biaya tambahan byte untuk bekerja.)
Versi:
Saya datang dengan 4 trik implementasi yang sangat berbeda (menggunakan mov
load / store sederhana (di atas), menggunakan lea
/ movsb
(rapi tapi tidak optimal), menggunakan xchg
/ xlatb
/ stosb
/ xchg
, dan yang memasuki loop dengan hack instruksi yang tumpang tindih. Lihat kode di bawah) . Yang terakhir membutuhkan trailing 0
di tabel pencarian untuk menyalin sebagai terminator string output, jadi saya menghitungnya sebagai +1 byte. Bergantung pada 32/64-bit (1-byte inc
atau tidak), dan apakah kita dapat mengasumsikan set pemanggil DF = 1 ( stosb
turun) atau apa pun, versi yang berbeda (terikat untuk) terpendek.
DF = 1 untuk disimpan dalam urutan menurun menjadikannya menang untuk xchg / stosb / xchg, tetapi pemanggil sering tidak menginginkannya; Rasanya seperti membongkar pekerjaan untuk penelepon dengan cara yang sulit dibenarkan. (Tidak seperti register arg-passing dan return-value kustom, yang biasanya tidak memerlukan biaya tambahan bagi pemanggil asm.) Namun dalam kode 64-bit, cld
/ scasb
berfungsi sebagai inc rdi
, menghindari pemotongan pointer output ke 32-bit, jadi terkadang tidak nyaman untuk mempertahankan DF = 1 dalam fungsi 64-bit-clean. . (Pointer ke kode / data statis 32-bit di x86-64 non-PIE dapat dieksekusi di Linux, dan selalu di Linux x32 ABI, jadi versi x86-64 menggunakan pointer 32-bit dapat digunakan dalam beberapa kasus.) Bagaimanapun, interaksi ini membuatnya menarik untuk melihat berbagai kombinasi persyaratan.
- IA32 dengan DF = 0 pada konvensi panggilan masuk / keluar: 17B (
nostring
) .
- IA32: 16B (dengan konvensi DF = 1:
stosb_edx_arg
atau skew
) ; atau dengan DF yang masuk = tidak peduli, biarkan set: 16 + 1Bstosb_decode_overlap
atau 17Bstosb_edx_arg
- x86-64 dengan pointer 64-bit, dan DF = 0 pada konvensi panggilan masuk / keluar: 17 + 1 byte (
stosb_decode_overlap
) , 18B ( stosb_edx_arg
atau skew
)
x86-64 dengan pointer 64-bit, penanganan DF lainnya: 16B (DF = 1 skew
) , 17B ( nostring
dengan DF = 1, menggunakan scasb
bukan dec
). 18B ( stosb_edx_arg
mempertahankan DF = 1 dengan 3-byte inc rdi
).
Atau jika kita membiarkan mengembalikan pointer ke 1 byte sebelum string, 15B ( stosb_edx_arg
tanpa inc
di akhir). Semua siap untuk memanggil lagi dan memperluas string lain ke buffer dengan basis / tabel yang berbeda ... Tapi itu akan lebih masuk akal jika kita tidak menyimpan terminating 0
juga, dan Anda mungkin meletakkan fungsi tubuh di dalam loop sehingga itu benar-benar sebuah masalah terpisah.
x86-64 dengan pointer output 32-bit, DF = 0 konvensi pemanggilan: tidak ada peningkatan lebih dari pointer output 64-bit, tetapi 18B ( nostring
) terikat sekarang.
- x86-64 dengan pointer output 32-bit: tidak ada peningkatan atas versi pointer 64-bit terbaik, jadi 16B (DF = 1
skew
). Atau untuk mengatur DF = 1 dan membiarkannya, 17B untuk skew
dengan std
tetapi tidak cld
. Atau 17 + 1B untuk stosb_decode_overlap
dengan inc edi
di akhir, bukan cld
/ scasb
.
Dengan konvensi pemanggilan DF = 1: 16 byte (IA32 atau x86-64)
Membutuhkan DF = 1 pada input, biarkan diatur. Hampir tidak masuk akal , setidaknya berdasarkan fungsi masing-masing. Apakah hal yang sama seperti versi di atas, tetapi dengan xchg untuk mendapatkan sisa / keluar dari AL sebelum / sesudah XLATB (pencarian tabel dengan R / EBX sebagai basis) dan STOSB ( *output-- = al
).
Dengan DF = 0 pada konvensi masuk / keluar yang normal, versi std
/ cld
/ scasb
adalah 18 byte untuk kode 32 dan 64-bit, dan 64-bit bersih (berfungsi dengan pointer output 64-bit).
Perhatikan bahwa input arg berada di register yang berbeda, termasuk RBX untuk tabel (untuk xlatb
). Perhatikan juga bahwa perulangan ini dimulai dengan menyimpan AL, dan diakhiri dengan karakter terakhir yang belum disimpan (karena itu mov
pada bagian akhir). Jadi loop "miring" relatif terhadap yang lain, maka namanya.
;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
Versi non-miring serupa melampaui EDI / RDI dan kemudian memperbaikinya.
; 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
Saya mencoba versi alternatif ini dengan lea esi, [rbx+rdx]
/ movsb
sebagai badan loop batin. (RSI diatur ulang setiap iterasi, tetapi RDI menurun). Tetapi tidak dapat menggunakan xor-zero / stos untuk terminator, jadi 1 byte lebih besar. (Dan itu tidak 64-bit bersih untuk tabel pencarian tanpa awalan REX pada LEA.)
LUT dengan panjang eksplisit dan 0 terminator: 16 + 1 byte (32-bit)
Versi ini menetapkan DF = 1 dan membiarkannya demikian. Saya menghitung byte LUT tambahan yang diperlukan sebagai bagian dari jumlah byte total.
Trik keren di sini adalah memiliki byte yang sama mendekode dua cara yang berbeda . Kami jatuh ke tengah-tengah loop dengan sisa = basis dan hasil bagi = nomor input, dan salin terminator 0 ke tempatnya.
Pada saat pertama kali melalui fungsi, 3 byte pertama dari loop dikonsumsi sebagai byte tinggi dari disp32 untuk LEA. LEA yang menyalin basis (modulus) ke EDX, idiv
menghasilkan sisanya untuk iterasi selanjutnya.
Byte ke-2 idiv ebp
adalah FD
, yang merupakan opcode untuk std
instruksi yang dibutuhkan fungsi ini. (Ini adalah penemuan yang beruntung. Saya telah melihat ini dengan yang div
sebelumnya, yang membedakan dirinya dari idiv
menggunakan /r
bit dalam ModRM. Byte ke-2 div epb
sebagai cmc
, yang tidak berbahaya tetapi tidak membantu. Tetapi dengan idiv ebp
dapatkah kita benar-benar menghapus std
dari atas fungsi.)
Perhatikan register input berbeda sekali lagi: EBP untuk basis.
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
Trik decode yang tumpang tindih ini juga dapat digunakan dengan cmp eax, imm32
: hanya dibutuhkan 1 byte untuk secara efektif melompat maju 4 byte, hanya flag clobbering. (Ini mengerikan untuk kinerja pada CPU yang menandai batas instruksi di L1i cache, BTW.)
Tapi di sini, kami menggunakan 3 byte untuk menyalin register dan melompat ke loop. Itu biasanya akan mengambil 2 + 2 (mov + jmp), dan akan membiarkan kita melompat ke loop tepat sebelum STOS daripada sebelum XLATB. Tetapi kemudian kita membutuhkan STD terpisah, dan itu tidak akan sangat menarik.
Cobalah online! (dengan _start
penelepon yang menggunakan sys_write
hasil)
Cara terbaik untuk debugging untuk menjalankannya di bawah strace
, atau hexdump output, sehingga Anda dapat melihat memverifikasi bahwa ada \0
terminator di tempat yang tepat dan seterusnya. Tetapi Anda dapat melihat ini benar-benar bekerja, dan menghasilkan AAAAAACHOO
input
num equ 698911
table: db "CHAO"
%endif
tablen equ $ - table
db 0 ; "terminator" needed by ascii_compress_stosb_decode_overlap
(Sebenarnya xxAAAAAACHOO\0x\0\0...
, karena kita membuang dari 2 byte sebelumnya di buffer ke panjang yang tetap. Jadi kita dapat melihat bahwa fungsi menulis byte yang seharusnya dan tidak menginjak byte yang seharusnya tidak ada. pointer-awal yang diteruskan ke fungsi adalah karakter ke-2 terakhir x
, yang diikuti oleh nol.)