x86 Assembly, 9 byte (untuk entri yang bersaing)
Setiap orang yang mencoba tantangan ini dalam bahasa tingkat tinggi kehilangan kesenangan nyata memanipulasi bit mentah. Ada begitu banyak variasi halus tentang cara melakukan ini, ini gila — dan banyak kesenangan untuk dipikirkan. Berikut adalah beberapa solusi yang saya rancang dalam bahasa assembly x86 32-bit.
Saya mohon maaf sebelumnya bahwa ini bukan jawaban khas kode-golf. Saya akan mengoceh banyak tentang proses pemikiran optimasi berulang (untuk ukuran). Semoga itu menarik dan mendidik untuk audiens yang lebih besar, tetapi jika Anda adalah tipe TL; DR, saya tidak akan tersinggung jika Anda melewatkannya sampai akhir.
Solusi yang jelas dan efisien adalah menguji apakah nilainya ganjil atau genap (yang dapat dilakukan secara efisien dengan melihat bit yang paling tidak signifikan), dan kemudian memilih antara n + 1 atau n − 1 yang sesuai. Dengan asumsi bahwa input dilewatkan sebagai parameter dalam ECX
register, dan hasilnya dikembalikan dalam EAX
register, kita mendapatkan fungsi berikut:
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)
Tetapi untuk tujuan kode-golf, LEA
instruksi itu tidak bagus, karena mereka membutuhkan 3 byte untuk dikodekan. DEC
Rement sederhana ECX
akan jauh lebih pendek (hanya satu byte), tetapi ini mempengaruhi flag, jadi kita harus sedikit pintar dalam bagaimana kita mengatur kode. Kita dapat melakukan pengurangan pertama , dan tes ganjil / genap kedua , tetapi kemudian kita harus membalikkan hasil tes ganjil / genap.
Juga, kita dapat mengubah instruksi pemindahan bersyarat ke cabang, yang dapat membuat kode berjalan lebih lambat (tergantung pada seberapa dapat diprediksi cabang itu - jika input bergantian secara tidak konsisten antara ganjil dan genap, cabang akan lebih lambat; jika ada pola, itu akan lebih cepat), yang akan menyelamatkan kita byte lain.
Bahkan, dengan revisi ini, seluruh operasi dapat dilakukan di tempat, hanya menggunakan satu register. Ini bagus jika Anda memasukkan kode ini di suatu tempat (dan kemungkinan besar, Anda pasti akan melakukannya, karena ini sangat singkat).
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)
(sebaris: 7 byte; sebagai fungsi: 10 byte)
Tetapi bagaimana jika Anda memang ingin membuatnya berfungsi? Tidak ada konvensi panggilan standar yang menggunakan register yang sama untuk melewatkan parameter seperti halnya untuk nilai pengembalian, jadi Anda perlu menambahkan MOV
instruksi register-register ke awal atau akhir fungsi. Ini hampir tidak ada biaya dalam kecepatan, tetapi menambah 2 byte. ( RET
Instruksi ini juga menambahkan byte, dan ada beberapa overhead yang diperkenalkan oleh kebutuhan untuk membuat dan kembali dari panggilan fungsi, yang berarti ini adalah salah satu contoh di mana inlining menghasilkan kecepatan dan manfaat ukuran, daripada hanya menjadi kecepatan klasik -untuk pengorbanan ruang.) Secara keseluruhan, ditulis sebagai fungsi, kode ini membengkak hingga 10 byte.
Apa lagi yang bisa kita lakukan dalam 10 byte? Jika kita benar-benar peduli tentang kinerja (setidaknya, kinerja yang dapat diprediksi ), alangkah baiknya untuk menyingkirkan cabang itu. Berikut ini adalah solusi branchless, bit-twiddling yang ukurannya sama dengan byte. Premis dasarnya sederhana: kita menggunakan bitor XOR untuk membalik bit terakhir, mengubah nilai ganjil menjadi genap genap, dan sebaliknya. Tapi ada satu niggle — untuk input ganjil, yang memberi kita n-1 , sedangkan untuk input genap, ia memberi kita n +1 - persis berlawanan dengan yang kita inginkan. Jadi, untuk memperbaikinya, kami melakukan operasi pada nilai negatif, secara efektif membalik tanda.
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
(sebaris: 7 byte; sebagai fungsi: 10 byte)
Cukup apik; sulit untuk melihat bagaimana hal itu dapat diperbaiki. Satu hal yang menarik perhatian saya: dua NEG
instruksi 2 byte itu . Terus terang, dua byte sepertinya satu byte terlalu banyak untuk menyandikan negasi sederhana, tapi itu adalah set instruksi yang harus kita kerjakan. Apakah ada solusi? Yakin! Jika XOR
dengan -2, kita dapat mengganti NEG
asi kedua dengan INC
rement:
8B C1 | mov eax, ecx
|
F7 D8 | neg eax
83 F0 FE | xor eax, -2
40 | inc eax
|
C3 | ret
(sebaris: 6 byte; sebagai fungsi: 9 byte)
Satu lagi keanehan dari set instruksi x86 adalah instruksi multigunaLEA
, yang dapat melakukan gerakan register-register, penambahan register-register, dikompensasi dengan konstanta, dan menskalakan semua dalam satu instruksi tunggal!
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)
The AND
instruksi seperti TEST
instruksi kita gunakan sebelumnya, dalam bahwa baik melakukan bitwise-AND dan set bendera sesuai, tapi AND
benar-benar memperbarui tujuan operan. The LEA
instruksi kemudian skala ini dengan 2, menambah nilai masukan asli, dan decrements oleh 1. Jika nilai masukan aneh, mengurangi ini 1 (2 × 0-1 = -1) dari itu; jika nilai inputnya genap, ini menambah 1 (2 × 1 - 1 = 1) padanya.
Ini adalah cara yang sangat cepat dan efisien untuk menulis kode, karena banyak dari eksekusi dapat dilakukan di front-end, tetapi itu tidak memberi kita banyak dalam hal byte, karena dibutuhkan begitu banyak untuk mengkodekan kompleks LEA
petunjuk. Versi ini juga tidak berfungsi dengan baik untuk tujuan inlining, karena mengharuskan nilai input asli dipertahankan sebagai input dari LEA
instruksi. Jadi dengan upaya pengoptimalan terakhir ini, kami telah benar-benar mundur, menyarankan mungkin sudah waktunya untuk berhenti.
Jadi, untuk entri terakhir yang bersaing, kami memiliki fungsi 9-byte yang mengambil nilai input dalam ECX
register ( konvensi panggilan berbasis register semi-standar pada 32-bit x86), dan mengembalikan hasilnya dalam EAX
register (seperti halnya semua konvensi pemanggilan 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
Siap berkumpul dengan MASM; panggilan dari C sebagai:
extern int __fastcall SwapParity(int value); // MSVC
extern int __attribute__((fastcall)) SwapParity(int value); // GNU