Salah satu alasan yang dinyatakan untuk mengetahui assembler adalah bahwa, kadang-kadang, dapat digunakan untuk menulis kode yang akan lebih berkinerja daripada menulis kode itu dalam bahasa tingkat yang lebih tinggi, khususnya C. Namun, saya juga pernah mendengarnya menyatakan berkali-kali bahwa meskipun itu tidak sepenuhnya salah, kasus-kasus di mana assembler sebenarnya dapat digunakan untuk menghasilkan lebih banyak kode performan keduanya sangat jarang dan memerlukan pengetahuan dan pengalaman ahli dalam perakitan.
Pertanyaan ini bahkan tidak masuk ke dalam fakta bahwa instruksi assembler akan spesifik untuk mesin dan non-portabel, atau aspek assembler lainnya. Ada banyak alasan bagus untuk mengetahui perakitan selain yang ini, tentu saja, tetapi ini dimaksudkan untuk menjadi pertanyaan spesifik yang meminta contoh dan data, bukan wacana panjang tentang assembler versus bahasa tingkat yang lebih tinggi.
Adakah yang bisa memberikan beberapa contoh spesifik kasus di mana perakitan akan lebih cepat daripada kode C yang ditulis dengan baik menggunakan kompiler modern, dan dapatkah Anda mendukung klaim tersebut dengan bukti profil? Saya cukup yakin kasus-kasus ini ada, tetapi saya benar-benar ingin tahu persis seberapa esoteriknya kasus-kasus ini, karena tampaknya menjadi pokok perdebatan.
sumber
-O3
flag, Anda mungkin lebih baik meninggalkan optimasi ke kompiler C :-)Jawaban:
Berikut adalah contoh dunia nyata: Titik tetap mengalikan pada kompiler lama.
Ini tidak hanya berguna pada perangkat tanpa floating point, mereka bersinar ketika datang ke presisi karena mereka memberi Anda 32 bit presisi dengan kesalahan yang dapat diprediksi (float hanya memiliki 23 bit dan lebih sulit untuk memprediksi kehilangan presisi). yaitu presisi absolut seragam pada seluruh rentang, bukannya presisi relatif dekat-seragam (
float
).Kompiler modern mengoptimalkan contoh titik tetap ini dengan baik, jadi untuk contoh lebih modern yang masih membutuhkan kode khusus penyusun, lihat
uint64_t
untuk 32x32 => Penggandaan 64-bit gagal untuk mengoptimalkan pada CPU 64-bit, jadi Anda memerlukan intrinsik atau__int128
kode efisien pada sistem 64-bit.C tidak memiliki operator multiplikasi penuh (hasil 2N-bit dari input N-bit). Cara biasa untuk mengekspresikannya dalam C adalah dengan memasukkan input ke tipe yang lebih luas dan berharap kompiler mengetahui bahwa bit atas dari input tidak menarik:
Masalah dengan kode ini adalah bahwa kita melakukan sesuatu yang tidak dapat secara langsung diekspresikan dalam bahasa C. Kami ingin melipatgandakan dua angka 32 bit dan mendapatkan hasil 64 bit yang kami kembalikan menjadi bit 32 tengah. Namun, dalam C, perkalian ini tidak ada. Yang dapat Anda lakukan adalah mempromosikan integer ke 64 bit dan melakukan 64 * 64 = 64 multiply.
x86 (dan ARM, MIPS, dan lainnya) dapat melakukan kalikan dalam satu instruksi. Beberapa kompiler digunakan untuk mengabaikan fakta ini dan menghasilkan kode yang memanggil fungsi pustaka runtime untuk melakukan penggandaan. Pergeseran oleh 16 juga sering dilakukan oleh rutin perpustakaan (juga x86 dapat melakukan pergeseran tersebut).
Jadi kita pergi dengan satu atau dua panggilan perpustakaan hanya untuk penggandaan. Ini memiliki konsekuensi serius. Tidak hanya shiftnya yang lebih lambat, register harus dilestarikan di seluruh fungsi panggilan dan itu tidak membantu inlining dan membuka kode juga.
Jika Anda menulis ulang kode yang sama di assembler (inline) Anda dapat memperoleh peningkatan kecepatan yang signifikan.
Selain itu: menggunakan ASM bukan cara terbaik untuk menyelesaikan masalah. Sebagian besar kompiler memungkinkan Anda untuk menggunakan beberapa instruksi assembler dalam bentuk intrinsik jika Anda tidak dapat mengekspresikannya dalam C. Kompiler VS.NET2008 misalnya memperlihatkan 32 * 32 = 64 bit mul sebagai __emul dan pergeseran 64 bit sebagai __ll_rshift.
Menggunakan intrinsik Anda dapat menulis ulang fungsi dengan cara yang membuat kompiler C memiliki kesempatan untuk memahami apa yang terjadi. Ini memungkinkan kode untuk diuraikan, register dialokasikan, eliminasi subekspresi umum dan propagasi konstan dapat dilakukan juga. Anda akan mendapatkan peningkatan kinerja yang sangat besar dibandingkan kode assembler yang ditulis tangan dengan cara itu.
Untuk referensi: Hasil akhir untuk mul titik tetap untuk kompiler VS.NET adalah:
Perbedaan kinerja pembagian titik tetap bahkan lebih besar. Saya memiliki peningkatan hingga faktor 10 untuk divisi kode titik tetap berat dengan menulis beberapa asm-lines.
Menggunakan Visual C ++ 2013 memberikan kode perakitan yang sama untuk kedua cara.
gcc4.1 dari 2007 juga mengoptimalkan versi C murni dengan baik. (Penjelajah kompiler Godbolt tidak memiliki versi gcc yang diinstal sebelumnya, tetapi mungkin versi GCC yang lebih lama dapat melakukan ini tanpa intrinsik.)
Lihat sumber + asm untuk x86 (32-bit) dan ARM pada explorer compiler Godbolt . (Sayangnya itu tidak memiliki kompiler yang cukup tua untuk menghasilkan kode buruk dari versi C murni sederhana.)
CPU modern dapat melakukan hal-hal yang tidak dimiliki operator C sama sekali , seperti
popcnt
atau bit-scan untuk menemukan bit set pertama atau terakhir . (POSIX memilikiffs()
fungsi, tetapi semantiknya tidak cocok dengan x86bsf
/bsr
. Lihat https://en.wikipedia.org/wiki/Find_first_set ).Beberapa kompiler terkadang dapat mengenali loop yang menghitung jumlah bit yang ditetapkan dalam integer dan mengkompilasinya ke
popcnt
instruksi (jika diaktifkan pada waktu kompilasi), tetapi jauh lebih dapat diandalkan untuk digunakan__builtin_popcnt
di GNU C, atau pada x86 jika Anda hanya menargetkan perangkat keras dengan SSE4.2:_mm_popcnt_u32
dari<immintrin.h>
.Atau di C ++, tetapkan ke a
std::bitset<32>
dan gunakan.count()
. (Ini adalah kasus di mana bahasa telah menemukan cara untuk mengekspos secara mudah implementasi popcount yang dioptimalkan melalui perpustakaan standar, dengan cara yang akan selalu dikompilasi ke sesuatu yang benar, dan dapat mengambil keuntungan dari apa pun yang didukung oleh target.) Lihat juga https : //en.wikipedia.org/wiki/Hamming_weight#Language_support .Demikian pula,
ntohl
dapat dikompilasi kebswap
(x86 swap 32-bit untuk konversi endian) pada beberapa implementasi C yang memilikinya.Bidang utama lain untuk intrinsik atau asm yang ditulis tangan adalah vektorisasi manual dengan instruksi SIMD. Kompiler tidak buruk dengan loop sederhana seperti
dst[i] += src[i] * 10.0;
, tetapi sering melakukan buruk atau tidak melakukan auto-vektor sama sekali ketika keadaan menjadi lebih rumit. Misalnya, Anda tidak mungkin mendapatkan apa pun seperti Bagaimana menerapkan atoi menggunakan SIMD? dihasilkan secara otomatis oleh kompiler dari kode skalar.sumber
Bertahun-tahun yang lalu saya mengajar seseorang untuk memprogram dalam C. Latihan adalah memutar grafik hingga 90 derajat. Dia kembali dengan solusi yang membutuhkan waktu beberapa menit untuk diselesaikan, terutama karena dia menggunakan penggandaan dan pembagian dll.
Saya menunjukkan kepadanya bagaimana menyusun kembali masalah menggunakan bit shift, dan waktu untuk memproses turun menjadi sekitar 30 detik pada kompiler non-optimalisasi yang dimilikinya.
Saya baru saja mendapatkan kompilator yang mengoptimalkan dan kode yang sama memutar grafik dalam <5 detik. Saya melihat kode perakitan yang dihasilkan oleh kompiler, dan dari apa yang saya lihat diputuskan di sana dan kemudian bahwa hari-hari saya menulis assembler telah berakhir.
sumber
add di,di / adc al,al / add di,di / adc ah,ah
dll. Untuk semua delapan register 8-bit, kemudian lakukan semua 8 register lagi, dan kemudian ulangi seluruh prosedur tiga lebih sering, dan akhirnya menyimpan empat kata dalam ax / bx / cx / dx. Tidak mungkin seorang assembler akan mendekati itu.Hampir setiap saat kompiler melihat kode titik apung, versi tulisan tangan akan lebih cepat jika Anda menggunakan kompiler buruk lama. ( Pembaruan 2019: Ini tidak berlaku secara umum untuk kompiler modern. Terutama ketika mengkompilasi untuk apa pun selain x87; kompiler memiliki waktu yang lebih mudah dengan SSE2 atau AVX untuk matematika skalar, atau non-x86 dengan set register FP datar, tidak seperti x87's register stack.)
Alasan utama adalah bahwa kompiler tidak dapat melakukan optimasi yang kuat. Lihat artikel ini dari MSDN untuk diskusi tentang masalah ini. Berikut adalah contoh di mana versi perakitan dua kali kecepatan dari versi C (dikompilasi dengan VS2K5):
Dan beberapa nomor dari PC saya yang menjalankan rilis rilis bawaan * :
Tidak tertarik, saya bertukar loop dengan dec / jnz dan tidak ada bedanya dengan timing - kadang lebih cepat, kadang lebih lambat. Saya kira aspek memori terbatas kurcaci optimasi lainnya. (Catatan editor: kemungkinan besar hambatan latensi FP cukup untuk menyembunyikan biaya tambahan
loop
. Melakukan dua penjumlahan Kahan secara paralel untuk elemen ganjil / genap, dan menambahkannya pada akhirnya, mungkin dapat mempercepat ini dengan faktor 2. )Aduh, saya menjalankan versi kode yang sedikit berbeda dan menghasilkan angka dengan cara yang salah (yaitu C lebih cepat!). Memperbaiki dan memperbarui hasil.
sumber
-ffast-math
. Mereka memiliki tingkat optimisasi,-Ofast
yang saat ini setara dengan-O3 -ffast-math
, tetapi di masa depan dapat mencakup lebih banyak optimasi yang dapat menyebabkan pembuatan kode yang salah dalam kasus sudut (seperti kode yang bergantung pada IEEE NaNs).a+b == b+a
), tetapi tidak asosiatif (penataan ulang operasi, jadi pembulatan perantara berbeda). re: kode ini: Saya tidak berpikir x87 uncommented danloop
instruksi adalah demonstrasi yang sangat mengagumkan dari asm cepat.loop
tampaknya bukan hambatan karena latensi FP. Saya tidak yakin apakah dia sedang melakukan pipeline operasi FP atau tidak; x87 sulit bagi manusia untuk membaca. Duafstp results
insn pada akhirnya jelas tidak optimal. Memunculkan hasil tambahan dari tumpukan akan lebih baik dilakukan dengan non-toko. Sepertifstp st(0)
IIRC.Tanpa memberikan contoh atau bukti profiler spesifik, Anda dapat menulis assembler yang lebih baik daripada kompiler ketika Anda tahu lebih banyak dari kompiler.
Dalam kasus umum, kompiler C modern tahu lebih banyak tentang bagaimana mengoptimalkan kode yang dimaksud: ia tahu cara kerja pipa prosesor, ia dapat mencoba menyusun ulang instruksi lebih cepat daripada yang dapat dilakukan manusia, dan seterusnya - itu pada dasarnya sama dengan komputer menjadi sebagus atau lebih baik dari pemain manusia terbaik untuk boardgames, dll. hanya karena ia dapat membuat pencarian dalam ruang masalah lebih cepat daripada kebanyakan manusia. Meskipun secara teoritis Anda dapat melakukan serta komputer dalam kasus tertentu, Anda tentu tidak dapat melakukannya dengan kecepatan yang sama, membuatnya tidak layak untuk lebih dari beberapa kasus (yaitu kompiler pasti akan mengungguli Anda jika Anda mencoba menulis lebih dari beberapa rutin dalam assembler).
Di sisi lain, ada kasus di mana kompiler tidak memiliki informasi sebanyak - saya akan mengatakan terutama ketika bekerja dengan berbagai bentuk perangkat keras eksternal, di mana kompiler tidak memiliki pengetahuan. Contoh utama mungkin adalah driver perangkat, di mana assembler dikombinasikan dengan pengetahuan intim manusia tentang perangkat keras tersebut dapat menghasilkan hasil yang lebih baik daripada yang bisa dilakukan oleh kompiler C.
Yang lain telah menyebutkan instruksi tujuan khusus, yang saya bicarakan pada paragraf di atas - instruksi yang mungkin dibatasi oleh kompilator atau tidak memiliki pengetahuan sama sekali, sehingga memungkinkan manusia untuk menulis kode lebih cepat.
sumber
ocamlopt
melompati penjadwalan instruksi pada x86 dan, sebaliknya, menyerahkannya ke CPU karena dapat menyusun ulang lebih efektif pada saat run-time.Dalam pekerjaan saya, ada tiga alasan bagi saya untuk mengetahui dan menggunakan perakitan. Dalam urutan kepentingan:
Debugging - Saya sering mendapatkan kode perpustakaan yang memiliki bug atau dokumentasi yang tidak lengkap. Saya mencari tahu apa yang dilakukannya dengan melangkah di tingkat perakitan. Saya harus melakukan ini seminggu sekali. Saya juga menggunakannya sebagai alat untuk debug masalah di mana mata saya tidak menemukan kesalahan idiomatik di C / C ++ / C #. Melihat majelis akan melewati itu.
Mengoptimalkan - kompiler tidak cukup baik dalam mengoptimalkan, tapi saya bermain di stadion baseball yang berbeda dari kebanyakan. Saya menulis kode pemrosesan gambar yang biasanya dimulai dengan kode yang terlihat seperti ini:
"lakukan sesuatu bagian" biasanya terjadi pada urutan beberapa juta kali (yaitu, antara 3 dan 30). Dengan menggores siklus dalam fase "lakukan sesuatu", keuntungan kinerja sangat diperbesar. Saya biasanya tidak mulai di sana - saya biasanya mulai dengan menulis kode untuk bekerja terlebih dahulu, kemudian melakukan yang terbaik untuk refactor C menjadi lebih baik secara alami (algoritma yang lebih baik, lebih sedikit beban dalam loop dll). Saya biasanya perlu membaca majelis untuk melihat apa yang terjadi dan jarang perlu menulisnya. Saya melakukan ini mungkin setiap dua atau tiga bulan.
melakukan sesuatu yang bahasa tidak akan membiarkan saya. Ini termasuk - mendapatkan arsitektur prosesor dan fitur prosesor tertentu, mengakses flag yang tidak ada di CPU (man, saya benar-benar berharap C memberi Anda akses ke flag carry), dll. Saya melakukan ini mungkin sekali setahun atau dua tahun.
sumber
Hanya ketika menggunakan beberapa instruksi tujuan khusus set compiler tidak mendukung.
Untuk memaksimalkan daya komputasi dari CPU modern dengan banyak saluran pipa dan percabangan prediktif, Anda perlu menyusun program perakitan dengan cara yang membuatnya a) hampir mustahil bagi manusia untuk menulis b) bahkan lebih tidak mungkin untuk dipertahankan.
Selain itu, algoritma, struktur data, dan manajemen memori yang lebih baik akan memberikan setidaknya urutan kinerja yang lebih besar daripada optimasi mikro yang dapat Anda lakukan dalam perakitan.
sumber
Meskipun C "dekat" dengan manipulasi tingkat rendah dari data 8-bit, 16-bit, 32-bit, 64-bit, ada beberapa operasi matematika yang tidak didukung oleh C yang sering dapat dilakukan secara elegan dalam instruksi perakitan tertentu set:
Perkalian titik tetap: Produk dua angka 16-bit adalah angka 32-bit. Tetapi aturan dalam C mengatakan bahwa produk dari dua angka 16-bit adalah angka 16-bit, dan produk dari dua angka 32-bit adalah angka 32-bit - bagian bawah dalam kedua kasus. Jika Anda ingin bagian atas dari kelipatan 16x16 atau kelipatan 32x32, Anda harus bermain gim dengan kompiler. Metode umum adalah untuk melemparkan ke lebar bit yang lebih besar dari yang diperlukan, berkembang biak, bergeser ke bawah, dan melemparkan kembali:
Dalam hal ini kompiler mungkin cukup pintar untuk mengetahui bahwa Anda benar-benar hanya mencoba untuk mendapatkan bagian atas dari kelipatan 16x16 dan melakukan hal yang benar dengan 16x16 asli mesin. Atau mungkin itu bodoh dan memerlukan panggilan perpustakaan untuk melakukan penggandaan 32x32 itu terlalu banyak karena Anda hanya membutuhkan 16 bit produk - tetapi standar C tidak memberi Anda cara untuk mengekspresikan diri.
Operasi bitshifting tertentu (rotasi / membawa):
Ini tidak terlalu salah dalam C, tetapi sekali lagi, kecuali jika kompiler cukup pintar untuk menyadari apa yang Anda lakukan, itu akan melakukan banyak pekerjaan yang "tidak perlu". Banyak set instruksi perakitan memungkinkan Anda untuk memutar atau bergeser ke kiri / kanan dengan hasil dalam register carry, sehingga Anda dapat mencapai instruksi di atas dalam 34 instruksi: memuat pointer ke awal array, menghapus carry, dan melakukan 32 8- menggeser ke kanan, menggunakan peningkatan otomatis pada pointer.
Sebagai contoh lain, ada register geser umpan balik linier (LFSR) yang secara elegan dilakukan dalam perakitan: Ambil sepotong N bit (8, 16, 32, 64, 128, dll), geser semuanya dengan benar oleh 1 (lihat di atas algoritma), maka jika carry yang dihasilkan adalah 1 maka Anda XOR dalam pola bit yang mewakili polinomial.
Karena itu, saya tidak akan menggunakan teknik ini kecuali saya memiliki kendala kinerja yang serius. Seperti yang orang lain katakan, perakitan jauh lebih sulit untuk didokumentasikan / debug / uji / pemeliharaan daripada kode C: peningkatan kinerja datang dengan beberapa biaya serius.
sunting: 3. Deteksi overflow dimungkinkan dalam perakitan (tidak dapat benar-benar melakukannya dalam C), ini membuat beberapa algoritma lebih mudah.
sumber
Jawaban singkat? Terkadang.
Secara teknis setiap abstraksi memiliki biaya dan bahasa pemrograman adalah abstraksi untuk cara kerja CPU. Namun C sangat dekat. Bertahun-tahun yang lalu saya ingat tertawa terbahak-bahak ketika saya masuk ke akun UNIX saya dan mendapat pesan keberuntungan berikut (ketika hal-hal seperti itu populer):
Ini lucu karena itu benar: C seperti bahasa rakitan portabel.
Perlu dicatat bahwa bahasa assembly hanya berjalan namun Anda menulisnya. Namun ada kompiler di antara C dan bahasa assembly yang dihasilkannya dan itu sangat penting karena seberapa cepat kode C Anda memiliki banyak sekali hubungannya dengan seberapa baik kompiler Anda.
Ketika gcc datang ke tempat kejadian, salah satu hal yang membuatnya sangat populer adalah sering kali jauh lebih baik daripada kompiler C yang dikirim dengan banyak rasa UNIX komersial. Tidak hanya itu ANSI C (tidak ada sampah K&R C ini), lebih kuat dan biasanya menghasilkan kode yang lebih baik (lebih cepat). Tidak selalu tetapi sering.
Saya memberitahu Anda semua ini karena tidak ada aturan selimut tentang kecepatan C dan assembler karena tidak ada standar objektif untuk C.
Demikian juga, assembler sangat bervariasi tergantung pada prosesor apa yang Anda jalankan, spesifikasi sistem Anda, set instruksi apa yang Anda gunakan dan sebagainya. Secara historis ada dua keluarga arsitektur CPU: CISC dan RISC. Pemain terbesar di CISC adalah arsitektur Intel x86 (dan set instruksi). RISC mendominasi dunia UNIX (MIPS6000, Alpha, Sparc dan sebagainya). CISC memenangkan pertempuran untuk hati dan pikiran.
Bagaimanapun, kearifan populer ketika saya adalah pengembang yang lebih muda adalah bahwa x86 yang ditulis tangan sering kali bisa lebih cepat daripada C karena cara arsitekturnya bekerja, ia memiliki kompleksitas yang diuntungkan oleh manusia yang melakukannya. RISC di sisi lain tampaknya dirancang untuk kompiler sehingga tidak seorang pun (saya tahu) menulis kata assembler Sparc. Saya yakin orang-orang seperti itu ada tetapi tidak diragukan lagi mereka berdua sudah gila dan sudah dilembagakan sekarang.
Set instruksi adalah poin penting bahkan dalam keluarga prosesor yang sama. Prosesor Intel tertentu memiliki ekstensi seperti SSE hingga SSE4. AMD memiliki instruksi SIMD mereka sendiri. Manfaat dari bahasa pemrograman seperti C adalah seseorang dapat menulis perpustakaan mereka sehingga dioptimalkan untuk prosesor yang Anda jalankan. Itu adalah kerja keras assembler.
Masih ada optimisasi yang dapat Anda lakukan di assembler yang tidak dapat dilakukan oleh compiler dan algoirthm assembler yang ditulis dengan baik akan lebih cepat atau lebih cepat daripada yang setara dengan C. Pertanyaan yang lebih besar adalah: apakah itu layak?
Akhirnya assembler adalah produk pada masanya dan lebih populer pada saat siklus CPU mahal. Saat ini CPU yang harganya $ 5-10 untuk pembuatan (Intel Atom) dapat melakukan hampir semua hal yang diinginkan. Satu-satunya alasan nyata untuk menulis assembler hari ini adalah untuk hal-hal tingkat rendah seperti beberapa bagian dari sistem operasi (meskipun demikian sebagian besar kernel Linux ditulis dalam C), driver perangkat, mungkin perangkat yang tertanam (meskipun C cenderung mendominasi di sana juga) dan seterusnya. Atau hanya untuk iseng (yang agak masokis).
sumber
Kasus penggunaan yang mungkin tidak berlaku lagi tetapi untuk kesenangan nerd Anda: Di Amiga, CPU dan chip grafis / audio akan berjuang untuk mengakses area RAM tertentu (2MB RAM pertama yang lebih spesifik). Jadi, ketika Anda hanya memiliki RAM 2MB (atau kurang), menampilkan grafik yang rumit plus suara yang diputar akan mematikan kinerja CPU.
Dalam assembler, Anda dapat melakukan interleave kode Anda sedemikian rupa sehingga CPU hanya akan mencoba mengakses RAM ketika chip grafis / audio sedang sibuk secara internal (yaitu ketika bus itu bebas). Jadi dengan memesan kembali instruksi Anda, penggunaan cache CPU yang cerdas, pengaturan waktu bus, Anda dapat mencapai beberapa efek yang sama sekali tidak mungkin menggunakan bahasa tingkat yang lebih tinggi karena Anda harus menghitung waktu setiap perintah, bahkan memasukkan NOP di sana-sini untuk menjaga berbagai chip dari masing-masing radar lainnya.
Yang merupakan alasan lain mengapa instruksi NOP (No Operation - do nothing) CPU benar-benar dapat membuat seluruh aplikasi Anda berjalan lebih cepat.
[EDIT] Tentu saja, tekniknya tergantung pada pengaturan perangkat keras tertentu. Itulah alasan utama mengapa banyak game Amiga tidak dapat mengatasi CPU yang lebih cepat: Waktu instruksi tidak aktif.
sumber
Poin satu yang bukan jawabannya.
Bahkan jika Anda tidak pernah memprogram di dalamnya, saya merasa berguna untuk mengetahui setidaknya satu set instruksi assembler. Ini adalah bagian dari pencarian programmer tanpa akhir untuk mengetahui lebih banyak dan karenanya menjadi lebih baik. Juga berguna ketika melangkah ke kerangka kerja Anda tidak memiliki kode sumber dan setidaknya memiliki ide kasar apa yang sedang terjadi. Ini juga membantu Anda untuk memahami JavaByteCode dan .Net IL karena keduanya mirip dengan assembler.
Untuk menjawab pertanyaan ketika Anda memiliki sejumlah kecil kode atau banyak waktu. Paling berguna untuk digunakan dalam chip yang disematkan, di mana kompleksitas chip yang rendah dan persaingan yang buruk dalam kompiler yang menargetkan chip ini dapat memberi keseimbangan bagi manusia. Juga untuk perangkat terbatas, Anda sering berdagang ukuran kode / ukuran memori / kinerja dengan cara yang sulit untuk menginstruksikan kompiler. misalnya saya tahu tindakan pengguna ini tidak sering dipanggil jadi saya akan memiliki ukuran kode kecil dan kinerja buruk, tetapi fungsi lain yang terlihat serupa ini digunakan setiap detik sehingga saya akan memiliki ukuran kode lebih besar dan kinerja lebih cepat. Itu adalah semacam trade off yang bisa digunakan oleh programmer ahli.
Saya juga ingin menambahkan ada banyak jalan tengah di mana Anda dapat kode dalam kompilasi C dan memeriksa Majelis yang dihasilkan, maka baik mengubah kode C Anda atau men-tweak dan mempertahankan sebagai perakitan.
Teman saya bekerja pada pengontrol mikro, saat ini chip untuk mengendalikan motor listrik kecil. Ia bekerja dalam kombinasi level rendah c dan Assembly. Dia pernah mengatakan kepada saya tentang hari yang baik di tempat kerja di mana dia mengurangi loop utama dari 48 instruksi menjadi 43. Dia juga dihadapkan dengan pilihan seperti kode telah tumbuh untuk mengisi chip 256k dan bisnis menginginkan fitur baru, apakah Anda
Saya ingin menambahkan sebagai pengembang komersial dengan cukup portofolio atau bahasa, platform, jenis aplikasi yang saya belum pernah merasa perlu untuk terjun ke perakitan tulisan. Saya selalu menghargai pengetahuan yang saya dapatkan tentang itu. Dan kadang-kadang menyimpang ke dalamnya.
Saya tahu saya telah jauh lebih menjawab pertanyaan "mengapa saya harus belajar assembler" tetapi saya merasa itu adalah pertanyaan yang lebih penting lalu kapan lebih cepat.
jadi mari kita coba sekali lagi. Anda harus berpikir tentang perakitan
Ingatlah untuk membandingkan perakitan Anda dengan kompiler yang dihasilkan untuk melihat mana yang lebih cepat / lebih kecil / lebih baik.
David.
sumber
sbi
dancbi
) yang digunakan oleh kompiler (dan kadang-kadang masih) tidak memanfaatkan sepenuhnya, karena keterbatasan pengetahuan mereka tentang perangkat keras.Saya terkejut tidak ada yang mengatakan ini. The
strlen()
Fungsi jauh lebih cepat jika ditulis dalam perakitan! Di C, hal terbaik yang dapat Anda lakukan adalahsaat berkumpul, Anda dapat mempercepatnya:
panjangnya di ecx. Ini membandingkan 4 karakter sekaligus, jadi ini 4 kali lebih cepat. Dan pikirkan menggunakan kata orde tinggi eax dan ebx, itu akan menjadi 8 kali lebih cepat dari rutin C sebelumnya!
sumber
(word & 0xFEFEFEFF) & (~word + 0x80808080)
nol jika semua byte dalam kata adalah non-nol.Operasi matriks menggunakan instruksi SIMD mungkin lebih cepat daripada kode yang dihasilkan kompiler.
sumber
Saya tidak dapat memberikan contoh spesifik karena sudah bertahun-tahun yang lalu, tetapi ada banyak kasus di mana assembler yang ditulis tangan dapat melakukan out-compiler apa pun. Alasan mengapa:
Anda bisa menyimpang dari memanggil konvensi, menyampaikan argumen dalam register.
Anda dapat dengan hati-hati mempertimbangkan cara menggunakan register, dan menghindari penyimpanan variabel dalam memori.
Untuk hal-hal seperti tabel lompatan, Anda bisa menghindari batas-memeriksa indeks.
Pada dasarnya, kompiler melakukan pekerjaan yang cukup baik untuk mengoptimalkan, dan itu hampir selalu "cukup baik", tetapi dalam beberapa situasi (seperti rendering grafik) di mana Anda membayar mahal untuk setiap siklus tunggal, Anda dapat mengambil jalan pintas karena Anda tahu kode , di mana kompiler tidak bisa karena itu harus di sisi yang aman.
Bahkan, saya telah mendengar beberapa kode rendering grafik di mana suatu rutin, seperti garis-menggambar atau rutinitas mengisi-poligon, benar-benar menghasilkan satu blok kecil kode mesin pada stack dan menjalankannya di sana, untuk menghindari pengambilan keputusan terus menerus tentang gaya garis, lebar, pola, dll.
Yang mengatakan, apa yang saya ingin lakukan kompiler adalah menghasilkan kode perakitan yang baik untuk saya tetapi tidak terlalu pintar, dan mereka kebanyakan melakukannya. Bahkan, salah satu hal yang saya benci tentang Fortran adalah perebutan kode dalam upaya untuk "mengoptimalkan" itu, biasanya tanpa tujuan yang signifikan.
Biasanya, ketika aplikasi memiliki masalah kinerja, itu karena desain yang boros. Hari-hari ini, saya tidak akan merekomendasikan assembler untuk kinerja kecuali aplikasi keseluruhan sudah disetel dalam satu inci dari kehidupannya, masih tidak cukup cepat, dan menghabiskan seluruh waktunya dalam loop batin yang ketat.
Ditambahkan: Saya telah melihat banyak aplikasi yang ditulis dalam bahasa assembly, dan keunggulan kecepatan utama daripada bahasa seperti C, Pascal, Fortran, dll. Adalah karena programmer jauh lebih berhati-hati ketika melakukan pengkodean dalam assembler. Dia akan menulis sekitar 100 baris kode sehari, terlepas dari bahasa, dan dalam bahasa kompiler yang akan sama dengan 3 atau 400 instruksi.
sumber
Beberapa contoh dari pengalaman saya:
Akses ke instruksi yang tidak dapat diakses dari C. Misalnya, banyak arsitektur (seperti x86-64, IA-64, DEC Alpha, dan MIPS atau PowerPC 64-bit) mendukung penggandaan 64 bit demi 64 bit menghasilkan hasil 128 bit. GCC baru-baru ini menambahkan ekstensi yang menyediakan akses ke instruksi tersebut, tetapi sebelum perakitan itu diperlukan. Dan akses ke instruksi ini dapat membuat perbedaan besar pada CPU 64-bit ketika mengimplementasikan sesuatu seperti RSA - terkadang sebanyak faktor 4 peningkatan kinerja.
Akses ke flag khusus CPU. Salah satu yang banyak menggigit saya adalah bendera pembawa; ketika melakukan penambahan presisi ganda, jika Anda tidak memiliki akses ke CPU carry bit, Anda harus membandingkan hasilnya untuk melihat apakah itu meluap, yang membutuhkan 3-5 instruksi lebih banyak per anggota gerak; dan lebih buruk, yang cukup serial dalam hal akses data, yang membunuh kinerja prosesor superscalar modern. Saat memproses ribuan bilangan bulat seperti itu secara berturut-turut, dapat menggunakan addc adalah kemenangan besar (ada masalah superscalar dengan pertikaian pada carry bit juga, tetapi CPU modern menangani dengan cukup baik dengan itu).
SIMD. Bahkan kompiler autovectorizing hanya dapat melakukan kasus-kasus yang relatif sederhana, jadi jika Anda ingin kinerja SIMD yang baik, sayangnya seringkali perlu untuk menulis kode secara langsung. Tentu saja Anda dapat menggunakan intrinsik alih-alih perakitan, tetapi begitu Anda berada di level intrinsik, pada dasarnya Anda menulis perakitan, cukup menggunakan kompiler sebagai pengalokasi register dan (secara nominal) penjadwal instruksi. (Saya cenderung menggunakan intrinsik untuk SIMD hanya karena kompiler dapat menghasilkan prolog fungsi dan yang lainnya untuk saya sehingga saya dapat menggunakan kode yang sama di Linux, OS X, dan Windows tanpa harus berurusan dengan masalah ABI seperti konvensi fungsi panggilan, tetapi yang lain selain itu SSE intrinsik sebenarnya tidak terlalu baik - yang Altivec tampak lebih baik walaupun saya tidak punya banyak pengalaman dengan mereka).bitslicing AES atau koreksi kesalahan SIMD - orang bisa membayangkan kompiler yang dapat menganalisis algoritma dan menghasilkan kode seperti itu, tetapi rasanya bagi saya seperti kompiler pintar setidaknya 30 tahun lagi dari yang ada (yang terbaik).
Di sisi lain, mesin multicore dan sistem terdistribusi telah mengubah banyak kemenangan kinerja terbesar di arah lain - dapatkan tambahan 20% percepatan menulis loop batin Anda dalam perakitan, atau 300% dengan menjalankannya di beberapa core, atau 10000% dengan menjalankannya melintasi sekelompok mesin. Dan tentu saja optimasi tingkat tinggi (hal-hal seperti futures, memoization, dll) seringkali lebih mudah dilakukan dalam bahasa tingkat yang lebih tinggi seperti ML atau Scala daripada C atau asm, dan seringkali dapat memberikan kemenangan kinerja yang jauh lebih besar. Jadi, seperti biasa, ada pengorbanan yang harus dilakukan.
sumber
Loop ketat, seperti saat bermain dengan gambar, karena suatu gambar dapat menghasilkan jutaan piksel. Duduk dan mencari tahu bagaimana memanfaatkan jumlah register prosesor yang terbatas dapat membuat perbedaan. Berikut ini contoh kehidupan nyata:
http://danbystrom.se/2008/12/22/optimizing-away-ii/
Kemudian sering prosesor memiliki beberapa instruksi esoteris yang terlalu khusus untuk dikompilasi dengan kompiler, tetapi kadang-kadang programmer assembler dapat memanfaatkannya. Ambil instruksi XLAT misalnya. Sangat bagus jika Anda perlu melakukan pencarian tabel dalam satu lingkaran dan tabel dibatasi hingga 256 byte!
Diperbarui: Oh, pikirkan saja apa yang paling penting ketika kita berbicara tentang loop secara umum: kompilator sering tidak tahu berapa banyak iterasi yang akan menjadi kasus umum! Hanya programmer yang tahu bahwa sebuah loop akan diulang berkali-kali dan oleh karena itu akan bermanfaat untuk mempersiapkan loop dengan beberapa pekerjaan tambahan, atau jika itu akan diulangi beberapa kali sehingga set-up sebenarnya akan memakan waktu lebih lama daripada iterasi diharapkan.
sumber
Lebih sering daripada yang Anda pikirkan, C perlu melakukan hal-hal yang tampaknya tidak perlu dari sudut pandang pembuat kode Majelis hanya karena standar C mengatakannya.
Promosi integer, misalnya. Jika Anda ingin menggeser variabel char di C, orang biasanya berharap bahwa kode akan melakukan hal itu, satu bit shift.
Standar, bagaimanapun, memaksa kompiler untuk melakukan perpanjangan tanda ke int sebelum shift dan memotong hasilnya menjadi char sesudahnya yang mungkin menyulitkan kode tergantung pada arsitektur prosesor target.
sumber
Anda tidak benar-benar tahu apakah kode C yang Anda tulis benar-benar cepat jika Anda belum melihat pembongkaran apa yang dihasilkan kompiler. Banyak kali Anda melihatnya dan melihat bahwa "tulisan yang baik" itu subjektif.
Jadi tidak perlu menulis assembler untuk mendapatkan kode tercepat, tetapi tentu saja layak untuk mengetahui assembler karena alasan yang sama.
sumber
Saya telah membaca semua jawaban (lebih dari 30) dan tidak menemukan alasan sederhana: assembler lebih cepat daripada C jika Anda telah membaca dan mempraktikkan Manual Referensi Optimasi Arsitektur Intel® 64 dan IA-32 , jadi alasan mengapa perakitan mungkin lebih lambat adalah bahwa orang-orang yang menulis perakitan lambat seperti itu tidak membaca Manual Pengoptimalan .
Di masa lalu yang baik dari Intel 80286, setiap instruksi dieksekusi pada jumlah tetap siklus CPU, tetapi sejak Pentium Pro, dirilis pada tahun 1995, prosesor Intel menjadi superscalar, menggunakan Pipelining Kompleks: Eksekusi Out-of-Order & Pengubahan Daftar. Sebelum itu, pada Pentium, diproduksi tahun 1993, ada jalur pipa U dan V: jalur pipa ganda yang dapat mengeksekusi dua instruksi sederhana pada satu siklus clock jika mereka tidak saling bergantung; tapi ini bukan apa-apa untuk membandingkan apa yang Eksekusi Out-of-Order & Daftar Ganti nama muncul di Pentium Pro, dan hampir tidak berubah saat ini.
Untuk menjelaskan dalam beberapa kata, kode tercepat adalah di mana instruksi tidak bergantung pada hasil sebelumnya, misalnya Anda harus selalu menghapus seluruh register (dengan movzx) atau menggunakan
add rax, 1
sebagai gantinya atauinc rax
untuk menghapus ketergantungan pada keadaan bendera sebelumnya, dll.Anda dapat membaca lebih lanjut tentang Eksekusi Out-of-Order & Mengganti Nama Registrasi jika waktu mengizinkan, ada banyak informasi yang tersedia di Internet.
Ada juga masalah penting lainnya seperti prediksi cabang, jumlah unit muat dan toko, jumlah gerbang yang menjalankan operasi mikro, dll, tetapi hal yang paling penting untuk dipertimbangkan adalah Eksekusi Di Luar Pesanan.
Kebanyakan orang tidak mengetahui tentang Eksekusi Out-of-Order, sehingga mereka menulis program perakitan mereka seperti untuk 80286, berharap instruksi mereka akan membutuhkan waktu yang tetap untuk dieksekusi terlepas dari konteksnya; sementara kompiler C mengetahui Eksekusi Out-of-Order dan menghasilkan kode dengan benar. Itu sebabnya kode orang yang tidak sadar itu lebih lambat, tetapi jika Anda menyadari, kode Anda akan lebih cepat.
sumber
Saya pikir kasus umum ketika assembler lebih cepat adalah ketika programmer perakitan pintar melihat output kompiler dan mengatakan "ini adalah jalur kritis untuk kinerja dan saya bisa menulis ini agar lebih efisien" dan kemudian orang itu mengubah assembler atau menulis ulang itu dari awal.
sumber
Itu semua tergantung pada beban kerja Anda.
Untuk operasi sehari-hari, C dan C ++ baik-baik saja, tetapi ada beban kerja tertentu (setiap transformasi yang melibatkan video (kompresi, dekompresi, efek gambar, dll)) yang cukup banyak membutuhkan perakitan untuk tampil.
Mereka juga biasanya melibatkan penggunaan ekstensi chipset khusus CPU (MME / MMX / SSE / apa pun) yang disesuaikan untuk jenis operasi tersebut.
sumber
Saya memiliki operasi transposisi bit yang perlu dilakukan, pada 192 atau 256 bit setiap interupsi, yang terjadi setiap 50 mikrodetik.
Ini terjadi oleh peta tetap (kendala perangkat keras). Menggunakan C, butuh sekitar 10 mikrodetik untuk membuatnya. Ketika saya menerjemahkan ini ke Assembler, dengan mempertimbangkan fitur spesifik dari peta ini, caching register spesifik, dan menggunakan operasi berorientasi bit; butuh kurang dari 3,5 mikrodetik untuk melakukan.
sumber
Mungkin layak untuk melihat Mengoptimalkan Immutable dan Purity oleh Walter Bright itu bukan tes yang diprofilkan tetapi menunjukkan kepada Anda satu contoh yang baik dari perbedaan antara ASM yang dibuat dengan tulisan tangan dan kompiler. Walter Bright menulis optimizer compiler sehingga mungkin ada baiknya melihat posting blog lainnya.
sumber
LInux assembly howto , menanyakan pertanyaan ini dan memberikan pro dan kontra untuk menggunakan assembly.
sumber
Jawaban sederhana ... Seseorang yang mengenal perakitan dengan baik (alias memiliki referensi di sampingnya, dan memanfaatkan setiap cache prosesor dan fitur pipa dll) dijamin dapat menghasilkan kode yang jauh lebih cepat daripada kompiler mana pun .
Namun perbedaannya akhir-akhir ini tidak masalah dalam aplikasi tipikal.
sumber
Salah satu kemungkinan untuk versi CP / M-86 PolyPascal (sibling to Turbo Pascal) adalah untuk mengganti fasilitas "use-bios-to-output-karakter-ke-layar" dengan rutinitas bahasa mesin yang pada dasarnya diberi x, dan y, dan string untuk diletakkan di sana.
Ini memungkinkan untuk memperbarui layar lebih cepat dari sebelumnya!
Ada ruang dalam biner untuk menanamkan kode mesin (beberapa ratus byte) dan ada hal-hal lain di sana juga, jadi penting untuk memeras sebanyak mungkin.
Ternyata karena layarnya 80x25, masing-masing koordinat bisa muat dalam satu byte, sehingga keduanya bisa masuk dalam kata dua-byte. Ini memungkinkan untuk melakukan perhitungan yang diperlukan dalam lebih sedikit byte karena satu penambahan dapat memanipulasi kedua nilai secara bersamaan.
Sepengetahuan saya tidak ada kompiler C yang dapat menggabungkan beberapa nilai dalam register, lakukan instruksi SIMD pada mereka dan bagi lagi nanti (dan saya pikir instruksi mesin tidak akan lebih pendek lagi).
sumber
Salah satu cuplikan perakitan yang lebih terkenal adalah dari loop pemetaan tekstur Michael Abrash ( dijelaskan secara rinci di sini ):
Saat ini kebanyakan kompiler mengekspresikan instruksi khusus CPU tingkat lanjut sebagai intrinsik, yaitu fungsi yang dikompilasi ke instruksi aktual. MS Visual C ++ mendukung intrinsik untuk MMX, SSE, SSE2, SSE3, dan SSE4, jadi Anda tidak perlu terlalu khawatir untuk drop down ke perakitan untuk mengambil keuntungan dari instruksi spesifik platform. Visual C ++ juga dapat memanfaatkan arsitektur aktual yang Anda targetkan dengan pengaturan / ARCH yang sesuai.
sumber
Mengingat programmer yang tepat, program Assembler selalu dapat dibuat lebih cepat daripada rekan C mereka (setidaknya sedikit). Akan sulit untuk membuat program C di mana Anda tidak bisa mengeluarkan setidaknya satu instruksi Assembler.
sumber
http://cr.yp.to/qhasm.html memiliki banyak contoh.
sumber
gcc telah menjadi kompiler yang banyak digunakan. Optimalisasi secara umum tidak begitu baik. Jauh lebih baik daripada assembler menulis programmer rata-rata, tetapi untuk kinerja nyata, tidak baik. Ada kompiler yang sangat luar biasa dalam kode yang mereka hasilkan. Jadi sebagai jawaban umum akan ada banyak tempat di mana Anda dapat pergi ke output dari kompiler dan men-tweak assembler untuk kinerja, dan / atau hanya menulis ulang rutin dari awal.
sumber
Longpoke, hanya ada satu batasan: waktu. Ketika Anda tidak memiliki sumber daya untuk mengoptimalkan setiap perubahan tunggal untuk kode dan menghabiskan waktu Anda mengalokasikan register, mengoptimalkan beberapa tumpahan dan yang tidak, kompiler akan menang setiap waktu. Anda melakukan modifikasi pada kode, mengkompilasi ulang dan mengukur. Ulangi jika perlu.
Juga, Anda dapat melakukan banyak hal di sisi level tinggi. Juga, memeriksa rakitan yang dihasilkan dapat memberikan IMPRESI bahwa kode itu omong kosong, tetapi dalam praktiknya akan berjalan lebih cepat daripada yang Anda pikir akan lebih cepat. Contoh:
int y = data [i]; // lakukan beberapa hal di sini .. call_function (y, ...);
Compiler akan membaca data, mendorongnya ke stack (spill) dan kemudian membaca dari stack dan lulus sebagai argumen. Kedengarannya shite? Ini mungkin sebenarnya kompensasi latensi yang sangat efektif dan menghasilkan runtime yang lebih cepat.
// fungsi call_fungsi versi yang dioptimalkan (data [i], ...); // bagaimanapun juga tidak dioptimalkan ..
Gagasan dengan versi yang dioptimalkan adalah, bahwa kami telah mengurangi tekanan register dan menghindari tumpah. Tapi sebenarnya, versi "shitty" lebih cepat!
Melihat kode perakitan, hanya melihat instruksi dan menyimpulkan: lebih banyak instruksi, lebih lambat, akan menjadi salah penilaian.
Hal yang perlu diperhatikan di sini adalah: banyak pakar perakitan berpikir mereka tahu banyak, tetapi hanya tahu sedikit. Aturan berubah dari arsitektur ke yang berikutnya juga. Tidak ada kode x86 silver-bullet, misalnya, yang selalu tercepat. Hari-hari ini lebih baik dilakukan dengan aturan praktis:
Juga, terlalu mempercayai kompiler secara ajaib mengubah kode C / C ++ yang kurang dipikirkan menjadi kode yang "secara teoritis optimal" adalah pemikiran yang penuh harapan. Anda harus mengetahui kompiler dan rantai alat yang Anda gunakan jika Anda peduli tentang "kinerja" di level rendah ini.
Kompiler dalam C / C ++ umumnya tidak terlalu bagus dalam memesan ulang sub-ekspresi karena fungsinya memiliki efek samping, sebagai permulaan. Bahasa fungsional tidak menderita dari peringatan ini tetapi tidak cocok dengan ekosistem saat ini dengan baik. Ada opsi kompiler untuk memungkinkan aturan presisi yang longgar yang memungkinkan urutan operasi diubah oleh kompiler / penghubung / pembuat kode.
Topik ini sedikit buntu; untuk sebagian besar itu tidak relevan, dan sisanya, mereka tahu apa yang sudah mereka lakukan.
Semuanya bermuara pada ini: "untuk memahami apa yang Anda lakukan", itu sedikit berbeda dari mengetahui apa yang Anda lakukan.
sumber