Mã máy i386 (x86-32), 8 byte (9B đối với dấu không dấu)
+ 1B nếu chúng ta cần xử lý b = 0
đầu vào.
mã máy amd64 (x86-64), 9 byte (10B cho dấu không dấu hoặc 14B 13B cho số nguyên 64b được ký hoặc không dấu)
10 9B đối với dấu không dấu trên amd64, ngắt với đầu vào = 0
Đầu vào là các số nguyên có chữ ký 32 bit khác không eax
và ecx
. Đầu ra trong eax
.
## 32bit code, signed integers: eax, ecx
08048420 <gcd0>:
8048420: 99 cdq ; shorter than xor edx,edx
8048421: f7 f9 idiv ecx
8048423: 92 xchg edx,eax ; there's a one-byte encoding for xchg eax,r32. So this is shorter but slower than a mov
8048424: 91 xchg ecx,eax ; eax = divisor(from ecx), ecx = remainder(from edx), edx = quotient(from eax) which we discard
; loop entry point if we need to handle ecx = 0
8048425: 41 inc ecx ; saves 1B vs. test/jnz in 32bit mode
8048426: e2 f8 loop 8048420 <gcd0>
08048428 <gcd0_end>:
; 8B total
; result in eax: gcd(a,0) = a
Cấu trúc vòng lặp này không thành công trong trường hợp thử nghiệm ecx = 0
. ( div
gây ra sự thực thi #DE
phần cứng khi chia cho số 0. (Trên Linux, kernel cung cấp một SIGFPE
(ngoại lệ dấu phẩy động)). Nếu điểm vào vòng lặp ở ngay trước inc
, chúng tôi sẽ tránh được vấn đề. Phiên bản x86-64 có thể xử lý nó miễn phí, xem bên dưới.
Câu trả lời của Mike Shlanta là điểm khởi đầu cho việc này . Vòng lặp của tôi thực hiện tương tự như vòng lặp của anh ấy, nhưng đối với các số nguyên đã ký vì cdq
một vòng lặp ngắn hơn xor edx,edx
. Và có, nó hoạt động chính xác với một hoặc cả hai đầu vào âm. Phiên bản của Mike sẽ chạy nhanh hơn và chiếm ít không gian hơn trong bộ đệm uop ( xchg
là 3 uops trên CPU Intel và loop
rất chậm trên hầu hết các CPU ), nhưng phiên bản này chiến thắng ở kích thước mã máy.
Lúc đầu tôi không để ý rằng câu hỏi yêu cầu 32 bit không dấu . Quay trở lại xor edx,edx
thay vì cdq
sẽ tốn một byte. div
có cùng kích thước idiv
và mọi thứ khác có thể giữ nguyên ( xchg
để di chuyển dữ liệu và inc/loop
vẫn hoạt động.)
Điều thú vị là, đối với kích thước toán hạng 64 bit ( rax
và rcx
), các phiên bản đã ký và không dấu có cùng kích thước. Phiên bản đã ký cần tiền tố REX cho cqo
(2B), nhưng phiên bản chưa ký vẫn có thể sử dụng 2B xor edx,edx
.
Trong mã 64 bit, inc ecx
là 2B: byte đơn inc r32
và dec r32
opcodes được tái sử dụng làm tiền tố REX. inc/loop
không lưu bất kỳ kích thước mã nào trong chế độ 64 bit, vì vậy bạn cũng có thể test/jnz
. Hoạt động trên số nguyên 64 bit thêm một byte cho mỗi lệnh trong tiền tố REX, ngoại trừ loop
hoặc jnz
. Phần còn lại có thể có tất cả các số 0 ở mức thấp 32b (ví dụ gcd((2^32), (2^32 + 1))
), vì vậy chúng tôi cần kiểm tra toàn bộ RCx và không thể lưu một byte bằng test ecx,ecx
. Tuy nhiên, tốc độ chậm hơn jrcxz
chỉ là 2B và chúng ta có thể đặt nó ở đầu vòng lặp để xử lý ecx=0
mục nhập :
## 64bit code, unsigned 64 integers: rax, rcx
0000000000400630 <gcd_u64>:
400630: e3 0b jrcxz 40063d <gcd_u64_end> ; handles rcx=0 on input, and smaller than test rcx,rcx/jnz
400632: 31 d2 xor edx,edx ; same length as cqo
400634: 48 f7 f1 div rcx ; REX prefixes needed on three insns
400637: 48 92 xchg rdx,rax
400639: 48 91 xchg rcx,rax
40063b: eb f3 jmp 400630 <gcd_u64>
000000000040063d <gcd_u64_end>:
## 0xD = 13 bytes of code
## result in rax: gcd(a,0) = a
Chương trình thử nghiệm đầy đủ có thể chạy bao gồm cả main
chạy printf("...", gcd(atoi(argv[1]), atoi(argv[2])) );
nguồn và đầu ra asm trên Godbolt Compiler Explorer , cho các phiên bản 32 và 64b. Đã thử nghiệm và làm việc cho 32bit ( -m32
), 64bit ( -m64
) và x32 ABI ( -mx32
) .
Cũng bao gồm: một phiên bản chỉ sử dụng phép trừ lặp đi lặp lại , là 9B cho dấu không dấu, ngay cả đối với chế độ x86-64 và có thể lấy một trong các đầu vào của nó trong một thanh ghi tùy ý. Tuy nhiên, nó không thể xử lý đầu vào là 0 trên mục nhập (nó phát hiện khi sub
tạo ra số 0, mà x - 0 không bao giờ thực hiện).
GNU C nguồn asm nội tuyến cho phiên bản 32 bit (biên dịch với gcc -m32 -masm=intel
)
int gcd(int a, int b) {
asm (// ".intel_syntax noprefix\n"
// "jmp .Lentry%=\n" // Uncomment to handle div-by-zero, by entering the loop in the middle. Better: `jecxz / jmp` loop structure like the 64b version
".p2align 4\n" // align to make size-counting easier
"gcd0: cdq\n\t" // sign extend eax into edx:eax. One byte shorter than xor edx,edx
" idiv ecx\n"
" xchg eax, edx\n" // there's a one-byte encoding for xchg eax,r32. So this is shorter but slower than a mov
" xchg eax, ecx\n" // eax = divisor(ecx), ecx = remainder(edx), edx = garbage that we will clear later
".Lentry%=:\n"
" inc ecx\n" // saves 1B vs. test/jnz in 32bit mode, none in 64b mode
" loop gcd0\n"
"gcd0_end:\n"
: /* outputs */ "+a" (a), "+c"(b)
: /* inputs */ // given as read-write outputs
: /* clobbers */ "edx"
);
return a;
}
Thông thường tôi sẽ viết toàn bộ một hàm bằng asm, nhưng mã asm GNU C dường như là cách tốt nhất để bao gồm một đoạn mã có thể có / đầu ra trong bất kỳ chế độ nào chúng ta chọn. Như bạn có thể thấy, cú pháp asm nội tuyến của GNU C làm cho asm trở nên xấu xí và ồn ào. Đó cũng là một cách thực sự khó khăn để học asm .
Nó thực sự sẽ biên dịch và hoạt động trong .att_syntax noprefix
chế độ, bởi vì tất cả các insns được sử dụng là đơn / không có toán hạng hoặc xchg
. Không thực sự là một quan sát hữu ích.