x86 ฟังก์ชันโค้ดเครื่อง 32 บิต 21 ไบต์
ฟังก์ชันโค้ดเครื่อง x86-64, 22 ไบต์
ประหยัดในโหมด 32 บิต 1B ต้องใช้คั่น = ฟิลเลอร์-1 เช่นและfill=0
sep=/
เวอร์ชัน 22 ไบต์สามารถใช้ตัวเลือกตัวคั่นและฟิลเลอร์โดยพลการ
นี่คือรุ่น 21- ไบต์พร้อมอินพุต - ตัวคั่น = \n
(0xa), เอาท์พุทฟิลเลอร์ = 0
, เอาท์พุท - ตัวคั่น = /
= filler-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 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
สะดวกมากที่จะรับประกันว่าอินพุตจะไม่จบด้วยทั้ง newline และ terminator
ในอินพุตค่าไบต์ใด ๆ ที่อยู่เหนือตัวแยก (การเปรียบเทียบที่ไม่ได้ลงชื่อ) จะถือว่าเป็นฟิลเลอร์และค่าไบต์ใด ๆ ที่อยู่ด้านล่างตัวคั่นจะถือว่าเป็นตัวทำเครื่องหมายสิ้นสุดสตริง (การตรวจสอบอย่างชัดเจนสำหรับศูนย์ไบต์จะใช้การtest al,al
เปรียบเทียบกับการแตกแขนงบนแฟล็กที่กำหนดโดยลูปภายใน)
กฎอนุญาตเฉพาะตัวคั่นต่อท้ายเมื่อเป็นบรรทัดใหม่ต่อท้าย การใช้งานของฉันผนวกตัวคั่นเสมอ ในการรับการประหยัด 1B ในโหมด 32 บิตกฎนั้นต้องการตัวคั่น = 0xa ( '\n'
ASCII LF = linefeed), ฟิลเลอร์ = 0xb ( '\v'
แท็บ ASCII VT = แนวตั้ง) นั่นไม่ได้เป็นมิตรกับมนุษย์ แต่เป็นไปตามตัวอักษรของกฎหมาย (คุณสามารถ hexdump หรือ
tr $'\v' x
เอาท์พุทเพื่อตรวจสอบว่ามันทำงานหรือเปลี่ยนค่าคงที่ดังนั้นเอาท์พุทตัวคั่นและฟิลเลอร์ที่พิมพ์ได้ฉันยังสังเกตเห็นว่ากฎดูเหมือนจะต้องการให้มันสามารถรับอินพุตด้วยการเติม / sep เดียวกัน แต่ฉันไม่เห็นอะไรเลยที่จะได้รับจากการละเมิดกฎนั้น)
แหล่ง NASM / YASM สร้างเป็นรหัส 32 หรือ 64 บิตโดยใช้%if
สิ่งต่าง ๆ ที่รวมอยู่ในโปรแกรมทดสอบหรือเปลี่ยน 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
ดังนั้นคุณสามารถพิจารณาค่าตอบแทนในรูปแบบที่ไม่ใช่ แบบแผนการโทรมาตรฐาน
มันจะมีราคา 1 หรือ 2 ไบต์ ( xchg eax,edi
) เพื่อส่งคืนตัวชี้ปลายทางใน eax หรือ rax (ถ้าใช้ x32 ABI พอยน์เตอร์จะรับประกันได้เพียง 32 บิตมิฉะนั้นเราจะต้องใช้xchg rax,rdi
ในกรณีที่ผู้เรียกส่งตัวชี้ไปยังบัฟเฟอร์นอก 32 บิตต่ำ) ฉันไม่ได้รวมสิ่งนี้ไว้ในรุ่นที่ฉันใช้ การโพสต์เนื่องจากมีวิธีแก้ปัญหาที่ผู้โทรสามารถใช้ได้โดยไม่ได้รับค่าrdi
ดังนั้นคุณจึงสามารถเรียกสิ่งนี้ได้จาก C โดยไม่มี wrapper
เราไม่ได้ยกเลิก null output string หรืออะไรเลยดังนั้นมันจึงขึ้นบรรทัดใหม่เท่านั้น จะใช้เวลา 2 ไบต์เพื่อแก้ไขว่า: xchg eax,ecx / stosb
(rcx เป็นศูนย์จากrep stosb
)
วิธีในการหาความยาวของเอาต์พุต - สตริงคือ
- rdi ชี้ไปที่หนึ่งในอดีตของสตริงเมื่อส่งคืน (ดังนั้นผู้เรียกสามารถทำ len = end-start)
- ผู้โทรสามารถรู้ได้ว่ามีกี่แถวในอินพุตและนับบรรทัดใหม่
- ผู้เรียกสามารถใช้บัฟเฟอร์ศูนย์ขนาดใหญ่และ
strlen()
หลังจากนั้น
พวกมันไม่ได้สวยหรือมีประสิทธิภาพ (ยกเว้นการใช้ค่าส่งคืน RDI จาก asm caller) แต่ถ้าคุณต้องการที่จะไม่เรียกฟังก์ชั่น asm จาก golfed C. : P
ข้อ จำกัด ด้านขนาด / ช่วง
ขนาดสตริงเอาต์พุตสูงสุดถูก จำกัด โดยข้อ จำกัด พื้นที่ที่อยู่หน่วยความจำเสมือนเท่านั้น (ส่วนใหญ่ว่าฮาร์ดแวร์ x86-64 ปัจจุบันรองรับเฉพาะบิตที่สำคัญ 48 บิตในที่อยู่เสมือนแบ่งครึ่งเนื่องจากพวกเขาลงชื่อเข้าใช้ขยายแทนการขยายศูนย์ดูแผนภาพในคำตอบที่เชื่อมโยง )
แต่ละแถวสามารถมีขนาดฟิลเลอร์ได้สูงสุด 2 ** 32 - 1 เท่านั้นเนื่องจากฉันสะสมเลขชี้กำลังในการลงทะเบียนแบบ 32 บิต
ฟังก์ชั่นทำงานได้อย่างถูกต้องสำหรับฐานตั้งแต่ 0 ถึง 2 ** 32 - 1 (ถูกต้องสำหรับฐาน 0 คือ 0 ^ x = 0 นั่นคือเพียงบรรทัดว่างที่ไม่มีไบต์ฟิลเลอร์แก้ไขสำหรับฐาน 1 คือ 1 ^ x = 1 เสมอ 1 ฟิลเลอร์ต่อบรรทัด)
นอกจากนี้ยังรวดเร็วอย่างเห็นได้ชัดบน Intel IvyBridge และต่อมาโดยเฉพาะอย่างยิ่งสำหรับแถวขนาดใหญ่ที่ถูกเขียนไปยังหน่วยความจำที่จัดตำแหน่ง rep stosb
คือการดำเนินการที่ดีที่สุดของmemset()
สำหรับการนับจำนวนขนาดใหญ่ที่มีตัวชี้จัดตำแหน่งบนซีพียูกับคุณลักษณะ ERMSB เช่น 180 ** 4 คือ 0.97GB และใช้เวลา 0.27 วินาทีกับ i7-6700k Skylake ของฉัน (ด้วยความผิดพลาดหน้าซอฟท์เพจ ~ 256k) เพื่อเขียนถึง / dev / null (บน Linux ไดรเวอร์อุปกรณ์สำหรับ / dev / null ไม่ได้คัดลอกข้อมูลทุกที่มันจะกลับมาดังนั้นตลอดเวลาที่อยู่ในrep stosb
และข้อผิดพลาดหน้าอ่อนที่เรียกเมื่อสัมผัสหน่วยความจำเป็นครั้งแรกมัน น่าเสียดายที่ไม่ได้ใช้ hugepages แบบโปร่งใสสำหรับอาเรย์ใน BSS อาจเป็นไปได้madvise()
ว่าการเรียกของระบบจะเพิ่มความเร็วขึ้น)
โปรแกรมทดสอบ :
สร้างไบนารีแบบคงที่และการทำงานเป็น./string-exponential $'#\n##\n###' $(seq 2)
ฐาน 2. เพื่อหลีกเลี่ยงการดำเนินการจะใช้atoi
base = argc-2
(ขีดจำกัดความยาวบรรทัดคำสั่งป้องกันการทดสอบฐานขนาดใหญ่ที่น่าขัน)
wrapper นี้ใช้งานได้กับสตริงเอาต์พุตสูงสุด 1 GB (มันทำให้การเขียนเพียงครั้งเดียว () การเรียกระบบแม้สำหรับสตริงขนาดใหญ่ แต่ Linux ก็สนับสนุนสิ่งนี้แม้กระทั่งการเขียนไปยังไพพ์) สำหรับการนับอักขระให้ไพพ์เข้าwc -c
หรือใช้strace ./foo ... > /dev/null
เพื่อดู arg เพื่อเขียน 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 Ops กฎได้รับการออกแบบมาเป็นอย่างดีเพื่อหลีกเลี่ยงการจัดการกับการขึ้นบรรทัดใหม่แล้วตัวยุติที่จุดสิ้นสุดของอินพุตสตริง
เลขชี้กำลังที่มีการคูณซ้ำ ๆ ก็เหมือนกับการคูณด้วยการเพิ่มซ้ำและฉันต้องวนซ้ำเพื่อนับตัวอักษรในแต่ละแถวอินพุต
ฉันพิจารณาว่าใช้ตัวถูกดำเนินการหนึ่งตัวmul
หรือimul
นานกว่าimul r,r
นั้น แต่การใช้ EAX โดยนัยจะขัดแย้งกับ LODSB
ฉันยังลอง SCASB แทนการโหลดและเปรียบเทียบแต่ฉันต้องการxchg esi,edi
ก่อนและหลังลูปด้านในเนื่องจาก SCASB และ STOSB ทั้งคู่ใช้ EDI (ดังนั้นรุ่น 64 บิตต้องใช้ x32 ABI เพื่อหลีกเลี่ยงการตัดทอนพอยต์ 64 บิต)
การหลีกเลี่ยง 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
"" <> "#"~Table~#
สั้นกว่า 3 ไบต์"#"~StringRepeat~#
และอาจเล่นกอล์ฟได้อีกด้วย