Apakah lebih cepat menggunakan say (i << 3) + (i << 1) untuk dikalikan dengan 10 daripada menggunakan i * 10 secara langsung?
Mungkin atau mungkin tidak ada di mesin Anda - jika Anda peduli, ukurlah dalam penggunaan dunia nyata Anda.
Sebuah studi kasus - dari 486 ke inti i7
Benchmarking sangat sulit dilakukan secara bermakna, tetapi kita dapat melihat beberapa fakta. Dari http://www.penguin.cz/~literakl/intel/s.html#SAL dan http://www.penguin.cz/~literakl/intel/i.html#IMUL kita mendapatkan gagasan tentang siklus clock x86 dibutuhkan untuk perubahan aritmatika dan perkalian. Katakanlah kita berpegang pada "486" (yang terbaru terdaftar), register 32 bit dan segera, IMUL mengambil 13-42 siklus dan IDIV 44. Setiap SAL mengambil 2, dan menambahkan 1, sehingga bahkan dengan beberapa dari mereka yang bersama-sama bergeser tampak dangkal seperti seorang pemenang.
Hari-hari ini, dengan i7 inti:
(dari http://software.intel.com/en-us/forums/showthread.php?t=61481 )
Latensi adalah 1 siklus untuk penambahan bilangan bulat dan 3 siklus untuk perkalian bilangan bulat . Anda dapat menemukan latensi dan masukan dalam Lampiran C "Manual Referensi Optimasi Arsitektur Intel® 64 dan IA-32", yang terdapat di http://www.intel.com/products/processor/manuals/ .
(dari beberapa uraian Intel)
Menggunakan SSE, Core i7 dapat mengeluarkan instruksi menambahkan dan mengalikan secara simultan, menghasilkan tingkat puncak 8 operasi floating-point (FLOP) per siklus clock
Itu memberi Anda gambaran tentang seberapa jauh hal-hal telah terjadi. Trivia optimisasi - seperti bit shifting versus*
- yang telah dianggap serius bahkan sampai tahun 90an sudah usang sekarang. Bit-shifting masih lebih cepat, tetapi untuk non-power-of-two mul / div pada saat Anda melakukan semua shift Anda dan menambahkan hasilnya lebih lambat lagi. Kemudian, lebih banyak instruksi berarti lebih banyak kesalahan cache, lebih banyak potensi masalah dalam pemipaan, lebih banyak menggunakan register sementara dapat berarti lebih banyak menyimpan dan memulihkan konten register dari stack ... dengan cepat menjadi terlalu rumit untuk mengukur semua dampak secara definitif tetapi mereka sebagian besar negatif.
fungsionalitas dalam kode sumber vs implementasi
Secara umum, pertanyaan Anda ditandai C dan C ++. Sebagai bahasa generasi ke-3, mereka dirancang khusus untuk menyembunyikan detail set instruksi CPU yang mendasarinya. Untuk memenuhi Standar bahasa mereka, mereka harus mendukung operasi multiplikasi dan perpindahan (dan banyak lainnya) bahkan jika perangkat keras yang mendasarinya tidak . Dalam kasus seperti itu, mereka harus mensintesis hasil yang diperlukan menggunakan banyak instruksi lain. Demikian pula, mereka harus memberikan dukungan perangkat lunak untuk operasi floating point jika CPU tidak memilikinya dan tidak ada FPU. CPU modern semua mendukung *
dan<<
, jadi ini mungkin tampak tidak masuk akal secara teoritis dan historis, tetapi yang penting adalah bahwa kebebasan untuk memilih implementasi berjalan dua arah: bahkan jika CPU memiliki instruksi yang mengimplementasikan operasi yang diminta dalam kode sumber dalam kasus umum, kompiler bebas untuk pilih sesuatu yang lebih disukai karena lebih baik untuk kasus spesifik yang dihadapi oleh kompiler.
Contoh (dengan bahasa majelis hipotetis)
source literal approach optimised approach
#define N 0
int x; .word x xor registerA, registerA
x *= N; move x -> registerA
move x -> registerB
A = B * immediate(0)
store registerA -> x
...............do something more with x...............
Instruksi seperti eksklusif atau ( xor
) tidak memiliki hubungan dengan kode sumber, tetapi xor-ing apa pun dengan sendirinya membersihkan semua bit, sehingga dapat digunakan untuk mengatur sesuatu menjadi 0. Kode sumber yang menyiratkan alamat memori mungkin tidak memerlukan penggunaan apa pun.
Jenis peretasan ini telah digunakan selama komputer ada. Pada hari-hari awal 3GL, untuk mengamankan serapan pengembang, output kompiler harus memuaskan pengembang bahasa pengoptimalisasi tangan hardcore yang ada. komunitas bahwa kode yang dihasilkan tidak lebih lambat, lebih banyak kata atau lebih buruk. Compiler dengan cepat mengadopsi banyak optimisasi hebat - mereka menjadi toko yang lebih tersentralisasi daripada yang bisa dilakukan oleh programmer bahasa assembly mana pun, meskipun selalu ada kemungkinan mereka kehilangan optimasi tertentu yang penting dalam kasus tertentu - manusia kadang-kadang dapat buang dan grope untuk sesuatu yang lebih baik sementara kompiler hanya melakukan apa yang diperintahkan sampai seseorang memberi makan pengalaman itu kembali ke mereka.
Jadi, bahkan jika pengalihan dan penambahan masih lebih cepat pada beberapa perangkat keras tertentu, maka penulis kompiler kemungkinan telah bekerja tepat ketika itu aman dan menguntungkan.
Maintabilitas
Jika perubahan perangkat keras Anda, Anda dapat mengkompilasi ulang dan itu akan melihat CPU target dan membuat pilihan terbaik lain, sedangkan Anda tidak akan pernah ingin mengunjungi kembali "optimisasi" Anda atau daftar mana lingkungan kompilasi yang harus menggunakan perkalian dan mana yang harus bergeser. Pikirkan semua "optimisasi" non-kekuatan-dua-bit-bergeser yang ditulis 10+ tahun yang lalu yang memperlambat kode mereka saat berjalan pada prosesor modern ...!
Untungnya, kompiler yang baik seperti GCC biasanya dapat mengganti serangkaian bithift dan aritmatika dengan perkalian langsung ketika optimasi apa pun diaktifkan (mis. ...main(...) { return (argc << 4) + (argc << 2) + argc; }
-> imull $21, 8(%ebp), %eax
) sehingga kompilasi ulang dapat membantu bahkan tanpa memperbaiki kode, tetapi itu tidak dijamin.
Kode bitshifting aneh yang menerapkan perkalian atau pembagian jauh lebih ekspresif dari apa yang Anda coba capai secara konseptual, sehingga pengembang lain akan bingung dengan hal itu, dan programmer yang bingung lebih mungkin memperkenalkan bug atau menghapus sesuatu yang penting dalam upaya mengembalikan kewarasan yang tampak. Jika Anda hanya melakukan hal-hal yang tidak jelas ketika mereka benar-benar bermanfaat, dan kemudian mendokumentasikannya dengan baik (tapi jangan mendokumentasikan hal-hal lain yang intuitif), semua orang akan lebih bahagia.
Solusi umum versus solusi parsial
Jika Anda memiliki pengetahuan tambahan, seperti bahwa Anda int
benar-benar hanya akan menyimpan nilai x
, y
dan z
, maka Anda mungkin dapat mengerjakan beberapa instruksi yang bekerja untuk nilai-nilai itu dan memberi Anda hasil Anda lebih cepat daripada ketika kompiler tidak memiliki wawasan itu dan membutuhkan implementasi yang bekerja untuk semua int
nilai. Misalnya, pertimbangkan pertanyaan Anda:
Perkalian dan pembagian dapat dicapai menggunakan operator bit ...
Anda menggambarkan perkalian, tetapi bagaimana dengan pembagian?
int x;
x >> 1; // divide by 2?
Menurut C ++ Standard 5.8:
-3- Nilai E1 >> E2 adalah posisi bit E2 bergeser kanan E1. Jika E1 memiliki tipe yang tidak ditandatangani atau jika E1 memiliki tipe yang ditandatangani dan nilai yang tidak negatif, nilai hasilnya adalah bagian integral dari hasil bagi E1 dibagi dengan jumlah 2 yang diangkat ke daya E2. Jika E1 memiliki tipe yang ditandatangani dan nilai negatif, nilai yang dihasilkan ditentukan oleh implementasi.
Jadi, bit shift Anda memiliki hasil yang ditentukan ketika hasilnya x
negatif: itu mungkin tidak bekerja dengan cara yang sama pada mesin yang berbeda. Tapi, /
kerjanya jauh lebih mudah ditebak. (Ini mungkin juga tidak sepenuhnya konsisten, karena mesin yang berbeda mungkin memiliki representasi berbeda dari bilangan negatif, dan karenanya rentang yang berbeda bahkan ketika ada jumlah bit yang sama yang membentuk representasi.)
Anda mungkin berkata, "Saya tidak peduli ... yang int
menyimpan usia karyawan, itu tidak mungkin negatif". Jika Anda memiliki wawasan khusus semacam itu, maka ya - >>
optimisasi aman Anda mungkin dilewati oleh kompiler kecuali Anda melakukannya secara eksplisit dalam kode Anda. Tapi, itu berisiko dan jarang berguna karena Anda tidak akan memiliki wawasan seperti ini, dan programmer lain yang bekerja dengan kode yang sama tidak akan tahu bahwa Anda telah bertaruh dengan harapan yang tidak biasa dari data yang Anda miliki. akan menangani ... apa yang tampaknya benar-benar perubahan yang aman bagi mereka mungkin menjadi bumerang karena "optimasi" Anda.
Apakah ada input yang tidak dapat dikalikan atau dibagi dengan cara ini?
Ya ... seperti yang disebutkan di atas, angka negatif memiliki implementasi perilaku yang ditentukan ketika "dibagi" dengan sedikit-bergeser.