Kapan perakitan lebih cepat dari C?


475

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.


17
sebenarnya cukup sepele untuk memperbaiki kode yang dikompilasi. Siapa pun yang memiliki pengetahuan yang kuat tentang bahasa assembly dan C dapat melihat ini dengan memeriksa kode yang dihasilkan. Yang mudah adalah tebing kinerja pertama yang Anda gagal ketika Anda kehabisan register sekali pakai dalam versi yang dikompilasi. Rata-rata kompiler akan melakukan jauh lebih baik daripada manusia untuk proyek besar, tetapi tidak sulit dalam proyek berukuran layak untuk menemukan masalah kinerja dalam kode yang dikompilasi.
old_timer

14
Sebenarnya, jawaban singkatnya adalah: Assembler selalu lebih cepat atau sama dengan kecepatan C. Alasannya adalah bahwa Anda dapat memiliki perakitan tanpa C, tetapi Anda tidak dapat memiliki C tanpa perakitan (dalam bentuk biner, yang kami di hari yang disebut "kode mesin"). Yang mengatakan, jawaban panjangnya adalah: C Compiler cukup bagus dalam mengoptimalkan dan "berpikir" tentang hal-hal yang biasanya tidak Anda pikirkan, jadi itu benar-benar tergantung pada keterampilan Anda, tetapi biasanya Anda selalu dapat mengalahkan kompiler C; masih hanya perangkat lunak yang tidak dapat berpikir dan mendapatkan ide. Anda juga dapat menulis assembler portabel jika Anda menggunakan makro dan Anda sabar.

11
Saya sangat tidak setuju bahwa jawaban untuk pertanyaan ini harus "berdasarkan pendapat" - mereka bisa sangat objektif - itu bukan sesuatu seperti mencoba membandingkan kinerja bahasa hewan peliharaan favorit, yang masing-masing akan memiliki poin kuat dan menarik kembali. Ini adalah masalah memahami seberapa jauh kompiler dapat membawa kita, dan dari titik mana lebih baik untuk mengambil alih.
jsbueno

21
Sebelumnya dalam karir saya, saya menulis banyak C dan assembler mainframe di sebuah perusahaan perangkat lunak. Salah satu rekan saya adalah apa yang saya sebut "assembler purist" (semuanya harus assembler), jadi saya yakin dia bisa menulis rutinitas tertentu yang berjalan lebih cepat dalam C daripada apa yang bisa dia tulis dalam assembler. Saya menang. Tetapi yang terpenting, setelah saya menang, saya mengatakan kepadanya bahwa saya menginginkan taruhan kedua - bahwa saya bisa menulis sesuatu yang lebih cepat di assembler daripada program C yang mengalahkannya pada taruhan sebelumnya. Saya menang juga, membuktikan bahwa sebagian besar turun ke keterampilan dan kemampuan programmer lebih dari yang lain.
Valerie R

3
Kecuali otak Anda memiliki -O3flag, Anda mungkin lebih baik meninggalkan optimasi ke kompiler C :-)
paxdiablo

Jawaban:


272

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

  • Mendapatkan bagian tinggi dari penggandaan integer 64 bit : Versi portabel yang digunakan uint64_tuntuk 32x32 => Penggandaan 64-bit gagal untuk mengoptimalkan pada CPU 64-bit, jadi Anda memerlukan intrinsik atau __int128kode efisien pada sistem 64-bit.
  • _umul128 pada Windows 32 bit : MSVC tidak selalu melakukan pekerjaan dengan baik ketika mengalikan bilangan bulat 32-bit dilemparkan ke 64, jadi intrinsik banyak membantu.

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:

// on a 32-bit machine, int can hold 32-bit fixed-point integers.
int inline FixedPointMul (int a, int b)
{
  long long a_long = a; // cast to 64 bit.

  long long product = a_long * b; // perform multiplication

  return (int) (product >> 16);  // shift by the fixed point bias
}

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:

int inline FixedPointMul (int a, int b)
{
    return (int) __ll_rshift(__emul(a,b),16);
}

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 popcntatau bit-scan untuk menemukan bit set pertama atau terakhir . (POSIX memiliki ffs()fungsi, tetapi semantiknya tidak cocok dengan x86 bsf/ 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 popcntinstruksi (jika diaktifkan pada waktu kompilasi), tetapi jauh lebih dapat diandalkan untuk digunakan __builtin_popcntdi GNU C, atau pada x86 jika Anda hanya menargetkan perangkat keras dengan SSE4.2: _mm_popcnt_u32dari<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, ntohldapat dikompilasi ke bswap(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.


6
Bagaimana dengan hal-hal seperti {x = c% d; y = c / d;}, apakah kompiler cukup pintar untuk membuat satu div atau idiv?
Jens Björnhager

4
Sebenarnya, kompiler yang baik akan menghasilkan kode optimal dari fungsi pertama. Mengaburkan kode sumber dengan intrinsik atau perakitan inline dengan benar-benar tidak bermanfaat bukanlah hal terbaik untuk dilakukan.
slacker

65
Halo Slacker, saya pikir Anda belum pernah bekerja pada kode waktu-kritis sebelum ... perakitan inline dapat membuat * perbedaan besar. Juga untuk kompiler, intrinsik sama dengan aritmatika normal dalam C. Itulah intinya dalam intrinsik. Mereka membiarkan Anda menggunakan fitur arsitektur tanpa harus berurusan dengan kekurangannya.
Nils Pipenbrinck

6
@slacker Sebenarnya, kode di sini cukup mudah dibaca: kode inline melakukan satu operasi unik, yang langsung dapat dimengerti dengan membaca tanda tangan metode. Kode hilang hanya secara perlahan dalam readibility ketika instruksi yang tidak jelas digunakan. Yang penting di sini adalah kita memiliki metode yang hanya melakukan satu operasi yang dapat diidentifikasi dengan jelas, dan itu benar-benar cara terbaik untuk menghasilkan kode yang dapat dibaca fungsi-fungsi atomik ini. By the way, ini tidak begitu mengaburkan komentar kecil seperti / * (a * b) >> 16 * / tidak bisa segera menjelaskannya.
Dereckson

5
Agar adil, ini adalah contoh yang buruk, setidaknya hari ini. Kompiler C telah lama dapat melakukan 32x32 -> 64 kali bahkan jika bahasa tidak menawarkannya secara langsung: mereka mengakui bahwa ketika Anda melemparkan argumen 32-bit ke 64-bit dan kemudian mengalikannya, ia tidak perlu lakukan kalikan 64-bit penuh, tetapi 32x32 -> 64 akan baik-baik saja. Saya memeriksa dan semua dentang, gcc, dan MSVC dalam versi mereka saat ini mendapatkan hak ini . Ini bukan hal baru - saya ingat melihat output kompiler dan memperhatikan ini satu dekade yang lalu.
BeeOnRope

143

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.


3
Ya itu adalah sistem monokrom satu bit, khususnya itu adalah blok gambar monokrom pada Atari ST.
lilburne

16
Apakah kompiler pengoptimalisasi mengkompilasi program asli atau versi Anda?
Thorbjørn Ravn Andersen

Pada prosesor apa? Pada 8086, saya berharap kode optimal untuk rotasi 8x8 akan memuat DI dengan 16 bit data menggunakan SI, ulangi add di,di / adc al,al / add di,di / adc ah,ahdll. 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.
supercat

1
Saya benar-benar tidak bisa memikirkan platform mana pun di mana kompiler akan cenderung masuk dalam satu atau dua kode optimal untuk rotasi 8x8.
supercat

65

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):

#include "stdafx.h"
#include <windows.h>

float KahanSum(const float *data, int n)
{
   float sum = 0.0f, C = 0.0f, Y, T;

   for (int i = 0 ; i < n ; ++i) {
      Y = *data++ - C;
      T = sum + Y;
      C = T - sum - Y;
      sum = T;
   }

   return sum;
}

float AsmSum(const float *data, int n)
{
  float result = 0.0f;

  _asm
  {
    mov esi,data
    mov ecx,n
    fldz
    fldz
l1:
    fsubr [esi]
    add esi,4
    fld st(0)
    fadd st(0),st(2)
    fld st(0)
    fsub st(0),st(3)
    fsub st(0),st(2)
    fstp st(2)
    fstp st(2)
    loop l1
    fstp result
    fstp result
  }

  return result;
}

int main (int, char **)
{
  int count = 1000000;

  float *source = new float [count];

  for (int i = 0 ; i < count ; ++i) {
    source [i] = static_cast <float> (rand ()) / static_cast <float> (RAND_MAX);
  }

  LARGE_INTEGER start, mid, end;

  float sum1 = 0.0f, sum2 = 0.0f;

  QueryPerformanceCounter (&start);

  sum1 = KahanSum (source, count);

  QueryPerformanceCounter (&mid);

  sum2 = AsmSum (source, count);

  QueryPerformanceCounter (&end);

  cout << "  C code: " << sum1 << " in " << (mid.QuadPart - start.QuadPart) << endl;
  cout << "asm code: " << sum2 << " in " << (end.QuadPart - mid.QuadPart) << endl;

  return 0;
}

Dan beberapa nomor dari PC saya yang menjalankan rilis rilis bawaan * :

  C code: 500137 in 103884668
asm code: 500137 in 52129147

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.


20
Atau di GCC, Anda dapat melepaskan tangan kompilator pada optimasi floating point (selama Anda berjanji untuk tidak melakukan apa pun dengan infinities atau NaNs) dengan menggunakan flag -ffast-math. Mereka memiliki tingkat optimisasi, -Ofastyang 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).
David Stone

2
Ya, mengapung tidak komutatif, kompiler harus melakukan PERSIS apa yang Anda tulis, pada dasarnya apa yang @DavidStone katakan.
Alec Teal

2
Apakah Anda mencoba matematika SSE? Performa adalah salah satu alasan MS meninggalkan x87 sepenuhnya di x86_64 dan double 80-bit panjang di x86
phuclv

4
@Praxeolitic: Tambah FP komutatif ( a+b == b+a), tetapi tidak asosiatif (penataan ulang operasi, jadi pembulatan perantara berbeda). re: kode ini: Saya tidak berpikir x87 uncommented dan loopinstruksi adalah demonstrasi yang sangat mengagumkan dari asm cepat. looptampaknya bukan hambatan karena latensi FP. Saya tidak yakin apakah dia sedang melakukan pipeline operasi FP atau tidak; x87 sulit bagi manusia untuk membaca. Dua fstp resultsinsn pada akhirnya jelas tidak optimal. Memunculkan hasil tambahan dari tumpukan akan lebih baik dilakukan dengan non-toko. Seperti fstp st(0)IIRC.
Peter Cordes

2
@PeterCordes: Konsekuensi yang menarik dari membuat komutatif tambahan adalah bahwa sementara 0 + x dan x + 0 setara satu sama lain, tidak selalu sama dengan x.
supercat

58

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.


Secara umum, pernyataan ini benar. Compiler melakukan yang terbaik untuk DWIW, tetapi dalam beberapa kasus tepi assembler coding tangan menyelesaikan pekerjaan ketika kinerja realtime adalah suatu keharusan.
spoulson

1
@Liedman: "ia dapat mencoba menyusun ulang instruksi lebih cepat daripada yang manusia". OCaml dikenal cepat dan, anehnya, kompiler kode-asli ocamloptmelompati penjadwalan instruksi pada x86 dan, sebaliknya, menyerahkannya ke CPU karena dapat menyusun ulang lebih efektif pada saat run-time.
Jon Harrop

1
Kompiler modern melakukan banyak hal, dan itu akan memakan waktu terlalu lama untuk dilakukan dengan tangan, tetapi kompiler itu tidak mendekati sempurna. Cari pelacak bug gcc atau llvm untuk menemukan bug "optimasi yang tidak terjawab". Ada banyak. Juga, ketika menulis dalam asm, Anda dapat lebih mudah memanfaatkan prasyarat seperti "input ini tidak boleh negatif" yang akan sulit dibuktikan oleh kompiler.
Peter Cordes

48

Dalam pekerjaan saya, ada tiga alasan bagi saya untuk mengetahui dan menggunakan perakitan. Dalam urutan kepentingan:

  1. 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.

  2. 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:

    for (int y=0; y < imageHeight; y++) {
        for (int x=0; x < imageWidth; x++) {
           // do something
        }
    }

    "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.

  3. 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.


Anda tidak memasang loop? :-)
Jon Harrop

1
@plinth: bagaimana maksud Anda "menggores siklus"?
lang2

@ lang2: itu berarti menghilangkan sebanyak mungkin waktu yang dihabiskan di loop dalam sebanyak mungkin - apa pun yang tidak berhasil dikompilasi oleh kompiler, yang mungkin termasuk menggunakan aljabar untuk mengangkat kelipatan dari satu loop untuk menjadikannya sebagai tambahan di batin, dll.
alas

1
Ubin lingkaran tampaknya tidak diperlukan jika Anda hanya melakukan satu kali melewati data.
James M. Lay

@ JamesM.Lay: Jika Anda hanya menyentuh setiap elemen sekali saja, tatanan traversal yang lebih baik dapat memberi Anda lokalitas spasial. (mis. gunakan semua byte dari garis cache yang Anda sentuh, alih-alih perulangan kolom dari matriks menggunakan satu elemen per baris cache.)
Peter Cordes

42

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.


4
+1, meskipun kalimat terakhir tidak benar-benar termasuk dalam diskusi ini - orang akan menganggap bahwa assembler berperan hanya setelah semua kemungkinan perbaikan algoritma dll telah direalisasikan.
mghie

18
@Matt: ASM yang ditulis tangan seringkali jauh lebih baik pada beberapa CPU EE kecil yang bekerja dengan yang memiliki dukungan kompiler vendor jelek.
Zan Lynx

5
"Hanya ketika menggunakan beberapa set instruksi tujuan khusus" ?? Anda mungkin belum pernah menulis kode asm yang dioptimalkan dengan tangan sebelumnya. Pengetahuan yang cukup akrab tentang arsitektur yang sedang Anda kerjakan memberi peluang bagus bagi Anda untuk menghasilkan kode (ukuran dan kecepatan) yang lebih baik daripada kompiler Anda. Jelas, seperti yang dikomentari @mghie, Anda selalu mulai mengkode algos terbaik yang bisa Anda gunakan untuk masalah Anda. Bahkan untuk kompiler yang sangat baik, Anda benar-benar harus menulis kode C dengan cara yang mengarahkan kompiler ke kode kompilasi terbaik. Jika tidak, kode yang dihasilkan akan kurang optimal.
ysap

2
@ysap - pada komputer aktual (bukan chip tertanam yang kurang bertenaga kecil) dalam penggunaan di dunia nyata, kode "optimal" tidak akan lebih cepat karena untuk setiap kumpulan data besar kinerja Anda akan dibatasi oleh akses memori dan kesalahan halaman ( dan jika Anda tidak memiliki set data yang besar, ini akan menjadi cepat dan tidak ada gunanya mengoptimalkannya) - hari-hari saya bekerja sebagian besar di C # (bahkan tidak c) dan kinerja meningkat dari manajer memori pemadatan out- bobot overhead pengumpulan sampah, pemadatan dan dan kompilasi JIT.
Nir

4
1 untuk menyatakan bahwa kompiler (khususnya JIT) dapat melakukan pekerjaan yang lebih baik daripada manusia, jika mereka dioptimalkan untuk perangkat keras yang mereka jalankan.
Sebastian

38

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:

  1. 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:

    int16_t x, y;
    // int16_t is a typedef for "short"
    // set x and y to something
    int16_t prod = (int16_t)(((int32_t)x*y)>>16);`

    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.

  2. Operasi bitshifting tertentu (rotasi / membawa):

    // 256-bit array shifted right in its entirety:
    uint8_t x[32];
    for (int i = 32; --i > 0; )
    {
       x[i] = (x[i] >> 1) | (x[i-1] << 7);
    }
    x[0] >>= 1;

    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.


23

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):

Bahasa Pemrograman C - Bahasa yang menggabungkan fleksibilitas bahasa assembly dengan kekuatan bahasa assembly.

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).


Ada banyak orang yang menggunakan assembler ARM sebagai bahasa pilihan pada mesin Acorn (awal 90-an). IIRC mereka mengatakan bahwa set instruksi risc kecil membuatnya lebih mudah dan lebih menyenangkan. Tapi saya menduga itu karena kompiler C adalah kedatangan terlambat untuk Acorn, dan kompiler C ++ tidak pernah selesai.
Andrew M

3
"... karena tidak ada standar subyektif untuk C." Maksudmu objektif .
Thomas

@AndrewM: Ya, saya menulis aplikasi bahasa campuran di BASIC dan assembler ARM selama sekitar 10 tahun. Saya belajar C selama waktu itu tetapi tidak terlalu berguna karena sama rumitnya dengan assembler dan lebih lambat. Norcroft melakukan beberapa optimasi luar biasa tapi saya pikir set instruksi bersyarat adalah masalah bagi para penyusun hari itu.
Jon Harrop

1
@AndrewM: yah, sebenarnya ARM adalah jenis RISC yang dilakukan mundur. SPA RISC lainnya dirancang dimulai dengan apa yang akan digunakan oleh kompiler. ARM ISA tampaknya telah dirancang mulai dari yang disediakan CPU (barrel shifter, flag kondisi → mari kita paparkan mereka dalam setiap instruksi).
ninjalj

16

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.


Amiga tidak memiliki 16 MB chip RAM, lebih seperti 512 kB hingga 2 MB tergantung pada chipset. Juga, banyak game Amiga tidak bekerja dengan CPU yang lebih cepat karena teknik seperti yang Anda gambarkan.
bk1e

1
@ bk1e - Amiga memproduksi berbagai macam model komputer, Amiga 500 dikirimkan dengan ram 512K yang diperluas hingga 1Meg dalam kasus saya. amigahistory.co.uk/amiedevsys.html adalah amiga dengan 128Meg Ram
David Waters

@ bk1e: Saya dikoreksi. Memori saya mungkin gagal, tetapi bukankah chip RAM terbatas pada ruang alamat 24bit pertama (yaitu 16MB)? Dan Fast dipetakan di atas itu?
Aaron Digulla

@ Harun Digulla: Wikipedia memiliki info lebih lanjut tentang perbedaan antara chip / cepat / lambat RAM: en.wikipedia.org/wiki/Amiga_Chip_RAM
bk1e

@ bk1e: Kesalahan saya. CPU 68k hanya memiliki 24 jalur alamat, itu sebabnya saya memiliki 16MB di kepala saya.
Aaron Digulla

15

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

  1. Hapus fitur yang ada
  2. Kurangi ukuran beberapa atau semua fitur yang ada mungkin dengan mengorbankan kinerja.
  3. Advokasi pindah ke chip yang lebih besar dengan biaya yang lebih tinggi, konsumsi daya yang lebih tinggi dan faktor bentuk yang lebih besar.

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

  • bekerja pada fungsi sistem operasi tingkat rendah
  • Bekerja pada kompiler.
  • Bekerja pada chip yang sangat terbatas, sistem tertanam dll

Ingatlah untuk membandingkan perakitan Anda dengan kompiler yang dihasilkan untuk melihat mana yang lebih cepat / lebih kecil / lebih baik.

David.


4
+1 untuk mempertimbangkan aplikasi yang tertanam pada chip kecil. Terlalu banyak insinyur perangkat lunak di sini yang tidak mempertimbangkan tertanam atau berpikir itu berarti ponsel pintar (32 bit, RAM MB, MB flash).
Martin

1
Aplikasi waktu tertanam adalah contoh yang bagus! Seringkali ada instruksi aneh (bahkan yang benar-benar sederhana seperti avr's sbidan cbi) yang digunakan oleh kompiler (dan kadang-kadang masih) tidak memanfaatkan sepenuhnya, karena keterbatasan pengetahuan mereka tentang perangkat keras.
felixphew

15

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 adalah

int c;
for(c = 0; str[c] != '\0'; c++) {}

saat berkumpul, Anda dapat mempercepatnya:

mov esi, offset string
mov edi, esi
xor ecx, ecx

lp:
mov ax, byte ptr [esi]
cmp al, cl
je  end_1
cmp ah, cl
je end_2
mov bx, byte ptr [esi + 2]
cmp bl, cl
je end_3
cmp bh, cl
je end_4
add esi, 4
jmp lp

end_4:
inc esi

end_3:
inc esi

end_2:
inc esi

end_1:
inc esi

mov ecx, esi
sub ecx, edi

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!


3
Bagaimana hal ini dibandingkan dengan yang ada di strchr.nfshost.com/optimized_strlen_function ?
ninjalj

@ninjalj: mereka adalah hal yang sama :) saya tidak berpikir itu bisa dilakukan dengan cara ini di C. Saya pikir ini bisa sedikit ditingkatkan
BlackBear

Masih ada operasi DAN bitwise sebelum setiap perbandingan dalam kode C. Mungkin saja kompiler akan cukup pintar untuk menguranginya menjadi perbandingan byte tinggi dan rendah, tapi saya tidak akan bertaruh uang untuk itu. Sebenarnya ada algoritma loop lebih cepat yang didasarkan pada properti yang (word & 0xFEFEFEFF) & (~word + 0x80808080)nol jika semua byte dalam kata adalah non-nol.
user2310967

@MichaWiedenmann benar, saya harus memuat bx setelah membandingkan dua karakter dalam kapak. Terima kasih
BlackBear

14

Operasi matriks menggunakan instruksi SIMD mungkin lebih cepat daripada kode yang dihasilkan kompiler.


Beberapa kompiler (VectorC, jika saya ingat dengan benar) menghasilkan kode SIMD, jadi bahkan itu mungkin tidak lagi menjadi argumen untuk menggunakan kode assembly.
OregonGhost

Compiler membuat kode sadar SSE, sehingga argumen itu tidak benar
vartec

5
Untuk banyak dari situasi tersebut, Anda dapat menggunakan SSE intrisics alih-alih perakitan. Ini akan membuat kode Anda lebih portabel (gcc visual c ++, 64bit, 32bit dll) dan Anda tidak perlu melakukan alokasi register.
Laserallan

1
Tentu Anda mau, tetapi pertanyaannya tidak bertanya di mana saya harus menggunakan perakitan, bukan C. Dikatakan ketika C compiler tidak menghasilkan kode yang lebih baik. Saya mengasumsikan sumber C yang tidak menggunakan panggilan SSE langsung atau perakitan inline.
Mehrdad Afshari

9
Tapi Mehrdad benar. Mendapatkan SSE dengan benar cukup sulit untuk kompiler dan bahkan dalam situasi yang jelas (untuk manusia, yaitu) kebanyakan kompiler tidak menggunakannya.
Konrad Rudolph

13

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.


8
+1: "Anda bisa menyimpang dari memanggil konvensi". Kompiler C / C ++ cenderung payah dalam mengembalikan beberapa nilai. Mereka sering menggunakan bentuk sret di mana penelepon tumpukan mengalokasikan blok yang berdekatan untuk sebuah struct dan memberikan referensi kepadanya untuk callee untuk mengisinya. Mengembalikan beberapa nilai dalam register beberapa kali lebih cepat.
Jon Harrop

1
@Jon: C / C ++ compiler melakukannya dengan baik ketika fungsi mendapat inline (fungsi non-inline harus sesuai dengan ABI, ini bukan batasan C dan C ++ tetapi model yang menghubungkan)
Ben Voigt


2
Saya tidak melihat ada panggilan fungsi yang masuk ke sana.
Ben Voigt

13

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.


2
@ Dennis itulah sebabnya saya menulis 'Tentu saja Anda dapat menggunakan intrinsik alih-alih perakitan, tetapi begitu Anda berada di level intrinsik, pada dasarnya Anda menulis majelis, hanya menggunakan kompiler sebagai pengalokasi register dan (secara nominal) penjadwal instruksi.'
Jack Lloyd

Juga, kode SIMD berbasis intrinsik cenderung kurang dapat dibaca daripada kode yang sama yang ditulis dalam assembler: Banyak kode SIMD bergantung pada reinterpretasi implisit data dalam vektor, yang merupakan PITA yang harus dilakukan dengan tipe data yang disediakan oleh kompilator intrinsik.
cmaster - mengembalikan monica

10

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.


3
Pengoptimalan terarah profil memberikan informasi kompilator tentang seberapa sering loop digunakan.
Zan Lynx

10

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.


Kompiler berkualitas untuk mikroskopi kecil telah bertahun-tahun dapat menghindari pemrosesan bagian atas dari nilai-nilai dalam kasus di mana hal tersebut tidak pernah dapat secara signifikan mempengaruhi hasil. Aturan promosi memang menimbulkan masalah, tetapi paling sering dalam kasus di mana kompiler tidak memiliki cara untuk mengetahui kasus sudut mana yang dan tidak relevan.
supercat

9

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.


2
"Jadi tidak perlu menulis di assembler untuk mendapatkan kode tercepat yang pernah" Yah, saya belum pernah melihat kompiler melakukan hal optimal dalam hal apa pun yang tidak sepele. Manusia yang berpengalaman dapat melakukan lebih baik daripada kompiler dalam hampir semua kasus. Jadi, sangat penting untuk menulis assembler untuk mendapatkan "kode tercepat yang pernah ada".
cmaster - mengembalikan monica

@ cmaster Dalam pengalaman saya, keluaran kompiler baik-baik saja, acak. Terkadang itu sangat bagus dan optimal dan kadang-kadang "bagaimana sampah ini bisa dipancarkan".
sharptooth

9

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, 1sebagai 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.


8

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.


7

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.


6

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.




5

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.


1
Anda lupa mengatakan "diberi banyak waktu dan usaha", dan "menciptakan mimpi buruk pemeliharaan". Seorang kolega saya sedang berupaya mengoptimalkan bagian kritis-kinerja dari kode OS, dan ia bekerja di C lebih dari sekadar perakitan, karena memungkinkannya menyelidiki dampak kinerja dari perubahan tingkat tinggi dalam jangka waktu yang masuk akal.
Artelius

Saya setuju. Terkadang Anda menggunakan makro dan skrip untuk menghasilkan kode perakitan untuk menghemat waktu dan berkembang dengan cepat. Kebanyakan perakit hari ini memiliki makro; jika tidak, Anda dapat membuat pra-prosesor makro (sederhana) menggunakan skrip Perl (cukup sederhana RegEx).

Ini. Tepat. Kompiler untuk mengalahkan pakar domain belum ditemukan.
cmaster - mengembalikan monica

4

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).


4

Salah satu cuplikan perakitan yang lebih terkenal adalah dari loop pemetaan tekstur Michael Abrash ( dijelaskan secara rinci di sini ):

add edx,[DeltaVFrac] ; add in dVFrac
sbb ebp,ebp ; store carry
mov [edi],al ; write pixel n
mov al,[esi] ; fetch pixel n+1
add ecx,ebx ; add in dUFrac
adc esi,[4*ebp + UVStepVCarry]; add in steps

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.


Lebih baik lagi, intrinsik SSE tersebut ditentukan oleh Intel sehingga sebenarnya cukup portabel.
James

4

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.


Ini akan menjadi sedikit lebih benar: "Akan sulit untuk membuat program C nontrivial di mana ..." Atau, Anda dapat mengatakan: "Akan sulit untuk menemukan program C dunia nyata di mana ..." Intinya adalah , ada loop sepele yang membuat kompiler menghasilkan output yang optimal. Meski begitu, jawaban yang bagus.
cmaster - mengembalikan monica


4

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.


8
GCC melakukan optimisasi "platform-independen" yang sangat cerdas. Namun, itu tidak begitu baik dalam menggunakan set instruksi tertentu sepenuhnya. Untuk kompiler portabel seperti itu melakukan pekerjaan yang sangat baik.
Artelius

2
sepakat. Portabilitasnya, bahasa yang masuk dan target keluar yang luar biasa. Menjadi yang portabel dapat dan memang menghalangi menjadi benar-benar baik dalam satu bahasa atau target. Jadi peluang bagi manusia untuk melakukan yang lebih baik ada untuk optimasi tertentu pada target tertentu.
old_timer

+1: GCC tentu tidak kompetitif dalam menghasilkan kode cepat tapi saya tidak yakin itu karena ini portabel. LLVM bersifat portable dan saya telah melihatnya menghasilkan kode 4x lebih cepat dari GCC.
Jon Harrop

Saya lebih suka GCC, karena sudah solid selama bertahun-tahun, ditambah lagi tersedia untuk hampir setiap platform yang dapat menjalankan kompiler portabel modern. Sayangnya saya belum dapat membangun LLVM (Mac OS X / PPC), jadi saya mungkin tidak akan dapat beralih ke itu. Salah satu hal baik tentang GCC adalah bahwa jika Anda menulis kode yang dibuat di GCC, kemungkinan besar Anda tetap dekat dengan standar, dan Anda akan yakin bahwa itu dapat dibangun untuk hampir semua platform.

4

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:

  • memori lambat
  • cache cepat
  • coba gunakan cache lebih baik
  • seberapa sering Anda akan ketinggalan? apakah Anda memiliki strategi kompensasi latensi?
  • Anda dapat menjalankan instruksi 10-100 ALU / FPU / SSE untuk satu cache miss
  • arsitektur aplikasi itu penting ..
  • ..tapi itu tidak membantu ketika masalah tidak ada dalam arsitektur

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.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.