Chức năng mã máy x86 32 bit, 21 byte
Chức năng mã máy x86-64, 22 byte
Việc tiết kiệm 1B ở chế độ 32 bit yêu cầu sử dụng separator = filler-1, vd fill=0
và sep=/
. Phiên bản 22 byte có thể sử dụng tùy chọn phân tách và phụ tùy ý.
Đây là phiên bản 21 byte, với input-separator = \n
(0xa), output-filler = 0
, output-separator = /
= filler-1. Các hằng số này có thể dễ dàng thay đổi.
; 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
Phiên bản 64 bit dài hơn 1 byte, sử dụng DEC 2 byte hoặc a mov al, output_separator
. Ngoài ra, mã máy giống nhau cho cả hai phiên bản, nhưng một số tên đăng ký thay đổi (ví dụ rcx
thay vì ecx
trong pop
).
Đầu ra mẫu từ khi chạy chương trình thử nghiệm (cơ sở 3):
$ ./string-exponential $'.\n..\n...\n....' $(seq 3);echo
000/000000000/000000000000000000000000000/000000000000000000000000000000000000000000000000000000000000000000000000000000000/
Thuật toán :
Lặp lại đầu vào, làm exp *= base
cho mọi char filler. Trên các dấu phân cách và byte bằng 0 kết thúc, nối các exp
byte của bộ đệm và sau đó là dấu phân cách vào chuỗi đầu ra và đặt lại thành exp=1
. Thật tiện lợi khi đầu vào được đảm bảo không kết thúc với cả dòng mới và bộ kết thúc.
Trên đầu vào, bất kỳ giá trị byte nào phía trên dấu phân cách (so sánh không dấu) được coi là bộ đệm và bất kỳ giá trị byte nào bên dưới dấu phân cách đều được coi là điểm đánh dấu cuối chuỗi. (Việc kiểm tra một cách rõ ràng cho một byte bằng 0 sẽ mất thêm test al,al
so với phân nhánh trên các cờ được đặt bởi vòng lặp bên trong).
Các quy tắc chỉ cho phép một dấu phân cách kéo dài khi đó là một dòng mới. Việc thực hiện của tôi luôn luôn nối thêm dải phân cách. Để có được mức tiết kiệm 1B ở chế độ 32 bit, quy tắc đó yêu cầu dấu phân cách = 0xa ( '\n'
ASCII LF = linefeed), filler = 0xb ( '\v'
ASCII VT = tab dọc). Điều đó không thân thiện với con người, nhưng đáp ứng thư pháp luật. (Bạn có thể hexdump hoặc
tr $'\v' x
đầu ra để xác minh rằng nó hoạt động hoặc thay đổi hằng số để phân tách đầu ra và filler có thể in được. Tôi cũng nhận thấy rằng các quy tắc dường như yêu cầu nó có thể chấp nhận đầu vào với cùng fill / sep mà nó sử dụng cho đầu ra , nhưng tôi không thấy gì để đạt được từ việc phá vỡ quy tắc đó.).
Nguồn NASM / YASM. Xây dựng dưới dạng mã 32 hoặc 64 bit, sử dụng %if
nội dung đi kèm với chương trình thử nghiệm hoặc chỉ cần thay đổi RCx thành 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
Hàm tuân theo x86-64 SystemV ABI, có chữ ký
void str_exp(char *out /*rdi*/, const char *src /*rsi*/, unsigned base /*edx*/);
Nó chỉ thông báo cho người gọi về độ dài của chuỗi đầu ra bằng cách để con trỏ một đầu quá khứ vào đó rdi
, vì vậy bạn có thể coi đây là giá trị trả về - quy ước gọi tiêu chuẩn.
Nó sẽ tốn 1 hoặc 2 byte ( xchg eax,edi
) để trả về con trỏ kết thúc bằng eax hoặc rax. (Nếu sử dụng x32 ABI, các con trỏ được đảm bảo chỉ có 32 bit, nếu không chúng ta phải sử dụng xchg rax,rdi
trong trường hợp người gọi chuyển một con trỏ đến bộ đệm bên ngoài 32 bit thấp.) Tôi đã không đưa nó vào phiên bản đăng bởi vì có cách giải quyết mà người gọi có thể sử dụng mà không nhận được giá trị từ đó rdi
, vì vậy bạn có thể gọi nó từ C mà không có trình bao bọc.
Chúng tôi thậm chí không chấm dứt chuỗi đầu ra hoặc bất cứ điều gì, vì vậy nó chỉ kết thúc dòng mới. Sẽ mất 2 byte để sửa lỗi đó: xchg eax,ecx / stosb
(RCx bằng 0 rep stosb
).
Các cách để tìm ra độ dài chuỗi đầu ra là:
- rdi trỏ đến một đầu quá khứ của chuỗi khi trả về (để người gọi có thể thực hiện len = end-start)
- người gọi có thể biết có bao nhiêu hàng trong đầu vào và đếm số dòng mới
- người gọi có thể sử dụng bộ đệm zeroed lớn và
strlen()
sau đó.
Chúng không đẹp hoặc hiệu quả (ngoại trừ việc sử dụng giá trị trả về RDI từ người gọi asm), nhưng nếu bạn muốn điều đó thì đừng gọi các hàm asm được đánh gôn từ C .: P
Giới hạn kích thước / phạm vi
Kích thước chuỗi đầu ra tối đa chỉ bị giới hạn bởi các giới hạn không gian địa chỉ bộ nhớ ảo. (Chủ yếu là phần cứng x86-64 hiện tại chỉ hỗ trợ 48 bit đáng kể trong các địa chỉ ảo, bị chia làm đôi vì chúng mở rộng thay vì mở rộng bằng không. Xem sơ đồ trong câu trả lời được liên kết .)
Mỗi hàng chỉ có thể có tối đa 2 ** 32 - 1 byte phụ, vì tôi tích lũy theo cấp số nhân trong một thanh ghi 32 bit.
Hàm hoạt động chính xác cho các cơ sở từ 0 đến 2 ** 32 - 1. (Đúng cho cơ sở 0 là 0 ^ x = 0, tức là chỉ các dòng trống không có byte phụ. Đúng cho cơ sở 1 là 1 ^ x = 1, vì vậy luôn luôn 1 filler trên mỗi dòng.)
Nó cũng cực kỳ nhanh trên Intel IvyBridge và sau đó, đặc biệt là đối với các hàng lớn được ghi vào bộ nhớ căn chỉnh. rep stosb
là một triển khai tối ưu memset()
cho số lượng lớn với các con trỏ được căn chỉnh trên CPU với tính năng ERMSB . ví dụ: 180 ** 4 là 0,97GB và mất 0,27 giây trên Skylake i7-6700k của tôi (với ~ 256k lỗi trang mềm) để ghi vào / dev / null. (Trên Linux, trình điều khiển thiết bị cho / dev / null không sao chép dữ liệu ở bất cứ đâu, nó chỉ trả về. Vì vậy, tất cả thời gian đều xảy ra rep stosb
và các lỗi trang mềm kích hoạt khi chạm vào bộ nhớ lần đầu tiên. tiếc là không sử dụng các vòng đệm trong suốt cho mảng trong BSS. Có lẽ một madvise()
cuộc gọi hệ thống sẽ tăng tốc nó.)
Chương trình kiểm tra :
Xây dựng một nhị phân tĩnh và chạy như ./string-exponential $'#\n##\n###' $(seq 2)
cho cơ sở 2. Để tránh thực hiện một atoi
, nó sử dụng base = argc-2
. (Giới hạn độ dài dòng lệnh ngăn việc kiểm tra các căn cứ lớn một cách lố bịch.)
Trình bao bọc này hoạt động cho các chuỗi đầu ra lên tới 1 GB. (Nó chỉ thực hiện một cuộc gọi hệ thống write () ngay cả đối với các chuỗi khổng lồ, nhưng Linux hỗ trợ điều này ngay cả khi ghi vào đường ống). Để đếm các ký tự, hoặc đặt ống vào wc -c
hoặc sử dụng strace ./foo ... > /dev/null
để xem arg tới tòa nhà viết.
Điều này tận dụng giá trị trả về RDI để tính toán độ dài chuỗi dưới dạng đối số 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.
Đây là một thử thách thú vị mà chính nó đã cho vay rất tốt, đặc biệt là các chuỗi op x86 . Các quy tắc được thiết kế độc đáo để tránh phải xử lý một dòng mới và sau đó là một bộ kết thúc ở cuối chuỗi đầu vào.
Một số mũ với phép nhân lặp đi lặp lại cũng giống như nhân với phép cộng lặp lại và tôi cần phải lặp để đếm số ký tự trong mỗi hàng đầu vào.
Tôi đã cân nhắc sử dụng một toán hạng mul
hoặc imul
thay vì dài hơn imul r,r
, nhưng việc sử dụng EAX ngầm của nó sẽ mâu thuẫn với LODSB.
Tôi cũng đã thử SCASB thay vì tải và so sánh , nhưng tôi cần xchg esi,edi
trước và sau vòng lặp bên trong, vì cả SCASB và STOSB đều sử dụng EDI. (Vì vậy, phiên bản 64 bit phải sử dụng x32 ABI để tránh cắt ngắn con trỏ 64 bit).
Tránh STOSB không phải là một lựa chọn; không có gì khác là bất cứ nơi nào gần như ngắn. Và một nửa lợi ích của việc sử dụng SCASB là AL = filler sau khi rời khỏi vòng lặp bên trong, vì vậy chúng tôi không cần bất kỳ thiết lập nào cho REP STOSB.
SCASB so sánh theo hướng khác với những gì tôi đã làm, vì vậy tôi cần phải đảo ngược các so sánh.
Nỗ lực tốt nhất của tôi với xchg và scasb. Hoạt động, nhưng không ngắn hơn. (Mã 32 bit, sử dụng inc
/ dec
trick để thay đổi filler thành dấu phân cách ).
; 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
Đối với một đầu vào ../.../.
, sản xuất ..../......../../
. Tôi sẽ không bận tâm đến việc hiển thị một hexdump của phiên bản với separator = newline.
"" <> "#"~Table~#
ngắn hơn 3 byte"#"~StringRepeat~#
, có thể chơi golf tốt hơn nữa.