Apakah bahasa rakitan inline lebih lambat daripada kode C ++ asli?

183

Saya mencoba membandingkan kinerja bahasa assembly inline dan kode C ++, jadi saya menulis sebuah fungsi yang menambahkan dua array ukuran 2000 untuk 100.000 kali. Berikut kodenya:

#define TIMES 100000
void calcuC(int *x,int *y,int length)
{
    for(int i = 0; i < TIMES; i++)
    {
        for(int j = 0; j < length; j++)
            x[j] += y[j];
    }
}


void calcuAsm(int *x,int *y,int lengthOfArray)
{
    __asm
    {
        mov edi,TIMES
        start:
        mov esi,0
        mov ecx,lengthOfArray
        label:
        mov edx,x
        push edx
        mov eax,DWORD PTR [edx + esi*4]
        mov edx,y
        mov ebx,DWORD PTR [edx + esi*4]
        add eax,ebx
        pop edx
        mov [edx + esi*4],eax
        inc esi
        loop label
        dec edi
        cmp edi,0
        jnz start
    };
}

Inilah main():

int main() {
    bool errorOccured = false;
    setbuf(stdout,NULL);
    int *xC,*xAsm,*yC,*yAsm;
    xC = new int[2000];
    xAsm = new int[2000];
    yC = new int[2000];
    yAsm = new int[2000];
    for(int i = 0; i < 2000; i++)
    {
        xC[i] = 0;
        xAsm[i] = 0;
        yC[i] = i;
        yAsm[i] = i;
    }
    time_t start = clock();
    calcuC(xC,yC,2000);

    //    calcuAsm(xAsm,yAsm,2000);
    //    for(int i = 0; i < 2000; i++)
    //    {
    //        if(xC[i] != xAsm[i])
    //        {
    //            cout<<"xC["<<i<<"]="<<xC[i]<<" "<<"xAsm["<<i<<"]="<<xAsm[i]<<endl;
    //            errorOccured = true;
    //            break;
    //        }
    //    }
    //    if(errorOccured)
    //        cout<<"Error occurs!"<<endl;
    //    else
    //        cout<<"Works fine!"<<endl;

    time_t end = clock();

    //    cout<<"time = "<<(float)(end - start) / CLOCKS_PER_SEC<<"\n";

    cout<<"time = "<<end - start<<endl;
    return 0;
}

Kemudian saya menjalankan program lima kali untuk mendapatkan siklus prosesor, yang bisa dilihat sebagai waktu. Setiap kali saya memanggil salah satu fungsi yang disebutkan di atas saja.

Dan inilah hasilnya.

Fungsi versi perakitan:

Debug   Release
---------------
732        668
733        680
659        672
667        675
684        694
Average:   677

Fungsi versi C ++:

Debug     Release
-----------------
1068      168
 999      166
1072      231
1002      166
1114      183
Average:  182

Kode C ++ dalam mode rilis hampir 3,7 kali lebih cepat dari kode perakitan. Mengapa?

Saya kira kode assembly yang saya tulis tidak seefektif yang dihasilkan oleh GCC. Sulit bagi programmer umum seperti saya untuk menulis kode lebih cepat dari lawannya yang dihasilkan oleh kompiler. Apakah itu berarti saya tidak boleh mempercayai kinerja bahasa assembly yang ditulis oleh tangan saya, fokus pada C ++ dan melupakan bahasa assembly?

pengguna957121
sumber
29
Kurang lebih. Perakitan handcoded sesuai dalam beberapa keadaan, tetapi harus diperhatikan untuk memastikan bahwa versi perakitan memang lebih cepat daripada apa yang dapat dicapai dengan bahasa tingkat yang lebih tinggi.
Magnus Hoff
161
Anda mungkin menemukan pelajaran untuk mempelajari kode yang dihasilkan oleh kompiler, dan mencoba memahami mengapa lebih cepat daripada versi perakitan Anda.
Paul R
34
Ya, sepertinya kompiler lebih baik dalam menulis asm daripada Anda. Kompiler modern benar-benar bagus.
David Heffernan
20
Sudahkah Anda melihat perakitan yang diproduksi GCC? Kemungkinan GCC menggunakan instruksi MMX. Fungsi Anda sangat paralel - Anda berpotensi menggunakan prosesor N untuk menghitung jumlah dalam 1 / N kali. Coba fungsi di mana tidak ada harapan untuk paralelisasi.
Chris
11
Hm, saya akan mengharapkan kompiler yang baik untuk melakukan ini ~ 100000 kali lebih cepat ...
PlasmaHH

Jawaban:

261

Ya, paling sering.

Pertama-tama Anda mulai dari asumsi yang salah bahwa bahasa tingkat rendah (perakitan dalam kasus ini) akan selalu menghasilkan kode lebih cepat daripada bahasa tingkat tinggi (C ++ dan C dalam kasus ini). Itu tidak benar. Apakah kode C selalu lebih cepat daripada kode Java? Tidak karena ada variabel lain: pemrogram. Cara Anda menulis kode dan pengetahuan tentang detail arsitektur sangat memengaruhi kinerja (seperti yang Anda lihat dalam kasus ini).

Anda selalu dapat menghasilkan contoh di mana kode perakitan buatan tangan lebih baik daripada kode yang dikompilasi, tetapi biasanya itu adalah contoh fiktif atau rutin tunggal bukan program sebenarnya dari 500.000+ baris kode C ++). Saya pikir kompiler akan menghasilkan kode rakitan yang lebih baik 95% kali dan kadang - kadang, hanya beberapa kali yang jarang, Anda mungkin perlu menulis kode rakitan untuk beberapa rutinitas kinerja kritis yang singkat, sangat sering digunakan , atau ketika Anda harus mengakses fitur bahasa tingkat tinggi favorit Anda tidak terbuka. Apakah Anda ingin sentuhan kompleksitas ini? Baca jawaban yang luar biasa ini di SO.

Kenapa ini?

Pertama-tama karena kompiler dapat melakukan optimasi yang bahkan tidak dapat kita bayangkan (lihat daftar pendek ini ) dan mereka akan melakukannya dalam hitungan detik (ketika kita mungkin perlu berhari-hari ).

Saat Anda membuat kode dalam perakitan, Anda harus membuat fungsi yang terdefinisi dengan antarmuka panggilan yang terdefinisi dengan baik. Namun mereka dapat memperhitungkan seluruh program optimasi dan optimasi antar-prosedural seperti alokasi register , propagasi konstan , eliminasi subekspresi umum , penjadwalan instruksi dan kompleks lainnya, optimasi tidak jelas ( model Polytope , misalnya). Pada arsitektur RISC , orang-orang berhenti mengkhawatirkan hal ini bertahun-tahun yang lalu (penjadwalan instruksi, misalnya, sangat sulit untuk disetel dengan tangan ) dan CPU CISC modern memiliki saluran pipa yang sangat panjang terlalu.

Untuk beberapa mikrokontroler yang kompleks, bahkan pustaka sistem ditulis dalam C daripada perakitan karena kompiler mereka menghasilkan kode akhir yang lebih baik (dan mudah dipelihara).

Kompiler terkadang dapat secara otomatis menggunakan beberapa instruksi MMX / SIMDx sendiri, dan jika Anda tidak menggunakannya, Anda tidak dapat membandingkan (jawaban lain sudah memeriksa kode perakitan Anda dengan sangat baik). Hanya untuk loop ini adalah daftar pendek dari optimasi loop dari apa yang biasanya diperiksa oleh kompiler (apakah Anda pikir Anda bisa melakukannya sendiri ketika jadwal Anda telah diputuskan untuk program C #?) Jika Anda menulis sesuatu dalam perakitan, saya pikir Anda harus mempertimbangkan setidaknya beberapa optimasi sederhana . Contoh buku sekolah untuk array adalah membuka gulungan siklus (ukurannya diketahui pada waktu kompilasi). Lakukan dan jalankan tes Anda lagi.

Hari-hari ini juga sangat jarang perlu menggunakan bahasa assembly untuk alasan lain: kebanyakan CPU yang berbeda . Apakah Anda ingin mendukung mereka semua? Masing-masing memiliki mikroarsitektur spesifik dan beberapa set instruksi khusus . Mereka memiliki jumlah unit fungsional yang berbeda dan instruksi perakitan harus diatur untuk membuat mereka semua sibuk . Jika Anda menulis dalam C Anda dapat menggunakan PGO tetapi dalam perakitan Anda akan membutuhkan pengetahuan besar tentang arsitektur tertentu (dan memikirkan kembali dan mengulang semuanya untuk arsitektur lain ). Untuk tugas-tugas kecil kompiler biasanya melakukannya dengan lebih baik, dan untuk tugas-tugas kompleks biasanya pekerjaan tidak dilunasi (dankompiler mungkin lebih baik ).

Jika Anda duduk dan melihat kode Anda mungkin Anda akan melihat bahwa Anda akan mendapatkan lebih banyak untuk mendesain ulang algoritma Anda daripada menerjemahkan ke perakitan (baca posting hebat ini di sini di SO ), ada optimasi tingkat tinggi (dan hints to compiler) Anda dapat menerapkan secara efektif sebelum Anda perlu menggunakan bahasa assembly. Mungkin perlu disebutkan bahwa sering menggunakan intrinsik Anda akan mendapatkan keuntungan kinerja yang Anda cari dan kompiler masih dapat melakukan sebagian besar optimasi.

Semua ini mengatakan, bahkan ketika Anda dapat menghasilkan kode perakitan 5 ~ 10 kali lebih cepat, Anda harus bertanya kepada pelanggan Anda apakah mereka lebih suka membayar satu minggu dari waktu Anda atau untuk membeli CPU 50 $ lebih cepat . Optimalisasi ekstrim lebih sering daripada tidak (dan terutama dalam aplikasi LOB) sama sekali tidak diperlukan dari kebanyakan dari kita.

Adriano Repetti
sumber
9
Tentu saja tidak. Saya pikir lebih baik dari 95% orang di 99% kali. Kadang-kadang karena itu hanya mahal (karena matematika yang rumit ) atau menghabiskan waktu (kemudian mahal lagi). Terkadang karena kita memang lupa tentang pengoptimalan ...
Adriano Repetti
62
@ ja72 - tidak, tidak lebih baik dalam menulis kode. Lebih baik dalam mengoptimalkan kode.
Mike Baranczak
14
Ini kontra-intuitif hingga Anda benar-benar mempertimbangkannya. Dengan cara yang sama, mesin berbasis VM mulai membuat optimasi runtime yang tidak dimiliki oleh kompiler.
Bill K
6
@ M28: Kompiler dapat menggunakan instruksi yang sama. Tentu, mereka membayarnya dalam ukuran biner (karena mereka harus memberikan jalur mundur jika instruksi tersebut tidak didukung). Juga, sebagian besar, "instruksi baru" yang akan ditambahkan adalah instruksi SMID, yang baik VMs dan Compiler cukup mengerikan dalam memanfaatkan. VM membayar untuk fitur ini karena mereka harus mengkompilasi kode saat startup.
Billy ONeal
9
@ BillK: PGO melakukan hal yang sama untuk kompiler.
Billy ONeal
194

Kode rakitan Anda tidak optimal dan dapat ditingkatkan:

  • Anda mendorong dan membuka register ( EDX ) di loop batin Anda. Ini harus dipindahkan dari loop.
  • Anda memuat ulang pointer array di setiap iterasi dari loop. Ini harus keluar dari loop.
  • Anda menggunakan loopinstruksi, yang dikenal sangat lambat pada kebanyakan CPU modern (mungkin akibat menggunakan buku rakitan kuno *)
  • Anda tidak mengambil keuntungan dari membuka gulungan manual.
  • Anda tidak menggunakan instruksi SIMD yang tersedia .

Jadi, kecuali jika Anda sangat meningkatkan keterampilan Anda tentang assembler, tidak masuk akal bagi Anda untuk menulis kode assembler untuk kinerja.

* Tentu saja saya tidak tahu apakah Anda benar-benar mendapatkan loopinstruksi dari buku rakitan kuno. Tapi Anda hampir tidak pernah melihatnya dalam kode dunia nyata, karena setiap kompiler di luar sana cukup pintar untuk tidak memancarkannya loop, Anda hanya melihatnya dalam buku-buku IMHO buruk dan ketinggalan jaman.

Piez Gunther
sumber
kompiler masih dapat memancarkan loop(dan banyak instruksi "usang") jika Anda mengoptimalkan untuk ukuran
phuclv
1
@ phuclv baik ya, tapi pertanyaan aslinya adalah persis tentang kecepatan, bukan ukuran.
IGR94
60

Bahkan sebelum mempelajari perakitan, ada transformasi kode yang ada di tingkat yang lebih tinggi.

static int const TIMES = 100000;

void calcuC(int *x, int *y, int length) {
  for (int i = 0; i < TIMES; i++) {
    for (int j = 0; j < length; j++) {
      x[j] += y[j];
    }
  }
}

dapat diubah menjadi melalui Loop Rotation :

static int const TIMES = 100000;

void calcuC(int *x, int *y, int length) {
    for (int j = 0; j < length; ++j) {
      for (int i = 0; i < TIMES; ++i) {
        x[j] += y[j];
      }
    }
}

yang jauh lebih baik sejauh memori lokalitas berjalan.

Ini dapat dioptimalkan lebih lanjut, melakukan a += bX kali setara dengan melakukannya a += X * bsehingga kita mendapatkan:

static int const TIMES = 100000;

void calcuC(int *x, int *y, int length) {
    for (int j = 0; j < length; ++j) {
      x[j] += TIMES * y[j];
    }
}

namun sepertinya pengoptimal favorit saya (LLVM) tidak melakukan transformasi ini.

[sunting] saya menemukan bahwa transformasi dilakukan jika kita memiliki restrictkualifikasi untuk xdan y. Memang tanpa batasan ini, x[j]dan y[j]bisa alias ke lokasi yang sama yang membuat transformasi ini salah. [sunting]

Lagi pula, ini , saya pikir, versi C yang dioptimalkan. Sudah jauh lebih sederhana. Berdasarkan ini, ini adalah crack saya di ASM (saya membiarkan Clang menghasilkannya, saya tidak berguna dalam hal itu):

calcuAsm:                               # @calcuAsm
.Ltmp0:
    .cfi_startproc
# BB#0:
    testl   %edx, %edx
    jle .LBB0_2
    .align  16, 0x90
.LBB0_1:                                # %.lr.ph
                                        # =>This Inner Loop Header: Depth=1
    imull   $100000, (%rsi), %eax   # imm = 0x186A0
    addl    %eax, (%rdi)
    addq    $4, %rsi
    addq    $4, %rdi
    decl    %edx
    jne .LBB0_1
.LBB0_2:                                # %._crit_edge
    ret
.Ltmp1:
    .size   calcuAsm, .Ltmp1-calcuAsm
.Ltmp2:
    .cfi_endproc

Saya khawatir saya tidak mengerti dari mana semua instruksi itu berasal, tetapi Anda selalu bisa bersenang-senang dan mencoba dan melihat bagaimana membandingkannya ... tapi saya masih menggunakan versi C yang dioptimalkan daripada yang perakitan, dalam kode, jauh lebih portabel.

Matthieu M.
sumber
Terima kasih atas jawaban Anda. Nah, agak membingungkan bahwa ketika saya mengambil kelas yang bernama "Prinsip-prinsip Kompiler", saya belajar bahwa kompiler akan mengoptimalkan kode kami dengan berbagai cara. Apakah itu berarti kita perlu mengoptimalkan kode kita secara manual? Bisakah kita melakukan pekerjaan yang lebih baik daripada kompiler? Itu pertanyaan yang selalu membingungkan saya.
user957121
2
@ user957121: kami dapat mengoptimalkannya dengan lebih baik ketika kami memiliki lebih banyak informasi. Khususnya di sini yang menghambat kompiler adalah kemungkinan alias antara xdan y. Artinya, compiler tidak dapat yakin bahwa untuk semua i,jdi [0, length)kita x + i != y + j. Jika ada tumpang tindih, maka optimasi tidak mungkin. Bahasa C memperkenalkan restrictkata kunci untuk memberi tahu kompiler bahwa dua pointer tidak bisa alias, namun tidak berfungsi untuk array karena mereka masih bisa tumpang tindih walaupun mereka tidak benar-benar alias.
Matthieu M.
GCC saat ini dan Dentang vektor otomatis (setelah memeriksa tidak tumpang tindih jika Anda hilangkan __restrict). SSE2 adalah dasar untuk x86-64, dan dengan pengocokan SSE2 dapat melakukan penggandaan 2x 32-bit sekaligus (menghasilkan produk 64-bit, maka pengocokan untuk menyatukan hasilnya kembali). godbolt.org/z/r7F_uo . (SSE4.1 diperlukan untuk pmulld: dikemas 32x32 => 32-bit multiply). GCC memiliki trik yang rapi untuk mengubah pengganda integer konstan menjadi shift / add (dan / atau kurangi), yang bagus untuk pengganda dengan beberapa bit yang ditetapkan. Kode shuffle-heavy Clang akan mengalami bottleneck pada throughput shuffle pada CPU Intel.
Peter Cordes
41

Jawaban singkat: ya.

Jawaban panjang: ya, kecuali Anda benar-benar tahu apa yang Anda lakukan, dan punya alasan untuk melakukannya.

Oliver Charlesworth
sumber
3
dan kemudian hanya jika Anda telah menjalankan alat profil tingkat perakitan seperti vtune untuk chip intel untuk melihat di mana Anda mungkin dapat meningkatkan hal
Mark Mullin
1
Ini secara teknis menjawab pertanyaan tetapi juga sama sekali tidak berguna. A -1 dari saya.
Navin
2
Jawaban yang sangat panjang: "Ya, kecuali Anda ingin mengubah seluruh kode Anda setiap kali CPU (er) baru digunakan. Pilih algoritme terbaik, tetapi biarkan kompiler melakukan optimasi"
Tommylee2k
35

Saya telah memperbaiki kode asm saya:

  __asm
{   
    mov ebx,TIMES
 start:
    mov ecx,lengthOfArray
    mov esi,x
    shr ecx,1
    mov edi,y
label:
    movq mm0,QWORD PTR[esi]
    paddd mm0,QWORD PTR[edi]
    add edi,8
    movq QWORD PTR[esi],mm0
    add esi,8
    dec ecx 
    jnz label
    dec ebx
    jnz start
};

Hasil untuk versi Rilis:

 Function of assembly version: 81
 Function of C++ version: 161

Kode rakitan dalam mode rilis hampir 2 kali lebih cepat daripada C ++.

sasha
sumber
18
Sekarang jika Anda mulai menggunakan SSE dan bukan MMX (nama register xmm0bukan mm0), Anda akan mendapatkan speedup lain dengan faktor dua ;-)
Gunther Piez
8
Saya berubah, mendapat 41 untuk versi perakitan. Ini dalam 4 kali lebih cepat :)
sasha
3
juga bisa mendapatkan hingga 5% lebih banyak jika menggunakan semua register xmm
sasha
7
Sekarang jika Anda berpikir tentang waktu yang sebenarnya Anda perlukan: berkumpul, sekitar 10 jam atau lebih? C ++, beberapa menit kurasa? Ada pemenang yang jelas di sini, kecuali jika itu adalah kode kritis kinerja.
Calimo
1
Kompiler yang baik sudah akan melakukan auto-vectorize dengan paddd xmm(setelah memeriksa tumpang tindih antara xdan y, karena Anda tidak menggunakan int *__restrict x). Misalnya gcc melakukan itu: godbolt.org/z/c2JG0- . Atau setelah masuk main, tidak perlu memeriksa tumpang tindih karena dapat melihat alokasi dan membuktikan mereka tidak tumpang tindih. (Dan itu akan bisa mengasumsikan keselarasan 16-byte pada beberapa implementasi x86-64, juga, yang tidak berlaku untuk definisi yang berdiri sendiri.) Dan jika Anda mengompilasinya gcc -O3 -march=native, Anda bisa mendapatkan 256-bit atau 512-bit vektorisasi.
Peter Cordes
24

Apakah itu berarti saya tidak boleh mempercayai kinerja bahasa assembly yang ditulis oleh tangan saya

Ya, itulah tepatnya artinya, dan memang benar untuk setiap orang bahasa. Jika Anda tidak tahu cara menulis kode efisien dalam bahasa X, maka Anda tidak boleh mempercayai kemampuan Anda untuk menulis kode efisien dalam X. Jadi, jika Anda ingin kode efisien, Anda harus menggunakan bahasa lain.

Majelis sangat peka terhadap ini, karena, yah, apa yang Anda lihat adalah apa yang Anda dapatkan. Anda menulis instruksi khusus yang ingin Anda jalankan CPU. Dengan bahasa tingkat tinggi, ada kompiler di antara, yang dapat mengubah kode Anda dan menghapus banyak inefisiensi. Dengan perakitan, Anda sendirian.

jalf
sumber
2
Saya pikir itu adalah untuk menulis bahwa terutama untuk prosesor x86 modern, sangat sulit untuk menulis kode perakitan yang efisien karena keberadaan pipa, beberapa unit eksekusi dan gimmick lain di dalam setiap inti. Menulis kode yang menyeimbangkan penggunaan semua sumber daya ini untuk mendapatkan kecepatan eksekusi tertinggi akan sering menghasilkan kode dengan logika tidak lurus yang "tidak boleh" cepat sesuai dengan kebijaksanaan perakitan "konvensional". Tetapi untuk CPU yang kurang kompleks, pengalaman saya bahwa pembuatan kode kompiler C dapat ditingkatkan secara signifikan.
Olof Forshell
4
C compiler kode dapat dilakukan biasanya menjadi baik, bahkan pada CPU x86 modern. Tetapi Anda harus memahami CPU dengan baik, yang lebih sulit dilakukan dengan CPU x86 modern. Itu maksudku. Jika Anda tidak memahami perangkat keras yang Anda targetkan, maka Anda tidak akan dapat mengoptimalkannya. Dan kemudian compiler kemungkinan akan melakukan yang lebih baik pekerjaan
jalf
1
Dan jika Anda benar-benar ingin meledakkan kompiler Anda harus kreatif dan mengoptimalkan dengan cara yang tidak bisa dilakukan oleh kompiler. Ini adalah tradeoff untuk waktu / hadiah itu sebabnya C adalah bahasa scripting untuk beberapa dan kode menengah untuk bahasa tingkat yang lebih tinggi untuk yang lain. Bagi saya, perakitan lebih untuk bersenang-senang :). seperti grc.com/smgassembly.htm
Hawken
22

Satu-satunya alasan untuk menggunakan bahasa rakitan saat ini adalah menggunakan beberapa fitur yang tidak dapat diakses oleh bahasa tersebut.

Ini berlaku untuk:

  • Pemrograman kernel yang perlu mengakses fitur perangkat keras tertentu seperti MMU
  • Pemrograman berkinerja tinggi yang menggunakan instruksi vektor atau multimedia yang sangat spesifik tidak didukung oleh kompiler Anda.

Tetapi kompiler saat ini cukup pintar, mereka bahkan dapat mengganti dua pernyataan terpisah seperti d = a / b; r = a % b;dengan instruksi tunggal yang menghitung divisi dan sisanya dalam sekali jalan jika tersedia, bahkan jika C tidak memiliki operator tersebut.

fortran
sumber
10
Ada tempat lain untuk ASM selain keduanya. Yaitu, perpustakaan bignum biasanya akan secara signifikan lebih cepat dalam ASM daripada C, karena memiliki akses untuk membawa bendera dan bagian atas perkalian dan semacamnya. Anda dapat melakukan hal-hal ini dalam portable C juga, tetapi mereka sangat lambat.
Mooing Duck
@ MoingDuck Itu mungkin dianggap sebagai mengakses fitur perangkat keras perangkat keras yang tidak tersedia secara langsung dalam bahasa ... Tapi selama Anda hanya menerjemahkan kode tingkat tinggi Anda ke perakitan dengan tangan, kompiler akan mengalahkan Anda.
fortran
1
memang begitu, tetapi ini bukan pemrograman kernel, atau vendor khusus. Meskipun dengan sedikit perubahan pengerjaan, itu bisa dengan mudah jatuh ke dalam kategori baik. Id tebak ASM saat Anda menginginkan kinerja instruksi prosesor yang tidak memiliki pemetaan C.
Mooing Duck
1
@fortran Anda pada dasarnya hanya mengatakan jika Anda tidak mengoptimalkan kode Anda itu tidak akan secepat kode kompiler dioptimalkan. Optimalisasi adalah alasan seseorang akan menulis perakitan di tempat pertama. Jika Anda bermaksud menerjemahkan maka optimalkan tidak ada alasan kompiler akan mengalahkan Anda kecuali Anda tidak pandai mengoptimalkan perakitan. Jadi untuk mengalahkan kompiler Anda harus mengoptimalkan dengan cara yang tidak bisa dilakukan oleh kompiler. Cukup jelas. Satu-satunya alasan untuk menulis rakitan adalah jika Anda lebih baik daripada kompiler / juru bahasa . Itu selalu menjadi alasan praktis untuk menulis pertemuan.
Hawken
1
Hanya mengatakan: Dentang memiliki akses ke bendera carry, perkalian 128 bit dan seterusnya melalui fungsi bawaan. Dan itu dapat mengintegrasikan semua ini ke dalam algoritma optimasi normal.
gnasher729
19

Memang benar bahwa kompiler modern melakukan pekerjaan yang luar biasa pada optimasi kode, namun saya masih akan mendorong Anda untuk terus belajar perakitan.

Pertama-tama Anda jelas tidak terintimidasi olehnya , itu adalah nilai tambah yang hebat, selanjutnya - Anda berada di jalur yang benar dengan membuat profil untuk memvalidasi atau membuang asumsi kecepatan Anda, Anda meminta masukan dari orang-orang yang berpengalaman , dan Anda memiliki alat pengoptimal terbesar yang diketahui umat manusia: otak .

Ketika pengalaman Anda meningkat, Anda akan belajar kapan dan di mana menggunakannya (biasanya loop yang paling ketat dan paling dalam dalam kode Anda, setelah Anda sangat dioptimalkan pada tingkat algoritmik).

Untuk inspirasi, saya sarankan Anda mencari artikel Michael Abrash (jika Anda belum pernah mendengar darinya, ia adalah seorang guru optimisasi; ia bahkan berkolaborasi dengan John Carmack dalam optimalisasi renderer perangkat lunak Quake!)

"Tidak ada yang namanya kode tercepat" - Michael Abrash


sumber
2
Saya percaya salah satu buku Michael Abrash adalah buku hitam pemrograman grafis. Tapi dia bukan satu-satunya yang menggunakan perakitan, Chris Sawyer menulis dua game taipan roller coaster pertama di perakitan sendirian.
Hawken
14

Saya telah mengubah kode asm:

 __asm
{ 
    mov ebx,TIMES
 start:
    mov ecx,lengthOfArray
    mov esi,x
    shr ecx,2
    mov edi,y
label:
    mov eax,DWORD PTR [esi]
    add eax,DWORD PTR [edi]
    add edi,4   
    dec ecx 
    mov DWORD PTR [esi],eax
    add esi,4
    test ecx,ecx
    jnz label
    dec ebx
    test ebx,ebx
    jnz start
};

Hasil untuk versi Rilis:

 Function of assembly version: 41
 Function of C++ version: 161

Kode rakitan dalam mode rilis hampir 4 kali lebih cepat daripada C ++. IMHo, kecepatan kode perakitan tergantung dari Programmer

sasha
sumber
Ya, kode saya benar-benar perlu dioptimalkan. Kerja bagus untuk Anda dan terima kasih!
user957121
5
Ini empat kali lebih cepat karena Anda hanya melakukan seperempat dari pekerjaan :-) Ini shr ecx,2berlebihan, karena panjang array sudah diberikan intdan bukan dalam byte. Jadi pada dasarnya Anda mencapai kecepatan yang sama. Anda dapat mencoba padddjawaban dari harold, ini akan benar-benar lebih cepat.
Gunther Piez
13

itu topik yang sangat menarik!
Saya telah mengubah MMX oleh SSE dalam kode Sasha.
Ini hasil saya:

Function of C++ version:      315
Function of assembly(simply): 312
Function of assembly  (MMX):  136
Function of assembly  (SSE):  62

Kode perakitan dengan SSE adalah 5 kali lebih cepat dari C ++

salaoshi
sumber
12

Kebanyakan kompiler bahasa tingkat tinggi sangat dioptimalkan dan tahu apa yang mereka lakukan. Anda dapat mencoba dan membuang kode membongkar dan membandingkannya dengan perakitan asli Anda. Saya yakin Anda akan melihat beberapa trik bagus yang digunakan kompiler Anda.

Sebagai contoh, meskipun saya tidak yakin itu benar lagi :):

Perbuatan:

mov eax,0

biaya siklus lebih banyak daripada

xor eax,eax

yang melakukan hal yang sama.

Kompiler mengetahui semua trik ini dan menggunakannya.

Nuno_147
sumber
4
Masih benar, lihat stackoverflow.com/questions/1396527/… . Bukan karena siklus yang digunakan, tetapi karena jejak memori berkurang.
Gunther Piez
10

Kompiler mengalahkan Anda. Saya akan mencobanya, tetapi saya tidak akan memberikan jaminan. Saya akan berasumsi bahwa "penggandaan" oleh TIMES dimaksudkan untuk membuatnya menjadi tes kinerja yang lebih relevan, yang ydan x16-sejajar, dan itu lengthadalah kelipatan non-nol dari 4. Itu mungkin semua tetap benar.

  mov ecx,length
  lea esi,[y+4*ecx]
  lea edi,[x+4*ecx]
  neg ecx
loop:
  movdqa xmm0,[esi+4*ecx]
  paddd xmm0,[edi+4*ecx]
  movdqa [edi+4*ecx],xmm0
  add ecx,4
  jnz loop

Seperti yang saya katakan, saya tidak membuat jaminan. Tapi saya akan terkejut jika itu bisa dilakukan jauh lebih cepat - hambatan di sini adalah throughput memori bahkan jika semuanya adalah hit L1.

Harold
sumber
Saya pikir pengalamatan kompleks memperlambat kode Anda, jika Anda mengubah kode ke mov ecx, length, lea ecx,[ecx*4], mov eax,16... add ecx,eaxdan kemudian hanya menggunakan [esi + ecx] di mana-mana Anda akan menghindari 1 siklus kios per instruksi mempercepat banyak loop. (Jika Anda memiliki Skylake terbaru maka ini tidak berlaku). Add reg, reg hanya membuat loop lebih kencang, yang mungkin atau mungkin tidak membantu.
Johan
@Johan yang seharusnya tidak menjadi warung, hanya latensi siklus tambahan, tapi pasti tidak ada salahnya untuk tidak memilikinya .. Saya menulis kode ini untuk Core2 yang tidak memiliki masalah itu. Bukankah r + r juga "kompleks" btw?
Harold
7

Secara membabi buta mengimplementasikan algoritma yang sama persis, instruksi demi instruksi, dalam perakitan dijamin lebih lambat dari apa yang dapat dilakukan oleh kompiler.

Itu karena bahkan optimasi terkecil yang dilakukan kompiler lebih baik daripada kode kaku Anda tanpa optimasi sama sekali.

Tentu saja, adalah mungkin untuk mengalahkan kompiler, terutama jika itu adalah bagian kecil dari kode yang terlokalisasi, saya bahkan harus melakukannya sendiri untuk mendapatkan kira-kira. 4x mempercepat, tetapi dalam hal ini kita harus sangat bergantung pada pengetahuan yang baik tentang perangkat keras dan banyak trik yang tampaknya kontra-intuitif.

vsz
sumber
3
Saya pikir ini tergantung pada bahasa dan kompiler. Saya dapat membayangkan sebuah kompiler C yang sangat tidak efisien yang hasilnya dapat dengan mudah dikalahkan oleh tulisan langsung perakitan manusia. GCC, tidak banyak.
Casey Rodarmor
Dengan kompiler C / ++ menjadi tugas seperti itu, dan hanya 3 yang utama di sekitar, mereka cenderung lebih baik pada apa yang mereka lakukan. Masih (sangat) mungkin dalam keadaan tertentu perakitan tulisan tangan akan lebih cepat; banyak pustaka matematika beralih ke asm untuk menangani lebih baik nilai banyak / lebar. Jadi, sementara dijamin agak terlalu kuat, kemungkinan besar.
ssube
@ peachykeen: Saya tidak bermaksud bahwa perakitan dijamin lebih lambat daripada C ++ secara umum. Maksud saya "jaminan" dalam kasus di mana Anda memiliki kode C ++ dan menerjemahkannya secara per baris ke perakitan. Baca paragraf terakhir dari jawaban saya juga :)
vsz
5

Sebagai kompiler saya akan mengganti sebuah loop dengan ukuran tetap untuk banyak tugas eksekusi.

int a = 10;
for (int i = 0; i < 3; i += 1) {
    a = a + i;
}

akan menghasilkan

int a = 10;
a = a + 0;
a = a + 1;
a = a + 2;

dan pada akhirnya ia akan tahu bahwa "a = a + 0;" tidak berguna sehingga akan menghapus baris ini. Semoga ada sesuatu di kepala Anda sekarang yang mau melampirkan beberapa opsi optimasi sebagai komentar. Semua optimasi yang sangat efektif akan membuat bahasa yang dikompilasi lebih cepat.

Miah
sumber
4
Dan kecuali avolatile, ada kemungkinan bagus bahwa kompiler hanya akan melakukan int a = 13;dari awal.
vsz
4

Persis apa artinya. Biarkan optimasi mikro ke kompiler.

Luchian Grigore
sumber
4

Saya suka contoh ini karena menunjukkan pelajaran penting tentang kode tingkat rendah. Ya, Anda dapat menulis rakitan yang secepat kode C Anda. Ini benar secara tautologis, tetapi tidak berarti apa - apa. Jelas seseorang bisa, kalau tidak assembler tidak akan tahu optimasi yang sesuai.

Demikian juga, prinsip yang sama berlaku saat Anda naik ke hierarki abstraksi bahasa. Ya, Anda dapat menulis parser dalam C yang secepat skrip perl cepat dan kotor, dan banyak orang melakukannya. Tetapi itu tidak berarti bahwa karena Anda menggunakan C, kode Anda akan cepat. Dalam banyak kasus, bahasa tingkat yang lebih tinggi melakukan optimasi yang mungkin belum pernah Anda pertimbangkan.

tylerl
sumber
3

Dalam banyak kasus, cara optimal untuk melakukan beberapa tugas mungkin tergantung pada konteks di mana tugas itu dilakukan. Jika suatu rutin ditulis dalam bahasa assembly, secara umum tidak mungkin urutan instruksi bervariasi berdasarkan konteks. Sebagai contoh sederhana, pertimbangkan metode sederhana berikut:

inline void set_port_high(void)
{
  (*((volatile unsigned char*)0x40001204) = 0xFF);
}

Kompiler untuk kode ARM 32-bit, yang diberikan di atas, kemungkinan akan membuatnya sebagai sesuatu seperti:

ldr  r0,=0x40001204
mov  r1,#0
strb r1,[r0]
[a fourth word somewhere holding the constant 0x40001204]

atau mungkin

ldr  r0,=0x40001000  ; Some assemblers like to round pointer loads to multiples of 4096
mov  r1,#0
strb r1,[r0+0x204]
[a fourth word somewhere holding the constant 0x40001000]

Itu bisa dioptimalkan sedikit dalam kode rakitan tangan, seperti:

ldr  r0,=0x400011FF
strb r0,[r0+5]
[a third word somewhere holding the constant 0x400011FF]

atau

mvn  r0,#0xC0       ; Load with 0x3FFFFFFF
add  r0,r0,#0x1200  ; Add 0x1200, yielding 0x400011FF
strb r0,[r0+5]

Kedua pendekatan rakitan tangan akan membutuhkan 12 byte ruang kode daripada 16; yang terakhir akan menggantikan "load" dengan "add", yang pada ARM7-TDMI akan mengeksekusi dua siklus lebih cepat. Jika kode akan dieksekusi dalam konteks di mana r0 tidak tahu / tidak peduli, maka versi bahasa assembly akan lebih baik daripada versi yang dikompilasi. Di sisi lain, anggap kompiler tahu bahwa beberapa register [misalnya r5] akan menyimpan nilai yang berada dalam 2047 byte dari alamat yang diinginkan 0x40001204 [misalnya 0x40001000], dan selanjutnya mengetahui bahwa beberapa register lain [misalnya r7] akan untuk memegang nilai yang bit-bit rendahnya adalah 0xFF. Dalam hal ini, kompiler dapat mengoptimalkan versi kode C hanya untuk:

strb r7,[r5+0x204]

Jauh lebih pendek dan lebih cepat daripada kode perakitan yang dioptimalkan dengan tangan. Selanjutnya, anggap set_port_high terjadi dalam konteks:

int temp = function1();
set_port_high();
function2(temp); // Assume temp is not used after this

Sama sekali tidak masuk akal ketika coding untuk sistem tertanam. Jika set_port_highditulis dalam kode assembly, kompiler harus memindahkan r0 (yang menyimpan nilai balik function1) dari tempat lain sebelum memanggil kode assembly, dan kemudian memindahkan nilai itu kembali ke r0 sesudahnya (karena dengan satu instruksi empat lebih kecil dan lebih cepat daripada kode perakitan "dioptimalkan dengan tangan".function2 akan mengharapkan parameter pertama di r0), jadi kode perakitan "yang dioptimalkan" akan membutuhkan lima instruksi. Bahkan jika kompiler tidak mengetahui register yang menyimpan alamat atau nilai untuk menyimpan, versi empat instruksi (yang dapat diadaptasi untuk menggunakan register yang tersedia - tidak harus r0 dan r1) akan mengalahkan rakitan "dioptimalkan" Versi bahasa. Jika kompiler memiliki alamat dan data yang diperlukan dalam r5 dan r7 seperti yang dijelaskan sebelumnya, function1tidak akan mengubah register tersebut, dan dengan demikian ia dapat menggantikanset_port_highstrb instruksi--

Perhatikan bahwa kode rakitan yang dioptimalkan dengan tangan sering kali dapat mengungguli kompiler dalam kasus-kasus di mana programmer mengetahui aliran program yang tepat, tetapi kompiler bersinar dalam kasus-kasus di mana sepotong kode ditulis sebelum konteksnya diketahui, atau di mana satu bagian dari kode sumber mungkin dipanggil dari berbagai konteks [jika set_port_high digunakan di lima puluh tempat yang berbeda dalam kode, kompilator dapat memutuskan sendiri untuk masing-masing cara terbaik untuk mengembangkannya].

Secara umum, saya akan menyarankan bahwa bahasa assembly cenderung untuk menghasilkan peningkatan kinerja terbesar dalam kasus-kasus di mana setiap bagian kode dapat didekati dari sejumlah konteks yang sangat terbatas, dan cenderung merusak kinerja di tempat-tempat di mana sepotong kode dapat didekati dari banyak konteks yang berbeda. Menariknya (dan mudahnya) kasus-kasus di mana perakitan paling bermanfaat bagi kinerja sering kali adalah di mana kode paling mudah dan mudah dibaca. Tempat-tempat kode bahasa majelis akan berubah menjadi berantakan lengket sering kali tempat menulis dalam pertemuan akan menawarkan manfaat kinerja terkecil.

[Catatan kecil: ada beberapa tempat kode perakitan dapat digunakan untuk menghasilkan kekacauan lengket yang dioptimalkan; misalnya, sepotong kode yang saya lakukan untuk ARM diperlukan untuk mengambil kata dari RAM dan menjalankan salah satu dari sekitar dua belas rutinitas berdasarkan enam bit teratas dari nilai (banyak nilai dipetakan ke rutin yang sama). Saya rasa saya mengoptimalkan kode itu ke sesuatu seperti:

ldrh  r0,[r1],#2! ; Fetch with post-increment
ldrb  r1,[r8,r0 asr #10]
sub   pc,r8,r1,asl #2

Register r8 selalu menyimpan alamat tabel pengiriman utama (dalam loop di mana kode menghabiskan 98% waktunya, tidak ada yang pernah menggunakannya untuk tujuan lain); semua 64 entri merujuk ke alamat dalam 256 byte sebelumnya. Karena loop utama dalam kebanyakan kasus memiliki batas waktu eksekusi yang sulit sekitar 60 siklus, pengambilan dan pengiriman sembilan siklus sangat berperan dalam mencapai tujuan itu. Menggunakan tabel 256 alamat 32-bit akan menjadi satu siklus lebih cepat, tetapi akan menelan 1KB RAM yang sangat berharga [flash akan menambahkan lebih dari satu keadaan tunggu]. Menggunakan 64 alamat 32-bit akan membutuhkan penambahan instruksi untuk menutupi beberapa bit dari kata yang diambil, dan masih akan menelan 192 byte lebih banyak daripada tabel yang sebenarnya saya gunakan. Menggunakan tabel offset 8-bit menghasilkan kode yang sangat ringkas dan cepat, tapi bukan sesuatu yang saya harapkan akan dikompilasi oleh kompiler; Saya juga tidak akan mengharapkan kompiler untuk mendedikasikan register "penuh waktu" untuk memegang alamat tabel.

Kode di atas dirancang untuk berjalan sebagai sistem mandiri; secara berkala dapat memanggil kode C, tetapi hanya pada waktu-waktu tertentu ketika perangkat keras yang digunakan berkomunikasi dengan aman dapat dimasukkan ke dalam status "idle" selama dua interval kira-kira satu milidetik setiap 16ms.

supercat
sumber
2

Baru-baru ini, semua optimisasi kecepatan yang telah saya lakukan adalah mengganti kode lambat yang rusak otak dengan hanya kode yang masuk akal. Tetapi untuk hal-hal yang kecepatan sangat penting dan saya berusaha keras untuk membuat sesuatu yang cepat, hasilnya selalu merupakan proses berulang, di mana setiap iterasi memberikan lebih banyak wawasan ke dalam masalah, menemukan cara bagaimana menyelesaikan masalah dengan operasi yang lebih sedikit. Kecepatan akhir selalu tergantung pada seberapa banyak wawasan yang saya dapatkan dalam masalah tersebut. Jika pada tahap apa pun saya menggunakan kode rakitan, atau kode C yang terlalu dioptimalkan, proses mencari solusi yang lebih baik akan menderita dan hasil akhirnya akan lebih lambat.

gnasher729
sumber
2

C ++ lebih cepat kecuali Anda menggunakan bahasa rakitan dengan pengetahuan yang lebih dalam dengan cara yang benar.

Ketika saya kode dalam ASM, saya mengatur ulang instruksi secara manual sehingga CPU dapat mengeksekusi lebih banyak dari mereka secara paralel jika secara logis memungkinkan. Saya hampir tidak menggunakan RAM ketika saya kode dalam ASM misalnya: Mungkin ada 20000+ baris kode di ASM dan saya tidak pernah menggunakan push / pop.

Anda berpotensi melompat di tengah opcode untuk memodifikasi sendiri kode dan perilaku tanpa kemungkinan penalti dari kode modifikasi diri. Mengakses register membutuhkan 1 centang (kadang-kadang membutuhkan 0,25 tick) dari CPU. Mengakses RAM bisa memakan waktu ratusan.

Untuk petualangan ASM terakhir saya, saya tidak pernah menggunakan RAM untuk menyimpan variabel (untuk ribuan baris ASM). ASM bisa berpotensi lebih cepat dari C ++. Tetapi itu tergantung pada banyak faktor variabel seperti:

1. I was writing my apps to run on the bare metal.
2. I was writing my own boot loader that was starting my programs in ASM so there was no OS management in the middle.

Saya sekarang belajar C # dan C ++ karena saya menyadari masalah produktivitas !! Anda dapat mencoba untuk melakukan program yang paling cepat dibayangkan menggunakan ASM murni saja di waktu luang. Tetapi untuk menghasilkan sesuatu, gunakan bahasa tingkat tinggi.

Sebagai contoh, program terakhir yang saya kodekan menggunakan JS dan GLSL dan saya tidak pernah melihat masalah kinerja, bahkan berbicara tentang JS yang lambat. Ini karena konsep pemrograman GPU untuk 3D saja membuat kecepatan bahasa yang mengirimkan perintah ke GPU hampir tidak relevan.

Kecepatan assembler sendiri pada bare metal tak terbantahkan. Mungkinkah lebih lambat di dalam C ++? - Bisa jadi karena Anda menulis kode perakitan dengan kompiler tidak menggunakan assembler untuk memulai.

Dewan pribadi saya adalah untuk tidak pernah menulis kode perakitan jika Anda dapat menghindarinya, meskipun saya suka berkumpul.


sumber
1

Semua jawaban di sini tampaknya mengecualikan satu aspek: kadang-kadang kita tidak menulis kode untuk mencapai tujuan tertentu, tetapi hanya untuk bersenang - senang . Mungkin tidak ekonomis untuk menginvestasikan waktu untuk melakukannya, tetapi bisa dibilang tidak ada kepuasan yang lebih besar daripada mengalahkan potongan kode tercepat yang dioptimalkan kompiler dalam kecepatan dengan alternatif asm digulung secara manual.

madoki
sumber
Ketika Anda hanya ingin mengalahkan kompiler, biasanya lebih mudah untuk mengambil output asm untuk fungsi Anda dan mengubahnya menjadi fungsi asm yang berdiri sendiri yang Anda atur. Menggunakan inline asm adalah pekerjaan tambahan untuk mendapatkan antarmuka antara C ++ dan asm yang benar dan memeriksa apakah kompilasi untuk kode optimal. (Tapi setidaknya ketika hanya melakukannya untuk bersenang-senang, Anda tidak perlu khawatir tentang hal itu mengalahkan optimasi seperti propagasi konstan ketika fungsi inline menjadi sesuatu yang lain. Gcc.gnu.org/wiki/DontUseInlineAsm ).
Peter Cordes
Lihat juga Collatz-conjecture C ++ vs. asm tangan Q&A untuk lebih lanjut tentang mengalahkan kompiler untuk bersenang-senang :) Dan juga saran tentang bagaimana menggunakan apa yang Anda pelajari untuk memodifikasi C ++ untuk membantu kompiler membuat kode yang lebih baik.
Peter Cordes
@PeterCordes Jadi yang Anda katakan adalah Anda setuju.
madoki
1
Ya, ASM itu menyenangkan, kecuali bahwa ASM inline biasanya pilihan yang salah bahkan untuk bermain-main. Secara teknis ini adalah pertanyaan inline-asm, jadi akan lebih baik untuk setidaknya membahas hal ini dalam jawaban Anda. Juga, ini benar-benar lebih dari sebuah komentar daripada jawaban.
Peter Cordes
OK setuju. Dulu saya hanya seorang lelaki asm tetapi itu adalah tahun 80-an.
madoki
-2

Compiler c ++ akan, setelah optimisasi pada level organisasi, menghasilkan kode yang akan memanfaatkan fungsi built-in dari cpu yang ditargetkan. HLL tidak akan pernah berlari lebih cepat atau lebih cepat dari assembler karena beberapa alasan; 1.) HLL akan dikompilasi dan di-output dengan kode Accessor, pengecekan batas dan kemungkinan dibangun dalam pengumpulan sampah (sebelumnya menangani ruang lingkup dalam perilaku OOP) semua memerlukan siklus (flips and flops). HLL melakukan pekerjaan yang sangat baik akhir-akhir ini (termasuk C ++ yang lebih baru dan yang lain seperti GO), tetapi jika mereka mengungguli assembler (yaitu kode Anda), Anda perlu berkonsultasi dengan Dokumentasi CPU - perbandingan dengan kode ceroboh tentu saja kumpulan yang tidak meyakinkan dan dikompilasi seperti assembler semua penyelesaian turun ke op-code HLL abstrak rincian dan tidak menghilangkan mereka yang Anda aplikasi tidak akan berjalan jika itu bahkan dikenali oleh OS host.

Sebagian besar kode assembler (terutama objek) adalah output sebagai "tanpa kepala" untuk dimasukkan ke dalam format yang dapat dieksekusi lainnya dengan proses yang jauh lebih sedikit diperlukan sehingga akan jauh lebih cepat, tetapi jauh lebih tidak aman; jika suatu executable adalah output oleh assembler (NAsm, YAsm; dll.) itu masih akan berjalan lebih cepat sampai benar-benar cocok dengan kode HLL dalam fungsionalitas kemudian hasilnya mungkin ditimbang secara akurat.

Memanggil objek kode berbasis assembler dari HLL dalam format apa pun akan secara inheren menambahkan overhead pemrosesan juga di samping panggilan ruang memori menggunakan memori yang dialokasikan secara global untuk tipe data variabel / konstan (ini berlaku untuk LLL dan HLL). Ingatlah bahwa hasil akhirnya menggunakan CPU pada akhirnya sebagai api dan abi relatif terhadap perangkat keras (opcode) dan keduanya, assembler dan "kompiler HLL" pada dasarnya / pada dasarnya identik dengan satu-satunya pengecualian adalah keterbacaan (tata bahasa).

Aplikasi konsol Halo dunia dalam assembler menggunakan FAsm adalah 1,5 KB (dan ini di Windows bahkan lebih kecil di FreeBSD dan Linux) dan mengungguli apa pun yang dapat dibuang GCC pada hari terbaiknya; alasannya adalah padding implisit dengan nops, validasi akses dan pemeriksaan batas untuk beberapa nama. Tujuan sebenarnya adalah lib HLL yang bersih dan kompiler yang dioptimalkan yang menargetkan cpu dengan cara "hardcore" dan sebagian besar dilakukan akhir-akhir ini (akhirnya). GCC tidak lebih baik dari YAsm - itu adalah praktik pengkodean dan pemahaman pengembang yang dipertanyakan dan "optimasi" muncul setelah eksplorasi pemula dan pelatihan sementara & pengalaman.

Compiler harus menautkan dan merakit untuk output dalam opcode yang sama dengan assembler karena kode-kode itu adalah semua yang CPU kecuali (CISC atau RISC [PIC juga]). YAsm dioptimalkan dan dibersihkan banyak pada NAsm awal pada akhirnya mempercepat semua output dari assembler itu, tetapi bahkan YAsm masih, seperti NAsm, menghasilkan executable dengan dependensi eksternal yang menargetkan perpustakaan OS atas nama pengembang sehingga jarak tempuh dapat bervariasi. Sebagai penutup C ++ berada pada titik yang luar biasa dan jauh lebih aman daripada assembler untuk 80+ persen terutama di sektor komersial ...

Gagak
sumber
1
C dan C ++ tidak memiliki batas pemeriksaan kecuali Anda memintanya, dan tidak ada pengumpulan sampah kecuali Anda menerapkannya sendiri atau menggunakan perpustakaan. Pertanyaan sebenarnya adalah apakah kompiler membuat loop yang lebih baik (dan optimisasi global) daripada manusia. Biasanya ya, kecuali manusia benar - benar tahu apa yang mereka lakukan dan menghabiskan banyak waktu untuk itu .
Peter Cordes
1
Anda dapat membuat executable statis menggunakan NASM atau YASM (tidak ada kode eksternal). Keduanya dapat menghasilkan dalam format biner datar, sehingga Anda bisa membuatnya merakit header ELF sendiri jika Anda benar-benar ingin tidak berjalan ld, tetapi tidak ada bedanya kecuali Anda mencoba untuk benar-benar mengoptimalkan ukuran file (bukan hanya ukuran file). segmen teks). Lihat Tutorial Whirlwind tentang Membuat Executables ELF yang Sangat Berlebihan untuk Linux .
Peter Cordes
1
Mungkin Anda berpikir tentang C #, atau std::vectordikompilasi dalam mode debug. Array C ++ tidak seperti itu. Kompiler dapat memeriksa hal-hal pada waktu kompilasi, tetapi kecuali jika Anda mengaktifkan opsi pengerasan ekstra, tidak ada pemeriksaan run-time. Lihat misalnya fungsi yang menambah 1024 elemen pertama dari sebuah int array[]argumen. Output asm tidak memiliki pemeriksaan runtime: godbolt.org/g/w1HF5t . Yang didapat hanyalah sebuah pointer rdi, tanpa informasi ukuran. Terserah programmer untuk menghindari perilaku tidak terdefinisi dengan tidak pernah memanggilnya dengan array yang lebih kecil dari 1024.
Peter Cordes
1
Apa pun yang Anda bicarakan bukanlah array C ++ biasa (alokasikan dengan new, hapus secara manual dengan delete, tanpa batas memeriksa). Anda dapat menggunakan C ++ untuk menghasilkan kode asm / machine shitty yang membengkak (seperti kebanyakan perangkat lunak), tapi itu kesalahan programmer, bukan C ++. Anda bahkan dapat menggunakan allocauntuk mengalokasikan ruang stack sebagai array.
Peter Cordes
1
Tautkan contoh di gcc.godbolt.org untuk g++ -O3membuat kode pemeriksaan batas untuk larik sederhana, atau melakukan apa pun yang Anda bicarakan. C ++ membuatnya lebih mudah untuk menghasilkan binari yang membengkak (dan sebenarnya Anda harus berhati-hati untuk tidak jika Anda mengincar kinerja), tetapi itu tidak bisa dihindari. Jika Anda memahami bagaimana C ++ mengkompilasi ke asm, Anda bisa mendapatkan kode yang hanya sedikit lebih buruk daripada yang bisa Anda tulis dengan tangan, tetapi dengan inlining dan propagasi konstan pada skala yang lebih besar daripada yang bisa Anda kelola dengan tangan.
Peter Cordes
-3

Perakitan bisa lebih cepat jika kompiler Anda menghasilkan banyak kode dukungan OO .

Edit:

Untuk downvoters: OP menulis "haruskah saya ... fokus pada C ++ dan melupakan bahasa assembly?" dan saya mendukung jawaban saya. Anda selalu perlu mengawasi kode yang dihasilkan OO, terutama saat menggunakan metode. Tidak lupa tentang bahasa rakitan berarti Anda akan secara berkala meninjau rakitan yang dihasilkan oleh kode OO yang saya yakini sebagai keharusan untuk menulis perangkat lunak yang berkinerja baik.

Sebenarnya, ini berkaitan dengan semua kode yang dapat dikompilasi, bukan hanya OO.

Olof Forshell
sumber
2
-1: Saya tidak melihat fitur OO digunakan. Argumen Anda sama dengan "perakitan juga bisa lebih cepat jika kompiler Anda menambahkan satu juta NOP."
Sjoerd
Saya tidak jelas, ini sebenarnya pertanyaan C. Jika Anda menulis kode C untuk kompiler C ++ Anda tidak menulis kode C ++ dan Anda tidak akan mendapatkan barang OO. Setelah Anda mulai menulis dalam C ++ nyata, menggunakan hal-hal OO Anda harus sangat berpengetahuan untuk mendapatkan kompiler untuk tidak menghasilkan kode dukungan OO.
Olof Forshell
jadi jawaban Anda bukan tentang pertanyaan itu? (Juga, klarifikasi masuk dalam jawaban, bukan komentar. Komentar dapat dihapus kapan saja tanpa pemberitahuan, pemberitahuan, atau riwayat.
Mooing Duck
1
Tidak yakin apa yang Anda maksud dengan "kode dukungan" OO. Tentu saja, jika Anda menggunakan banyak RTTI dan sejenisnya, kompiler harus membuat banyak instruksi tambahan untuk mendukung fitur-fitur tersebut - tetapi masalah apa pun yang cukup tinggi untuk meratifikasi penggunaan RTTI terlalu kompleks untuk dapat dituliskan secara layak dalam perakitan . Apa yang dapat Anda lakukan, tentu saja, adalah menulis hanya antarmuka abstrak di luar sebagai OO, mengirim ke kode prosedural murni yang dioptimalkan kinerja di mana itu penting. Tetapi, tergantung pada aplikasinya, C, Fortran, CUDA atau cukup C ++ tanpa warisan virtual mungkin lebih baik daripada perakitan di sini.
leftaroundtentang
2
Paling tidak sangat tidak mungkin. Ada sesuatu dalam C ++ yang disebut aturan overhead nol, dan ini berlaku sebagian besar waktu. Pelajari lebih lanjut tentang OO - Anda akan menemukan bahwa pada akhirnya itu meningkatkan keterbacaan kode Anda, meningkatkan kualitas kode, meningkatkan kecepatan pengkodean, meningkatkan ketahanan. Juga untuk tertanam - tetapi gunakan C ++ karena memberi Anda lebih banyak kontrol, tertanam + OO cara Java akan dikenakan biaya.
Zane