x86 32 bit makine kodu işlevi, 21 bayt
x86-64 makine kodu fonksiyonu, 22 bayt
32-bit modunda tasarrufu 1B, örneğin ayırıcı = dolgu maddesi-1, kullanılmasını gerektirir fill=0
ve sep=/
. 22 baytlık sürüm isteğe bağlı bir ayırıcı ve doldurucu seçimi kullanabilir.
Bu, giriş ayırıcı = \n
(0xa), çıkış dolgu = 0
, çıkış ayırıcı = /
= dolgu-1 olan 21 baytlık versiyondur . Bu sabitler kolayca değiştirilebilir.
; 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 bit sürümü, 2 bayt DEC veya a kullanılarak 1 bayt daha uzundur mov al, output_separator
. Bunun dışında, makine kodu iki versiyonu için aynıdır, ancak bazı kayıt adları değişiklik (örneğin rcx
yerine ecx
de pop
).
Test programının çalıştırılmasından gelen örnek çıktı (temel 3)
$ ./string-exponential $'.\n..\n...\n....' $(seq 3);echo
000/000000000/000000000000000000000000000/000000000000000000000000000000000000000000000000000000000000000000000000000000000/
Algoritma :
exp *= base
Her dolgu maddesi için giriş yaparak giriş yapın . Sınırlayıcılarda ve sonlandırıcı sıfır baytta, exp
çıkış dizesine dolgu baytlarını ve ardından bir ayırıcı ekleyip sıfırlayın exp=1
. Bu girdi bir satır hem sonuna kadar garanti olduğunu çok uygun ve bir terminatör.
Girişte, ayırıcının üstündeki herhangi bir bayt değeri (işaretsiz karşılaştırma) dolgu maddesi olarak kabul edilir ve ayırıcının altındaki herhangi bir bayt değeri dizge sonu işaretçisi olarak değerlendirilir. (Sıfır bayt için açıkça kontrol etmek test al,al
, iç döngü tarafından ayarlanan bayraklarda dallanma vs.
Kurallar sadece takip eden bir yeni satır olduğunda takip eden bir ayırıcıya izin verir. Uygulamam her zaman ayırıcıyı ekler. 1B'nin 32 bit modda tasarrufunu sağlamak için, bu kural ayırıcı = 0xa ( '\n'
ASCII LF = satır besleme), dolgu = 0xb ( '\v'
ASCII VT = dikey sekme) gerektirir. Bu çok insan dostu değil, ancak kanunun mektubunu karşılar. (Çalışıp
tr $'\v' x
çalışmadığını doğrulamak için hexdump veya çıktısını alabilir ya da çıktı ayırıcı ve doldurucu yazdırılabilir olacak şekilde sabiti değiştirebilirsiniz. , ama bu kuralı çiğnemekten kazanacak bir şey göremiyorum.).
NASM / YASM kaynağı. %if
Test programına dahil olanları kullanarak 32 veya 64 bit kod olarak oluşturun veya sadece rcx'i ecx olarak değiştirin.
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
İşlev, imza ile birlikte x86-64 SystemV ABI'yi izler,
void str_exp(char *out /*rdi*/, const char *src /*rsi*/, unsigned base /*edx*/);
yalnızca arayan kişi için çıkış dizgesinin uzunluğunu bir son işaretçi bırakarak bildirir rdi
, böylece geri dönüş değerini olmayan bir değer olarak değerlendirebilirsiniz. -standart arama sözleşmesi.
Son xchg eax,edi
imleci eax veya rax olarak döndürmek 1 veya 2 byte'a ( ) mal olur. (X32 ABI kullanıyorsa, işaretçilerin yalnızca 32 bit olduğu garantilidir, aksi takdirde xchg rax,rdi
arayan kişinin düşük 32 bit dışındaki bir arabellek için bir işaretçi geçirmesi durumunda kullanmamız gerekir .) Bunu sürümüne dahil etmedim. Gönderen, arayanın değeri almadan kullanabileceği geçici çözümler olduğundan rdi
, bunu C 'yi sarmalayıcı olmadan da çağırabilirsin.
Çıkış dizgisini veya başka bir şeyi null sonlandırmıyoruz, bu yüzden sadece newline sonlandırıldı. Bunu düzeltmek 2 bayt alır: xchg eax,ecx / stosb
(rcx sıfırdan rep stosb
.)
Çıktı dizgisi uzunluğunu bulmanın yolları şunlardır:
- rdi, döndürülen dizenin sonundan birine işaret eder (böylece arayan kişi len = end-start yapabilir)
- arayan kişi girişte kaç satır olduğunu bilir ve yeni satırları sayar.
- arayan, büyük sıfırlanmış bir tampon kullanabilir ve
strlen()
daha sonra.
Güzel ya da verimli değiller (bir asm arayandan RDI dönüş değerini kullanmak hariç), ancak bunu yapmak istiyorsanız, golflü asm işlevlerini C'den çağırmayın: P
Boyut / aralık sınırlamaları
Maksimum çıktı dizesi boyutu yalnızca sanal bellek adres alanı sınırlamaları ile sınırlıdır. (Temel olarak mevcut x86-64 donanımı, sanal adreslerde yalnızca 48 önemli bit'i destekler , sıfır olarak uzatmak yerine işaret-uzadıklarından ikiye bölünürler. Bağlantılı cevaptaki şemaya bakın .)
Her bir satır en fazla 2 ** 32 - 1 dolgu baytına sahip olabilir, çünkü ben üsteli bir 32 bitlik kayıt defterinde biriktiriyorum.
İşlev 0 - 2 ** 32 - 1 arasındaki tabanlar için doğru çalışır (Temel 0 için 0 = x = 0, yani yalnızca dolgu baytı olmayan boş satırlar. Temel 1 için doğru 1 ^ x = 1, yani her zaman Satır başına 1 dolgu.)
Ayrıca Intel IvyBridge ve sonrasında cayır cayır yanan hızlı, özellikle de büyük satırlar hizalanmış belleğe yazılmış. ERMSB özelliğine sahip CPU'larda hizalı işaretçiler bulunan büyük sayımların rep stosb
optimum bir uygulamasıdırmemset()
. örn. 180 ** 4 0,97 GB'dir ve i7-6700k Skylake'imde (~ 256k yumuşak sayfa hatasıyla) yazmak / 0.25 saniye sürer / dev / null. (Linux'ta / dev / null aygıt sürücüsü verileri hiçbir yere kopyalamaz, yalnızca geri döner. Bu nedenle, her rep stosb
zaman belleğe ilk kez dokunduğunuzda tetikleyen yumuşak sayfa hatalarında bulunur. Maalesef BSS'deki dizi için şeffaf sarmallar kullanmamaya çalışıyorum. Muhtemelen bir madvise()
sistem çağrısı hızlandırabilir.)
Test programı :
Statik bir ikili yapı oluşturun ve ./string-exponential $'#\n##\n###' $(seq 2)
taban 2 için çalıştırın . Bir uygulanmasını önlemek atoi
için kullanır base = argc-2
. (Komut satırı uzunluğu sınırları gülünç büyük tabanların test edilmesini önler.)
Bu sarıcı, 1 GB'a kadar çıktı dizeleri için çalışır. (Sadece tek bir write () sistemini devasa karakter dizileri için bile çağırır, fakat Linux bunu borulara yazmak için bile destekler). Karakterleri saymak için, ya içine girin wc -c
ya strace ./foo ... > /dev/null
da yazma sistemindeki argümanı görmek için kullanın .
Bu, dize uzunluğunu bir argüman olarak hesaplamak için RDI dönüş değerinden yararlanır 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.
Bu, özellikle x86 string ops için kendini çok iyi ödünç veren eğlenceli bir mücadeleydi . Kurallar, yeni bir satırın ardından giriş dizesinin sonunda bir sonlandırıcı kullanmak zorunda kalmamak için güzel bir şekilde tasarlanmıştır.
Tekrarlanan çarpma ile bir üstel tekrarlanan toplama ile çarpma gibidir ve yine de her giriş satırındaki karakterleri saymak için döngü yapmam gerekiyordu.
Bir işlenen mul
veya imul
daha uzun süre imul r,r
kullanmak yerine , ancak EAX'in örtük kullanımı LODSB ile çakışacaktır.
Ayrıca yük ve karşılaştırma yerine SCASB'yi denedim , ancak xchg esi,edi
iç döngüden önce ve sonra gerekliydi , çünkü SCASB ve STOSB'de EDI kullanılıyor. (64-bit sürümünün 64-bit işaretçilerin kesilmesini önlemek için x32 ABI kullanması gerekir).
STOSB'den kaçınmak bir seçenek değildir; başka hiçbir şey kısa herhangi bir yere yakın değildir. Ve SCASB kullanmanın yararının yarısı, iç döngüyü terk ettikten sonra AL = dolgu maddesidir, bu nedenle REP STOSB için herhangi bir kurulum gerekmez.
SCASB, yaptığımdan farklı bir yönde karşılaştırıyor, bu yüzden karşılaştırmaları tersine çevirmem gerekiyordu.
Xchg ve scasb ile en iyi girişimim. Çalışır, ancak daha kısa değildir. ( Dolguyu ayırıcıya değiştirmek için inc
/ dec
trick'i kullanan 32 bit kod ).
; 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
Bir giriş için ../.../.
üretir ..../......../../
. Ayırıcı = newline olan sürümde bir hexdump göstererek uğraşmayacağım.
"" <> "#"~Table~#
3 byte daha kısadır"#"~StringRepeat~#
, muhtemelen daha ileride golf oynamaktadır.