Hội x86, 9 byte (cho mục nhập cạnh tranh)
Tất cả mọi người đang thực hiện thử thách này bằng các ngôn ngữ cấp cao đều bỏ lỡ niềm vui thực sự của việc thao túng các bit thô. Có rất nhiều biến thể tinh tế trong cách thực hiện điều này, đó là sự điên rồ và rất nhiều điều thú vị để suy nghĩ. Dưới đây là một vài giải pháp mà tôi đã nghĩ ra trong ngôn ngữ lắp ráp x86 32 bit.
Tôi xin lỗi trước rằng đây không phải là câu trả lời golf-code điển hình. Tôi sẽ lan man rất nhiều về quá trình suy nghĩ tối ưu hóa lặp (cho kích thước). Hy vọng rằng điều đó thú vị và mang tính giáo dục cho đối tượng lớn hơn, nhưng nếu bạn là loại TL; DR, tôi sẽ không bị xúc phạm nếu bạn bỏ qua đến cuối.
Giải pháp rõ ràng và hiệu quả là kiểm tra xem giá trị là số lẻ hay số chẵn (có thể được thực hiện hiệu quả bằng cách xem xét bit có ý nghĩa nhỏ nhất), sau đó chọn giữa n + 1 hoặc n 1 tương ứng. Giả sử rằng đầu vào được truyền dưới dạng tham số trong thanh ECX
ghi và kết quả được trả về trong thanh EAX
ghi, chúng ta có được hàm sau:
F6 C1 01 | test cl, 1 ; test last bit to see if odd or even
8D 41 01 | lea eax, DWORD PTR [ecx + 1] ; set EAX to n+1 (without clobbering flags)
8D 49 FF | lea ecx, DWORD PTR [ecx - 1] ; set ECX to n-1 (without clobbering flags)
0F 44 C1 | cmovz eax, ecx ; move in different result if input was even
C3 | ret
(13 byte)
Nhưng đối với mục đích chơi gôn mã, những LEA
hướng dẫn đó không tuyệt vời, vì chúng mất 3 byte để mã hóa. Một đơn giản DEC
Re-ment của ECX
sẽ ngắn hơn nhiều (chỉ có một byte), nhưng điều này ảnh hưởng đến cờ, vì vậy chúng ta phải có một chút khéo léo trong cách chúng tôi sắp xếp các mã. Chúng ta có thể thực hiện giảm dần trước và thử nghiệm lẻ / chẵn thứ hai , nhưng sau đó chúng ta phải đảo ngược kết quả của thử nghiệm lẻ / chẵn.
Ngoài ra, chúng ta có thể thay đổi lệnh di chuyển có điều kiện sang một nhánh, điều này có thể làm cho mã chạy chậm hơn (tùy thuộc vào mức độ dự đoán của nhánh đó nếu đầu vào thay thế không nhất quán giữa lẻ và chẵn, một nhánh sẽ chậm hơn; mẫu, nó sẽ nhanh hơn), sẽ tiết kiệm cho chúng ta một byte khác.
Trong thực tế, với sửa đổi này, toàn bộ hoạt động có thể được thực hiện tại chỗ, chỉ sử dụng một thanh ghi duy nhất. Điều này thật tuyệt nếu bạn đặt nội dung mã này ở đâu đó (và rất có thể, bạn sẽ như vậy, vì nó quá ngắn).
48 | dec eax ; decrement first
A8 01 | test al, 1 ; test last bit to see if odd or even
75 02 | jnz InputWasEven ; (decrement means test result is inverted)
40 | inc eax ; undo the decrement...
40 | inc eax ; ...and add 1
InputWasEven: ; (two 1-byte INCs are shorter than one 3-byte ADD with 2)
(nội tuyến: 7 byte; dưới dạng hàm: 10 byte)
Nhưng nếu bạn muốn làm cho nó một chức năng thì sao? Không có quy ước gọi tiêu chuẩn nào sử dụng cùng một thanh ghi để truyền các tham số giống như cho giá trị trả về, vì vậy bạn cần thêm một MOV
lệnh đăng ký đăng ký vào đầu hoặc cuối của hàm. Điều này hầu như không có chi phí về tốc độ, nhưng nó có thêm 2 byte. ( RET
Hướng dẫn cũng thêm một byte và có một số chi phí được đưa ra bởi nhu cầu thực hiện và trả về từ một lệnh gọi hàm, có nghĩa đây là một ví dụ trong đó nội tuyến tạo ra cả lợi ích về tốc độ và kích thước, thay vì chỉ là tốc độ cổ điển -cho không gian trao đổi.) Trong tất cả, được viết dưới dạng hàm, mã này nở ra 10 byte.
Chúng ta có thể làm gì khác trong 10 byte? Nếu chúng ta quan tâm đến tất cả về hiệu suất (ít nhất là hiệu suất có thể dự đoán được ), sẽ rất tốt nếu thoát khỏi nhánh đó. Đây là một giải pháp xoay vòng không phân nhánh, có cùng kích thước tính theo byte. Tiền đề cơ bản rất đơn giản: chúng tôi sử dụng XOR bitwise để lật bit cuối cùng, chuyển đổi một giá trị lẻ thành một số chẵn và ngược lại. Nhưng có một niggle đối với các đầu vào lẻ, mang lại cho chúng ta n-1 , trong khi đối với các đầu vào chẵn, nó mang lại cho chúng ta n + 1 - hoàn toàn ngược lại với những gì chúng ta muốn. Vì vậy, để khắc phục điều đó, chúng tôi thực hiện thao tác trên một giá trị âm, lật hiệu quả.
8B C1 | mov eax, ecx ; copy parameter (ECX) to return register (EAX)
|
F7 D8 | neg eax ; two's-complement negation
83 F0 01 | xor eax, 1 ; XOR last bit to invert odd/even
F7 D8 | neg eax ; two's-complement negation
|
C3 | ret ; return from function
(nội tuyến: 7 byte; dưới dạng hàm: 10 byte)
Khá bóng bẩy; thật khó để thấy điều đó có thể được cải thiện như thế nào. Mặc dù vậy, có một điều khiến tôi chú ý: hai NEG
hướng dẫn 2 byte đó . Thành thật mà nói, hai byte có vẻ như một byte quá nhiều để mã hóa một phủ định đơn giản, nhưng đó là tập lệnh chúng ta phải làm việc với. Có cách giải quyết nào không? Chắc chắn rồi! Nếu chúng ta XOR
bằng -2, chúng ta có thể thay thế hành tây thứ hai NEG
bằng một INC
rement:
8B C1 | mov eax, ecx
|
F7 D8 | neg eax
83 F0 FE | xor eax, -2
40 | inc eax
|
C3 | ret
(nội tuyến: 6 byte; dưới dạng hàm: 9 byte)
Một một trong những điểm kỳ quặc của các tập lệnh x86 là sự đa năng LEA
hướng dẫn , có thể làm một động thái đăng ký-đăng ký, một bổ sung đăng ký-đăng ký, bù đắp bởi một hằng số, và mở rộng quy mô tất cả trong một chỉ dẫn duy nhất!
8B C1 | mov eax, ecx
83 E0 01 | and eax, 1 ; set EAX to 1 if even, or 0 if odd
8D 44 41 FF | lea eax, DWORD PTR [ecx + eax*2 - 1]
C3 | ret
(10 byte)
Các AND
hướng dẫn cũng giống như TEST
hướng dẫn chúng tôi sử dụng trước đó, trong đó cả hai làm một Bitwise-AND và bộ cờ cho phù hợp, nhưng AND
thực sự cập nhật các điểm đến toán hạng. Sau LEA
đó, lệnh này chia tỷ lệ này bằng 2, thêm giá trị đầu vào ban đầu và giảm đi 1. Nếu giá trị đầu vào là số lẻ, điều này sẽ trừ 1 (2 × 0 - 1 = 1) từ nó; nếu giá trị đầu vào là chẵn, điều này sẽ thêm 1 (2 × 1 - 1 = 1) cho nó.
Đây là một cách rất nhanh và hiệu quả để viết mã, vì phần lớn việc thực thi có thể được thực hiện ở mặt trước, nhưng nó không mua cho chúng ta nhiều theo cách của byte, vì phải mất rất nhiều để mã hóa một phức tạp LEA
chỉ dẫn. Phiên bản này cũng không hoạt động tốt cho mục đích nội tuyến, vì nó yêu cầu giá trị đầu vào ban đầu phải được giữ nguyên làm đầu vào của LEA
hướng dẫn. Vì vậy, với nỗ lực tối ưu hóa cuối cùng này, chúng tôi thực sự đã đi lùi, cho thấy có lẽ đã đến lúc phải dừng lại.
Do đó, đối với mục cạnh tranh cuối cùng, chúng ta có hàm 9 byte lấy giá trị đầu vào trong thanh ECX
ghi ( quy ước gọi dựa trên thanh ghi bán chuẩn trên 32 bit x86) và trả về kết quả trong thanh EAX
ghi (như với tất cả các quy ước gọi x86):
SwapParity PROC
8B C1 mov eax, ecx
F7 D8 neg eax
83 F0 FE xor eax, -2
40 inc eax
C3 ret
SwapParity ENDP
Sẵn sàng để lắp ráp với MASM; gọi từ C là:
extern int __fastcall SwapParity(int value); // MSVC
extern int __attribute__((fastcall)) SwapParity(int value); // GNU