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?
sumber
Jawaban:
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.
sumber
Kode rakitan Anda tidak optimal dan dapat ditingkatkan:
loop
instruksi, yang dikenal sangat lambat pada kebanyakan CPU modern (mungkin akibat menggunakan buku rakitan kuno *)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
loop
instruksi dari buku rakitan kuno. Tapi Anda hampir tidak pernah melihatnya dalam kode dunia nyata, karena setiap kompiler di luar sana cukup pintar untuk tidak memancarkannyaloop
, Anda hanya melihatnya dalam buku-buku IMHO buruk dan ketinggalan jaman.sumber
loop
(dan banyak instruksi "usang") jika Anda mengoptimalkan untuk ukuranBahkan sebelum mempelajari perakitan, ada transformasi kode yang ada di tingkat yang lebih tinggi.
dapat diubah menjadi melalui Loop Rotation :
yang jauh lebih baik sejauh memori lokalitas berjalan.
Ini dapat dioptimalkan lebih lanjut, melakukan
a += b
X kali setara dengan melakukannyaa += X * b
sehingga kita mendapatkan:namun sepertinya pengoptimal favorit saya (LLVM) tidak melakukan transformasi ini.
[sunting] saya menemukan bahwa transformasi dilakukan jika kita memiliki
restrict
kualifikasi untukx
dany
. Memang tanpa batasan ini,x[j]
dany[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):
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.
sumber
x
dany
. Artinya, compiler tidak dapat yakin bahwa untuk semuai,j
di[0, length)
kitax + i != y + j
. Jika ada tumpang tindih, maka optimasi tidak mungkin. Bahasa C memperkenalkanrestrict
kata 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.__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 untukpmulld
: 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.Jawaban singkat: ya.
Jawaban panjang: ya, kecuali Anda benar-benar tahu apa yang Anda lakukan, dan punya alasan untuk melakukannya.
sumber
Saya telah memperbaiki kode asm saya:
Hasil untuk versi Rilis:
Kode rakitan dalam mode rilis hampir 2 kali lebih cepat daripada C ++.
sumber
xmm0
bukanmm0
), Anda akan mendapatkan speedup lain dengan faktor dua ;-)paddd xmm
(setelah memeriksa tumpang tindih antarax
dany
, karena Anda tidak menggunakanint *__restrict x
). Misalnya gcc melakukan itu: godbolt.org/z/c2JG0- . Atau setelah masukmain
, 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 mengompilasinyagcc -O3 -march=native
, Anda bisa mendapatkan 256-bit atau 512-bit vektorisasi.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.
sumber
Satu-satunya alasan untuk menggunakan bahasa rakitan saat ini adalah menggunakan beberapa fitur yang tidak dapat diakses oleh bahasa tersebut.
Ini berlaku untuk:
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.sumber
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!)
sumber
Saya telah mengubah kode asm:
Hasil untuk versi Rilis:
Kode rakitan dalam mode rilis hampir 4 kali lebih cepat daripada C ++. IMHo, kecepatan kode perakitan tergantung dari Programmer
sumber
shr ecx,2
berlebihan, karena panjang array sudah diberikanint
dan bukan dalam byte. Jadi pada dasarnya Anda mencapai kecepatan yang sama. Anda dapat mencobapaddd
jawaban dari harold, ini akan benar-benar lebih cepat.itu topik yang sangat menarik!
Saya telah mengubah MMX oleh SSE dalam kode Sasha.
Ini hasil saya:
Kode perakitan dengan SSE adalah 5 kali lebih cepat dari C ++
sumber
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:
biaya siklus lebih banyak daripada
yang melakukan hal yang sama.
Kompiler mengetahui semua trik ini dan menggunakannya.
sumber
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
y
danx
16-sejajar, dan itulength
adalah kelipatan non-nol dari 4. Itu mungkin semua tetap benar.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.
sumber
mov ecx, length, lea ecx,[ecx*4], mov eax,16... add ecx,eax
dan 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.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.
sumber
Sebagai kompiler saya akan mengganti sebuah loop dengan ukuran tetap untuk banyak tugas eksekusi.
akan menghasilkan
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.
sumber
a
volatile, ada kemungkinan bagus bahwa kompiler hanya akan melakukanint a = 13;
dari awal.Persis apa artinya. Biarkan optimasi mikro ke kompiler.
sumber
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.
sumber
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:
Kompiler untuk kode ARM 32-bit, yang diberikan di atas, kemungkinan akan membuatnya sebagai sesuatu seperti:
atau mungkin
Itu bisa dioptimalkan sedikit dalam kode rakitan tangan, seperti:
atau
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:
Jauh lebih pendek dan lebih cepat daripada kode perakitan yang dioptimalkan dengan tangan. Selanjutnya, anggap set_port_high terjadi dalam konteks:
Sama sekali tidak masuk akal ketika coding untuk sistem tertanam. Jika
set_port_high
ditulis dalam kode assembly, kompiler harus memindahkan r0 (yang menyimpan nilai balikfunction1
) 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,function1
tidak akan mengubah register tersebut, dan dengan demikian ia dapat menggantikanset_port_high
strb
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:
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.
sumber
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.
sumber
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:
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
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.
sumber
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 ...
sumber
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 .std::vector
dikompilasi 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 sebuahint array[]
argumen. Output asm tidak memiliki pemeriksaan runtime: godbolt.org/g/w1HF5t . Yang didapat hanyalah sebuah pointerrdi
, tanpa informasi ukuran. Terserah programmer untuk menghindari perilaku tidak terdefinisi dengan tidak pernah memanggilnya dengan array yang lebih kecil dari 1024.new
, hapus secara manual dengandelete
, 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 menggunakanalloca
untuk mengalokasikan ruang stack sebagai array.g++ -O3
membuat 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.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.
sumber