Apakah kompiler C ++ menghapus / mengoptimalkan tanda kurung yang tidak berguna?

19

Akankah kodenya

int a = ((1 + 2) + 3); // Easy to read

jalankan lebih lambat dari

int a = 1 + 2 + 3; // (Barely) Not quite so easy to read

atau kompiler modern cukup pintar untuk menghapus / mengoptimalkan tanda kurung "tidak berguna".

Ini mungkin tampak seperti masalah optimisasi yang sangat kecil, tetapi memilih C ++ daripada C # / Java / ... adalah semua tentang optimasi (IMHO).

Serge
sumber
9
Saya pikir C # dan Java akan mengoptimalkan itu juga. Saya percaya ketika mereka mem-parsing dan membuat AST, mereka hanya akan menghapus hal-hal tidak berguna yang jelas seperti itu.
Farid Nouri Neshat
5
Semua yang saya baca poin ke kompilasi JIT dengan mudah memberikan kompilasi sebelumnya uang, sehingga itu saja bukan argumen yang sangat menarik. Anda memunculkan pemrograman game - alasan sebenarnya untuk mendukung kompilasi di muka adalah bahwa hal itu dapat diprediksi - dengan kompilasi JIT Anda tidak pernah tahu kapan kompiler akan memulai dan mencoba memulai kompilasi kode. Tetapi saya akan mencatat bahwa kompilasi sebelumnya ke kode asli tidak eksklusif satu sama lain dengan pengumpulan sampah, lihat misalnya Standar ML dan D. Dan saya telah melihat argumen meyakinkan bahwa pengumpulan sampah lebih efisien ...
Doval
7
... daripada RAII dan smart pointer, jadi ini lebih merupakan pertanyaan untuk mengikuti jalur yang dikalahkan (C ++) vs jalur yang relatif belum dibaca dalam melakukan pemrograman game dalam bahasa-bahasa tersebut. Saya juga mencatat bahwa khawatir tentang tanda kurung itu gila - saya melihat dari mana Anda berasal, tapi itu optimasi mikro yang konyol. Pilihan struktur data dan algoritma dalam program Anda pasti akan mendominasi kinerja, bukan hal sepele seperti itu.
Doval
6
Um ... Optimalisasi seperti apa yang Anda harapkan, tepatnya? Jika Anda berbicara tentang analisis statis, dalam sebagian besar bahasa yang saya tahu, ini akan digantikan oleh hasil yang diketahui secara statis (implementasi berbasis LLVM bahkan menegakkan ini, AFAIK). Jika Anda berbicara tentang urutan eksekusi, itu tidak masalah, karena ini adalah operasi yang sama dan tanpa efek samping. Penambahan membutuhkan dua operan. Dan jika Anda menggunakan ini untuk membandingkan C ++, Java dan C # mengenai kinerja, sepertinya Anda tidak memiliki gagasan yang jelas tentang apa itu optimasi dan bagaimana cara kerjanya, jadi Anda harus fokus belajar itu sebagai gantinya.
Theodoros Chatzigiannakis
5
Saya bertanya-tanya mengapa a) Anda menganggap ungkapan kurung lebih mudah dibaca (bagi saya itu hanya terlihat jelek, menyesatkan (mengapa mereka menekankan urutan khusus ini? Tidakkah harus asosiatif di sini?) Dan kikuk) b) mengapa Anda akan berpikir tanpa tanda kurung itu mungkin berkinerja lebih baik (parens jelas parsing lebih mudah untuk mesin daripada harus alasan tentang fixture operator. Seperti yang dikatakan Marc van Leuwen, ini sama sekali tidak memiliki pengaruh pada runtime).
leftaround sekitar

Jawaban:

87

Kompiler sebenarnya tidak pernah menyisipkan atau menghapus tanda kurung; itu hanya membuat pohon parse (di mana tidak ada tanda kurung) yang sesuai dengan ekspresi Anda, dan dalam melakukannya harus menghormati tanda kurung yang Anda tulis. Jika Anda sepenuhnya mengurung ekspresi Anda maka itu akan segera menjadi jelas bagi pembaca manusia apa pohon parse itu; jika Anda pergi ke ekstrim menempatkan kurung terang-terangan berlebihan seperti dalam int a = (((0)));maka Anda akan menyebabkan beberapa tekanan yang tidak berguna pada neuron pembaca sementara juga membuang-buang beberapa siklus di pengurai, namun mengubah pohon parsing yang dihasilkan (dan karena itu kode yang dihasilkan ) sedikit pun.

Jika Anda tidak menulis tanda kurung, maka parser masih harus melakukan tugasnya membuat pohon parse, dan aturan untuk operator diutamakan dan asosiatif mengatakan dengan tepat pohon parse mana yang harus dibangun. Anda dapat mempertimbangkan aturan-aturan itu sebagai memberi tahu kompiler yang mana (implisit) tanda kurung harus dimasukkan ke dalam kode Anda, meskipun parser sebenarnya tidak pernah berurusan dengan tanda kurung dalam kasus ini: itu hanya telah dibangun untuk menghasilkan pohon parse yang sama seolah-olah tanda kurung hadir di tempat-tempat tertentu. Jika Anda menempatkan tanda kurung di tempat-tempat itu, seperti int a = (1+2)+3;(asosiatifitas +adalah di sebelah kiri) maka parser akan tiba pada hasil yang sama dengan rute yang sedikit berbeda. Jika Anda memasukkan tanda kurung yang berbeda seperti dalamint a = 1+(2+3);maka Anda memaksa parse tree yang berbeda, yang mungkin akan menyebabkan kode yang berbeda untuk dihasilkan (meskipun mungkin tidak, karena kompiler dapat menerapkan transformasi setelah membangun parse tree, selama efek mengeksekusi kode yang dihasilkan tidak akan pernah berbeda untuk Itu). Andaikata ada perbedaan dalam kode resuting, tidak ada yang secara umum dapat dikatakan lebih efisien; titik yang paling penting tentu saja adalah bahwa sebagian besar pohon parse tidak memberikan ekspresi yang setara secara matematis, jadi membandingkan kecepatan eksekusi mereka di samping intinya: orang harus menulis ekspresi yang memberikan hasil yang tepat.

Jadi hasilnya adalah: gunakan tanda kurung sesuai kebutuhan untuk kebenaran, dan sesuai keinginan untuk keterbacaan; jika berlebihan mereka tidak memiliki efek sama sekali pada kecepatan eksekusi (dan efek yang dapat diabaikan pada waktu kompilasi).

Dan semua ini tidak ada hubungannya dengan optimasi , yang muncul setelah pohon parse dibangun, sehingga tidak dapat mengetahui bagaimana pohon parse dibangun. Ini berlaku tanpa perubahan dari kompiler yang paling tua dan paling bodoh ke yang paling pintar dan paling modern. Hanya dalam bahasa yang diinterpretasikan (di mana "waktu kompilasi" dan "waktu eksekusi" adalah kebetulan) mungkin ada hukuman untuk kurung yang berlebihan, tetapi meskipun demikian saya pikir sebagian besar bahasa tersebut diatur sehingga setidaknya fase parsing dilakukan hanya sekali untuk setiap pernyataan (menyimpan beberapa bentuk yang sudah diurai untuk dieksekusi).

Marc van Leeuwen
sumber
s / oldes / tertua /. Jawaban bagus, +1.
David Conrad
23
Peringatan total nitpick: "Pertanyaannya tidak terlalu bagus." - Saya tidak setuju, menganggap "well put" berarti "dengan jelas menyarankan celah pengetahuan mana yang ingin diisi oleh pertanyaan tersebut". Pada dasarnya pertanyaannya adalah "mengoptimalkan untuk X, memilih A atau B, dan mengapa? Apa yang terjadi di bawahnya?", Yang setidaknya bagi saya menunjukkan dengan sangat jelas apa kesenjangan pengetahuan itu. Kelemahan dalam pertanyaan, yang dengan tepat Anda tunjukkan dan atasi dengan sangat baik, adalah bahwa itu dibangun pada model mental yang salah.
Jonas Kölker
Sebab a = b + c * d;, a = b + (c * d);akan menjadi kurung redundan [tidak berbahaya]. Jika mereka membantu Anda membuat kode lebih mudah dibaca, baiklah. a = (b + c) * d;akan menjadi tanda kurung non-berlebihan - mereka benar-benar mengubah pohon parse yang dihasilkan dan memberikan hasil yang berbeda. Sangat legal untuk dilakukan (pada kenyataannya, perlu), tetapi mereka tidak sama dengan pengelompokan default tersirat.
Phil Perry
1
@OrangeDog benar, itu memalukan komentar Ben mengeluarkan beberapa orang yang suka mengklaim VM lebih cepat daripada asli.
gbjbaanb
1
@ JonasKölker: Kalimat pembuka saya sebenarnya merujuk pada pertanyaan sebagaimana dirumuskan dalam judul: seseorang tidak dapat menjawab pertanyaan tentang apakah kompiler memasukkan atau menghapus tanda kurung, karena itu didasarkan pada kesalahpahaman tentang bagaimana kompiler beroperasi. Tetapi saya setuju bahwa cukup jelas kesenjangan pengetahuan mana yang perlu diatasi.
Marc van Leeuwen
46

Tanda kurung ada di sana hanya untuk keuntungan Anda - bukan kompiler. Kompiler akan membuat kode mesin yang benar untuk mewakili pernyataan Anda.

FYI, kompiler cukup pintar untuk mengoptimalkannya sepenuhnya jika bisa. Dalam contoh Anda, ini akan berubah menjadi int a = 6;pada waktu kompilasi.

gbjbaanb
sumber
9
Tentu saja - tempelkan tanda kurung sebanyak yang Anda suka dan biarkan kompiler melakukan kerja keras untuk mengetahui apa yang Anda inginkan :)
gbjbaanb
23
@Memunculkan pemrograman yang benar lebih tentang keterbacaan kode Anda daripada tentang kinerja. Anda akan membenci diri sendiri tahun depan ketika Anda harus men-debug crash dan Anda hanya memiliki kode "dioptimalkan" untuk pergi.
ratchet freak
1
@ scratchetfreak, Anda benar, tetapi saya juga tahu bagaimana mengomentari kode saya. int a = 6; // = (1 + 2) + 3
Serge
20
@Jadi, saya tahu itu tidak akan bertahan setelah satu tahun tweak, dalam waktu komentar dan kode akan keluar dari sinkronisasi dan kemudian Anda berakhir denganint a = 8;// = 2*3 + 5
ratchet freak
21
atau dari www.thedailywtf.com:int five = 7; //HR made us change this to six...
Mooing Duck
23

Jawaban untuk pertanyaan yang sebenarnya Anda tanyakan adalah tidak, tetapi jawaban untuk pertanyaan yang ingin Anda tanyakan adalah ya. Menambahkan tanda kurung tidak memperlambat kode.

Anda mengajukan pertanyaan tentang pengoptimalan, tetapi kurung tidak ada hubungannya dengan pengoptimalan. Kompiler menerapkan berbagai teknik pengoptimalan dengan tujuan meningkatkan ukuran atau kecepatan kode yang dihasilkan (kadang-kadang keduanya). Misalnya, mungkin mengambil ekspresi A ^ 2 (A kuadrat) dan menggantinya dengan A x A (A dikalikan dengan sendirinya) jika itu lebih cepat. Jawabannya di sini adalah tidak, kompiler tidak melakukan apa pun yang berbeda dalam fase optimisasi tergantung pada apakah tanda kurung hadir tidak.

Saya pikir Anda bermaksud bertanya apakah kompiler masih menghasilkan kode yang sama jika Anda menambahkan tanda kurung yang tidak perlu ke ekspresi, di tempat yang menurut Anda dapat meningkatkan keterbacaan. Dengan kata lain, jika Anda menambahkan tanda kurung adalah kompiler yang cukup pintar untuk mengeluarkannya kembali daripada menghasilkan kode yang lebih buruk. Jawabannya adalah ya, selalu.

Biarkan saya mengatakannya dengan hati-hati. Jika Anda menambahkan tanda kurung ke ekspresi yang benar-benar tidak perlu (tidak memiliki efek apa pun pada arti atau urutan evaluasi ekspresi), kompiler akan secara diam-diam membuangnya dan menghasilkan kode yang sama.

Namun, ada ekspresi tertentu di mana kurung yang tampaknya tidak perlu benar-benar akan mengubah urutan evaluasi ekspresi dan dalam hal itu kompiler akan menghasilkan kode untuk memberlakukan apa yang sebenarnya Anda tulis, yang mungkin berbeda dari yang Anda inginkan. Berikut ini sebuah contoh. Jangan lakukan ini!

short int a = 30001, b = 30002, c = 30003;
int d = -a + b + c;    // ok
int d = (-a + b) + c;  // ok, same code
int d = (-a + b + c);  // ok, same code
int d = ((((-a + b)) + c));  // ok, same code
int d = -a + (b + c);  // undefined behaviour, different code

Jadi, tambahkan tanda kurung jika Anda mau, tetapi pastikan itu benar-benar tidak perlu!

Saya tidak pernah melakukan. Ada risiko kesalahan tanpa manfaat nyata.


Catatan kaki: perilaku unsigned terjadi ketika ekspresi integer yang ditandatangani mengevaluasi nilai yang berada di luar rentang yang dapat diekspresikan, dalam hal ini -32767 hingga +32767. Ini adalah topik yang kompleks, di luar jangkauan untuk jawaban ini.

david.pfx
sumber
Perilaku tidak terdefinisi di baris terakhir adalah karena pendek yang ditandatangani hanya memiliki 15 bit setelah tanda, jadi ukuran maksimal 32.767, kan? Dalam contoh sepele itu, kompiler harus memperingatkan tentang overflow, kan? Memberi +1 untuk contoh penghitung, bagaimanapun juga. Jika mereka parameter untuk suatu fungsi, Anda tidak akan mendapatkan peringatan. Selain itu, jika abenar-benar tidak ditandatangani, memimpin perhitungan Anda dengan -a + bmudah bisa meluap juga, jika anegatif dan bpositif.
Patrick M
@ Patrickrick: lihat edit. Perilaku tidak terdefinisi berarti kompiler dapat melakukan apa yang disukainya, termasuk mengeluarkan peringatan, atau tidak. Aritmatika yang tidak ditandatangani tidak menghasilkan UB, tetapi direduksi menjadi kekuatan dua yang lebih tinggi berikutnya.
david.pfx
Ekspresi (b+c)di baris terakhir akan mempromosikan argumennya int, jadi kecuali jika compiler mendefinisikan intmenjadi 16 bit (baik karena itu kuno atau menargetkan mikrokontroler kecil) baris terakhir akan sangat sah.
supercat
@ supercat: Saya rasa tidak. Jenis umum dan jenis hasilnya harus int pendek. Jika itu bukan sesuatu yang pernah ditanyakan, mungkin Anda ingin memposting pertanyaan?
david.pfx
@ david.pfx: Aturan promosi aritmatika C cukup jelas: semua yang lebih kecil daripada intdipromosikan intkecuali tipe itu tidak dapat mewakili semua nilainya dalam hal mana ia dipromosikan unsigned int. Penyusun dapat mengabaikan promosi jika semua perilaku yang ditetapkan akan sama seperti jika promosi dimasukkan . Pada mesin di mana tipe 16-bit berperilaku sebagai cincin aljabar abstrak pembungkus, (a + b) + c dan + (b + c) akan sama. Namun, jika inttipe 16-bit yang terjebak pada overflow, maka akan ada kasus di mana salah satu ekspresi ...
supercat
7

Kurung hanya ada agar Anda dapat memanipulasi urutan prioritas operator. Setelah dikompilasi, tanda kurung tidak ada lagi karena run-time tidak membutuhkannya. Proses kompilasi menghilangkan semua tanda kurung, spasi dan gula sintaksis lain yang Anda dan saya butuhkan dan mengubah semua operator menjadi sesuatu yang [jauh] lebih sederhana bagi komputer untuk dieksekusi.

Jadi, di mana Anda dan saya mungkin melihat ...

  • "int a = ((1 + 2) + 3);"

... kompiler mungkin memancarkan sesuatu yang lebih seperti ini:

  • Char [1] :: "a"
  • Int32 :: DeclareStackVariable ()
  • Int32 :: 0x00000001
  • Int32 :: 0x00000002
  • Int32 :: Tambah ()
  • Int32 :: 0x00000003
  • Int32 :: Tambah ()
  • Int32 :: AssignToVariable ()
  • void :: DiscardResult ()

Program dijalankan dengan mulai dari awal dan mengeksekusi setiap instruksi secara bergantian.
Prioritas operator sekarang "pertama datang, pertama dilayani".
Semuanya diketik dengan kuat, karena kompiler mengerjakan semua itu saat sedang merobek sintaksis aslinya.

OK, tidak seperti barang-barang yang Anda dan saya tangani, tapi kemudian kita tidak menjalankannya!

Phill W.
sumber
4
Tidak ada kompiler C ++ tunggal yang akan menghasilkan sesuatu yang bahkan jauh seperti ini. Umumnya mereka menghasilkan kode CPU yang sebenarnya, dan perakitan tidak terlihat seperti ini.
MSalters
3
maksudnya di sini adalah untuk menunjukkan perbedaan struktur antara kode seperti yang ditulis dan output yang dikompilasi. bahkan di sini kebanyakan orang tidak akan bisa membaca kode atau perakitan mesin yang sebenarnya
DHall
@ MSalters Nitpicking dentang akan memancarkan 'sesuatu seperti itu' jika Anda memperlakukan LLVM ISA sebagai 'sesuatu seperti itu' (itu SSA bukan berbasis stack). Mengingat bahwa dimungkinkan untuk menulis backend JVM untuk LLVM dan JVM ISA (AFAIK) adalah clang berbasis-> llvm-> JVM akan terlihat sangat mirip.
Maciej Piechotka
Saya tidak berpikir LLVM ISA memiliki kemampuan untuk mendefinisikan variabel nama stack menggunakan literal string runtime (hanya dua instruksi pertama). Ini serius mencampur run-time dan compile-time. Perbedaannya penting karena pertanyaan ini persis tentang kebingungan itu.
MSalters
6

Tergantung apakah itu floating point atau tidak:

  • Dalam penambahan aritmatika floating point tidak asosiatif sehingga optimizer tidak dapat menyusun ulang operasi (kecuali jika Anda menambahkan saklar kompiler fastmath).

  • Dalam operasi integer, mereka dapat disusun ulang.

Dalam contoh Anda, keduanya akan menjalankan waktu yang sama persis karena keduanya akan dikompilasi dengan kode yang sama persis (penambahan dievaluasi dari kiri ke kanan).

Namun bahkan java dan C # akan dapat mengoptimalkannya, mereka hanya akan melakukannya saat runtime.

ratchet freak
sumber
+1 untuk mengemukakan bahwa operasi floating point tidak asosiatif.
Doval
Dalam contoh pertanyaan, kurung tidak mengubah asosiatif default (kiri), jadi titik ini diperdebatkan.
Marc van Leeuwen
1
Tentang kalimat terakhir, saya kira tidak. Dalam java a dan c # kompiler akan menghasilkan bytecode / IL yang dioptimalkan. Runtime tidak terpengaruh.
Stefano Altieri
IL tidak bekerja pada ekspresi semacam ini, instruksi mengambil sejumlah nilai tertentu dari tumpukan, dan mengembalikan sejumlah nilai tertentu (umumnya 0 atau 1) ke tumpukan. Berbicara tentang hal semacam ini yang dioptimalkan saat runtime di C # adalah omong kosong.
Jon Hanna
6

Kompiler C ++ khas diterjemahkan ke kode mesin, bukan C ++ itu sendiri . Itu menghilangkan parens yang tidak berguna, ya, karena pada saat itu selesai, tidak ada parens sama sekali. Kode mesin tidak berfungsi seperti itu.

Spooniest
sumber
5

Kedua kode berakhir dengan kode keras 6:

movl    $6, -4(%rbp)

Periksa sendiri di sini

MonoThreaded
sumber
2
Apa itu assembly.ynh.io thingy?
Peter Mortensen
Ini adalah kompiler semu yang mengubah kode C dalam perakitan x86 online
MonoThreaded
1

Tidak, tapi ya, tapi mungkin, tapi mungkin sebaliknya, tapi tidak.

Seperti yang telah ditunjukkan orang, (dengan asumsi bahasa di mana penambahan adalah asosiatif-kiri, seperti C, C ++, C # atau Java) ekspresi ((1 + 2) + 3)tersebut persis sama dengan 1 + 2 + 3. Mereka berbeda cara menulis sesuatu dalam kode sumber, yang akan memiliki efek nol pada kode mesin atau kode byte yang dihasilkan.

Apa pun hasilnya, ini akan menjadi instruksi untuk mis. Menambah dua register dan kemudian menambahkan yang ketiga, atau mengambil dua nilai dari tumpukan, menambahkannya, mendorongnya kembali, lalu mengambilnya dan yang lain dan menambahkannya, atau menambahkan tiga register di operasi tunggal, atau cara lain untuk menjumlahkan tiga angka tergantung pada apa yang paling masuk akal di tingkat berikutnya (kode mesin atau kode byte). Dalam kasus kode byte, yang pada gilirannya kemungkinan akan mengalami penataan ulang yang serupa di dalam hal itu misalnya IL yang setara dengan ini (yang akan menjadi serangkaian beban ke tumpukan, dan pasangan popping untuk menambah dan kemudian mendorong kembali hasilnya) tidak akan menghasilkan salinan langsung dari logika itu di tingkat kode mesin, tetapi sesuatu yang lebih masuk akal untuk mesin yang bersangkutan.

Tetapi ada sesuatu yang lebih dari pertanyaan Anda.

Dalam hal setiap kompiler C, C ++, Java, atau C # yang waras, saya mengharapkan hasil dari kedua pernyataan yang Anda berikan memiliki hasil yang sama persis seperti:

int a = 6;

Mengapa kode yang dihasilkan harus membuang waktu mengerjakan matematika pada literal? Tidak ada perubahan pada keadaan program akan berhenti hasilnya dari 1 + 2 + 3menjadi 6, sehingga ini apa yang harus pergi dalam kode dieksekusi. Memang, mungkin bahkan tidak itu (tergantung pada apa yang Anda lakukan dengan 6 itu, mungkin kita bisa membuang semuanya; dan bahkan C # dengan filosofi itu "jangan mengoptimalkan, karena jitter akan mengoptimalkan ini pula" akan menghasilkan sama int a = 6atau hanya membuang semuanya sebagai tidak perlu).

Namun ini menuntun kami ke kemungkinan perpanjangan pertanyaan Anda. Pertimbangkan yang berikut ini:

int a = (b - 2) / 2;
/* or */
int a = (b / 2)--;

dan

int c;
if(d < 100)
  c = 0;
else
  c = d * 31;
/* or */
int c = d < 100 ? 0 : d * 32 - d
/* or */
int c = d < 100 && d * 32 - d;
/* or */
int c = (d < 100) * (d * 32 - d);

(Catatan, dua contoh terakhir ini tidak valid C #, sedangkan yang lainnya di sini adalah, dan mereka valid dalam C, C ++ dan Java.)

Di sini sekali lagi kita memiliki kode yang sama persis dalam hal output. Karena mereka bukan ekspresi konstan, mereka tidak akan dihitung pada waktu kompilasi. Mungkin satu bentuk lebih cepat dari yang lain. Mana yang lebih cepat? Itu akan tergantung pada prosesor dan mungkin pada beberapa perbedaan yang agak sewenang-wenang dalam keadaan (terutama karena jika seseorang lebih cepat, itu tidak akan jauh lebih cepat).

Dan mereka tidak sepenuhnya tidak terkait dengan pertanyaan Anda, karena kebanyakan tentang perbedaan dalam urutan di mana sesuatu dilakukan secara konseptual .

Di masing-masing dari mereka, ada alasan untuk curiga bahwa satu mungkin lebih cepat daripada yang lain. Dekremen tunggal mungkin memiliki instruksi khusus, jadi (b / 2)--memang bisa lebih cepat daripada (b - 2) / 2. d * 32mungkin bisa diproduksi lebih cepat dengan mengubahnya d << 5jadi membuat d * 32 - dlebih cepat daripada d * 31. Perbedaan antara dua yang terakhir sangat menarik; satu memungkinkan beberapa pemrosesan dilewati dalam beberapa kasus, tetapi yang lain menghindari kemungkinan salah prediksi cabang.

Jadi, ini meninggalkan kita dengan dua pertanyaan: 1. Apakah satu sebenarnya lebih cepat dari yang lain? 2. Apakah kompiler akan mengkonversi lebih lambat menjadi lebih cepat?

Dan jawabannya adalah 1. Tergantung. 2. Mungkin.

Atau untuk memperluas, itu tergantung karena itu tergantung pada prosesor yang bersangkutan. Tentu saja ada prosesor di mana setara kode mesin naif dari satu akan lebih cepat daripada kode mesin naif yang setara dengan yang lain. Selama sejarah komputasi elektronik, tidak ada satu yang selalu lebih cepat, baik (elemen mis-prediksi cabang khususnya tidak relevan bagi banyak ketika CPU non-pipa lebih umum).

Dan mungkin, karena ada banyak optimisasi berbeda yang dilakukan oleh kompiler (dan kegugupan, dan mesin skrip), dan sementara beberapa mungkin diamanatkan dalam kasus-kasus tertentu, umumnya kita akan dapat menemukan beberapa bagian dari kode yang secara logis setara yang bahkan kompiler yang paling naif memiliki hasil yang persis sama dan beberapa bagian dari kode yang setara secara logis di mana bahkan yang paling canggih menghasilkan kode yang lebih cepat untuk satu daripada yang lain (bahkan jika kita harus menulis sesuatu yang benar-benar patologis hanya untuk membuktikan maksud kita).

Ini mungkin tampak seperti masalah optimasi yang sangat kecil,

Tidak. Bahkan dengan perbedaan yang lebih rumit daripada yang saya berikan di sini, sepertinya masalah kecil sekali yang tidak ada hubungannya dengan optimasi. Jika ada, itu masalah pesimisasi karena Anda curiga semakin sulit dibaca ((1 + 2) + 3bisa lebih lambat daripada lebih mudah dibaca 1 + 2 + 3.

tetapi memilih C ++ daripada C # / Java / ... adalah semua tentang optimasi (IMHO).

Jika itu yang benar-benar memilih C ++ daripada C # atau Java adalah "semua tentang" Saya akan mengatakan orang harus membakar salinan Stroustrup dan ISO / IEC 14882 mereka dan membebaskan ruang kompiler C ++ mereka untuk meninggalkan ruang untuk lebih banyak MP3 atau sesuatu.

Bahasa-bahasa ini memiliki keunggulan yang berbeda satu sama lain.

Salah satunya adalah bahwa C ++ umumnya masih lebih cepat dan lebih ringan pada penggunaan memori. Ya, ada contoh di mana C # dan / atau Java lebih cepat dan / atau memiliki penggunaan memori aplikasi yang lebih baik seumur hidup, dan ini menjadi lebih umum karena teknologi yang terlibat meningkat, tetapi kita masih bisa mengharapkan program rata-rata yang ditulis dalam C ++ menjadi dieksekusi lebih kecil yang melakukan tugasnya lebih cepat dan menggunakan lebih sedikit memori daripada yang setara di salah satu dari kedua bahasa.

Ini bukan optimasi.

Optimalisasi kadang-kadang berarti "membuat segalanya berjalan lebih cepat". Ini bisa dimengerti, karena sering kali ketika kita benar-benar berbicara tentang "optimasi" kita memang berbicara tentang membuat segalanya berjalan lebih cepat, dan karena itu seseorang telah menjadi singkatan untuk yang lain dan saya akui saya menyalahgunakan kata itu dengan cara saya sendiri.

Kata yang benar untuk "membuat segalanya berjalan lebih cepat" bukanlah pengoptimalan . Kata yang benar di sini adalah peningkatan . Jika Anda membuat perubahan pada suatu program dan satu-satunya perbedaan yang berarti adalah sekarang lebih cepat, tidak dioptimalkan dengan cara apa pun, itu hanya lebih baik.

Optimalisasi adalah ketika kami melakukan peningkatan dalam hal aspek tertentu dan / atau kasus tertentu. Contoh umum adalah:

  1. Sekarang lebih cepat untuk satu use case, tetapi lebih lambat untuk yang lain.
  2. Sekarang lebih cepat, tetapi menggunakan lebih banyak memori.
  3. Sekarang lebih ringan di memori, tetapi lebih lambat.
  4. Sekarang lebih cepat, tetapi lebih sulit untuk dipertahankan.
  5. Sekarang lebih mudah dirawat, tetapi lebih lambat.

Kasus-kasus seperti itu akan dibenarkan jika, misalnya:

  1. Kasus penggunaan yang lebih cepat lebih umum atau lebih parah terhambat untuk memulai.
  2. Program ini sangat lambat, dan kami memiliki banyak RAM gratis.
  3. Program ini terhenti karena menggunakan begitu banyak RAM sehingga menghabiskan lebih banyak waktu bertukar daripada menjalankan pemrosesan super cepat.
  4. Program ini sangat lambat, dan kode yang sulit dipahami didokumentasikan dengan baik dan relatif stabil.
  5. Program ini masih dapat diterima dengan cepat, dan basis kode yang lebih mudah dipahami lebih murah untuk dipelihara dan memungkinkan perbaikan lainnya agar lebih mudah dilakukan.

Tapi, kasus-kasus seperti itu juga tidak akan dibenarkan dalam skenario lain: Kode belum dibuat lebih baik oleh ukuran kualitas mutlak mutlak, itu dibuat lebih baik dalam hal tertentu yang membuatnya lebih cocok untuk penggunaan tertentu; dioptimalkan.

Dan pilihan bahasa memang berpengaruh di sini, karena kecepatan, penggunaan memori, dan keterbacaan semua dapat dipengaruhi olehnya, tetapi juga kompatibilitas dengan sistem lain, ketersediaan perpustakaan, ketersediaan runtime, kematangan runtime tersebut pada sistem operasi yang diberikan (untuk dosa-dosa saya, saya entah bagaimana akhirnya memiliki Linux dan Android sebagai OS favorit saya dan C # sebagai bahasa favorit saya, dan sementara Mono hebat, tetapi saya masih menghadapi yang ini sedikit).

Mengatakan "memilih C ++ daripada C # / Java / ... adalah semua tentang optimasi" hanya masuk akal jika Anda berpikir C ++ benar-benar menyebalkan, karena optimasi adalah tentang "lebih baik meskipun ..." tidak "lebih baik". Jika Anda berpikir C ++ lebih baik dari itu sendiri, maka hal terakhir yang Anda butuhkan adalah khawatir tentang kemungkinan kecil memilih mikro-opts. Memang, Anda mungkin lebih baik meninggalkannya sama sekali; hacker senang adalah kualitas untuk dioptimalkan juga!

Namun, jika Anda cenderung mengatakan "Saya suka C ++, dan salah satu hal yang saya sukai adalah memeras siklus tambahan", maka itu masalah yang berbeda. Itu masih merupakan kasus bahwa mikro-opts hanya layak jika mereka dapat menjadi kebiasaan refleksif (yaitu, cara Anda cenderung untuk kode secara alami akan lebih cepat lebih sering daripada lambat). Kalau tidak, mereka bahkan bukan optimasi prematur, mereka pesimis prematur yang hanya memperburuk keadaan.

Jon Hanna
sumber
0

Tanda kurung ada di sana untuk memberi tahu kompiler di mana ekspresi urutan harus dievaluasi. Kadang-kadang mereka tidak berguna (kecuali mereka meningkatkan atau memperburuk keterbacaan), karena mereka menentukan urutan yang akan digunakan. Terkadang mereka mengubah urutan. Di

int a = 1 + 2 + 3;

praktis setiap bahasa yang ada memiliki aturan bahwa jumlah dievaluasi dengan menambahkan 1 + 2, kemudian menambahkan hasilnya ditambah 3. Jika Anda menulis

int a = 1 + (2 + 3);

maka tanda kurung akan memaksakan urutan yang berbeda: Pertama menambahkan 2 + 3, kemudian menambahkan 1 ditambah hasilnya. Contoh tanda kurung Anda menghasilkan urutan yang sama dengan yang seharusnya dihasilkan. Sekarang dalam contoh ini, urutan operasi sedikit berbeda, tetapi cara penambahan integer bekerja, hasilnya sama. Di

int a = 10 - (5 - 4);

tanda kurung sangat penting; meninggalkan mereka akan mengubah hasilnya dari 9 menjadi 1.

Setelah kompiler menentukan operasi apa yang dilakukan dalam urutan apa, kurung benar-benar dilupakan. Semua yang diingat oleh kompiler pada titik ini adalah operasi mana yang harus dijalankan sesuai urutan. Jadi sebenarnya tidak ada yang bisa dioptimalkan oleh kompiler di sini, tanda kurung hilang .

gnasher729
sumber
practically every language in existence; kecuali APL: Coba (di sini) [tryapl.org] memasukkan (1-2)+3(2), 1-(2+3)(-4) dan 1-2+3(juga -4).
mulai
0

Saya setuju dengan banyak dari apa yang telah dikatakan, namun ... lengkungan berlebihan di sini adalah bahwa tanda kurung ada untuk memaksa urutan operasi ... yang benar-benar dilakukan oleh kompiler. Ya, itu menghasilkan kode mesin ... tapi, bukan itu intinya dan bukan itu yang ditanyakan.

Tanda kurung memang hilang: seperti telah dikatakan, mereka bukan bagian dari kode mesin, yang merupakan angka dan bukan hal lain. Kode assembly bukan kode mesin, ini dapat dibaca semi-manusia dan berisi instruksi berdasarkan nama - bukan opcode. Mesin menjalankan apa yang disebut opcodes - representasi numerik dari bahasa assembly.

Bahasa seperti Jawa jatuh ke dalam area di antara keduanya karena mereka hanya mengkompilasi sebagian pada mesin yang memproduksinya. Mereka dikompilasi ke mesin kode spesifik pada mesin yang menjalankannya, tetapi itu tidak membuat perbedaan untuk pertanyaan ini - tanda kurung masih hilang setelah kompilasi pertama.

jinzai
sumber
1
Saya tidak yakin ini menjawab pertanyaan. Paragraf pendukung lebih membingungkan daripada membantu. Bagaimana kompiler Java relevan dengan kompiler C ++?
Adam Zuckerman
OP bertanya apakah tanda kurungnya hilang ... Saya mengatakan bahwa itu dan lebih lanjut menjelaskan bahwa kode yang dapat dieksekusi hanyalah angka yang mewakili opcodes. Jawa dibesarkan dalam jawaban lain. Saya pikir itu menjawab pertanyaan dengan baik ... tapi itu hanya pendapat saya. Terima kasih telah membalas.
jinzai
3
Kurung tidak memaksa "urutan operasi". Mereka mengubah prioritas. Jadi, dalam a = f() + (g() + h());, compiler gratis untuk menelepon f, gdan hagar (atau dalam urutan apapun itu menyenangkan).
Alok
Saya memiliki beberapa ketidaksepakatan dengan pernyataan itu ... Anda benar-benar dapat memaksa urutan operasi dengan tanda kurung.
jinzai
0

Compiler, terlepas dari bahasa, menerjemahkan semua matematika infiks ke postfix. Dengan kata lain, ketika kompiler melihat sesuatu seperti:

((a+b)+c)

menerjemahkannya menjadi ini:

 a b + c +

Hal ini dilakukan karena sementara notasi infiks lebih mudah bagi orang untuk membaca, notasi postfix jauh lebih dekat dengan langkah aktual yang harus diambil komputer untuk menyelesaikan pekerjaan (dan karena sudah ada algoritma yang dikembangkan dengan baik untuk itu.) definisi, postfix telah menghilangkan semua masalah dengan urutan operasi atau tanda kurung, yang secara alami membuat segalanya lebih mudah ketika benar-benar menulis kode mesin.

Saya merekomendasikan artikel wikipedia tentang Notasi Polandia Mundur untuk informasi lebih lanjut tentang subjek ini.

Brian Drozd
sumber
5
Itu asumsi yang salah tentang bagaimana kompiler menerjemahkan operasi. Anda misalnya mengasumsikan mesin stack di sini. Bagaimana jika Anda memiliki prosesor vektor? bagaimana jika Anda memiliki mesin dengan register dalam jumlah besar?
Ahmed Masud