Apa yang mendukung klaim bahwa C ++ dapat lebih cepat daripada JVM atau CLR dengan JIT? [Tutup]

119

Tema berulang pada SE yang saya perhatikan dalam banyak pertanyaan adalah argumen yang sedang berlangsung bahwa C ++ lebih cepat dan / atau lebih efisien daripada bahasa tingkat yang lebih tinggi seperti Java. Argumen tandingannya adalah bahwa JVM atau CLR modern dapat sama efisiennya berkat JIT dan seterusnya untuk semakin banyak tugas dan bahwa C ++ semakin efisien jika Anda tahu apa yang Anda lakukan dan mengapa melakukan hal-hal dengan cara tertentu akan pantas meningkat kinerja. Itu jelas dan masuk akal.

Saya ingin tahu penjelasan dasar (jika ada hal seperti itu ...) mengapa dan bagaimana tugas-tugas tertentu lebih cepat di C ++ daripada JVM atau CLR? Apakah hanya karena C ++ dikompilasi ke dalam kode mesin sedangkan JVM atau CLR masih memiliki overhead pemrosesan kompilasi JIT pada saat run time?

Ketika saya mencoba untuk meneliti topik, semua yang saya temukan adalah argumen yang sama yang telah saya uraikan di atas tanpa informasi terperinci untuk memahami secara tepat bagaimana C ++ dapat digunakan untuk komputasi berkinerja tinggi.

Anonim, tanpa nama
sumber
Kinerja juga tergantung pada kompleksitas program.
pandu
23
Saya akan menambahkan ke "C ++ hanya lebih efisien jika Anda tahu apa yang Anda lakukan dan mengapa melakukan sesuatu dengan cara tertentu akan pantas meningkatkan kinerja." dengan mengatakan bahwa ini bukan hanya masalah pengetahuan, ini masalah waktu pengembang. Tidak selalu efisien untuk memaksimalkan pengoptimalan. Inilah sebabnya mengapa bahasa tingkat yang lebih tinggi seperti Java dan Python ada (di antara alasan lain) - untuk mengurangi jumlah waktu seorang programmer harus menghabiskan pemrograman untuk menyelesaikan tugas yang diberikan dengan mengorbankan optimasi yang sangat disesuaikan.
Joel Cornett
4
@ Joel Cornett: Saya sepenuhnya setuju. Saya jelas lebih produktif di Jawa daripada di C ++ dan saya hanya mempertimbangkan C ++ ketika saya harus menulis kode yang sangat cepat. Di sisi lain saya telah melihat kode C ++ yang ditulis dengan buruk menjadi sangat lambat: C ++ kurang berguna di tangan programmer yang tidak terampil.
Giorgio
3
Output kompilasi apa pun yang dapat diproduksi oleh JIT dapat diproduksi oleh C ++, tetapi kode yang dapat diproduksi oleh C ++ mungkin tidak harus diproduksi oleh JIT. Jadi kapabilitas dan karakteristik kinerja C ++ adalah superset dari semua bahasa tingkat yang lebih tinggi. QED
tylerl
1
@Doval Secara teknis benar, tetapi sebagai aturan Anda dapat menghitung kemungkinan faktor runtime yang mempengaruhi kinerja program di satu sisi. Biasanya tanpa menggunakan lebih dari dua jari. Jadi, kasus terburuk Anda mengirimkan banyak binari ... kecuali ternyata Anda bahkan tidak perlu melakukan itu karena potensi percepatan diabaikan, itulah sebabnya tidak ada yang mengganggu.
tylerl

Jawaban:

200

Ini semua tentang memori (bukan JIT). JIT 'keunggulan dibandingkan C' sebagian besar terbatas pada mengoptimalkan panggilan virtual atau non-virtual melalui inlining, sesuatu yang sudah dilakukan oleh CPU BTB.

Dalam mesin modern, mengakses RAM benar - benar lambat (dibandingkan dengan apa pun yang dilakukan CPU), yang berarti aplikasi yang menggunakan cache sebanyak mungkin (yang lebih mudah bila lebih sedikit memori digunakan) dapat mencapai seratus kali lebih cepat daripada yang jangan. Dan ada banyak cara di mana Java menggunakan lebih banyak memori daripada C ++ dan membuatnya lebih sulit untuk menulis aplikasi yang sepenuhnya mengeksploitasi cache:

  • Ada overhead memori minimal 8 byte untuk setiap objek, dan penggunaan objek bukan primitif diperlukan atau disukai di banyak tempat (yaitu koleksi standar).
  • String terdiri dari dua objek dan memiliki overhead 38 byte
  • UTF-16 digunakan secara internal, yang berarti bahwa setiap karakter ASCII membutuhkan dua byte alih-alih satu (Oracle JVM baru-baru ini memperkenalkan optimisasi untuk menghindari hal ini untuk string ASCII murni).
  • Tidak ada tipe referensi agregat (yaitu struct), dan pada gilirannya, tidak ada array tipe referensi agregat. Objek Java, atau array objek Java, memiliki lokalitas cache L1 / L2 yang sangat buruk dibandingkan dengan C-struct dan array.
  • Java generics menggunakan tipe-erasure, yang memiliki lokalitas cache yang buruk dibandingkan dengan tipe-instantiation.
  • Alokasi objek bersifat buram dan harus dilakukan secara terpisah untuk setiap objek, sehingga tidak mungkin bagi aplikasi untuk sengaja menata datanya dengan cara yang ramah-cache dan masih memperlakukannya sebagai data terstruktur.

Beberapa faktor lain yang berhubungan dengan memori tetapi tidak terkait cache:

  • Tidak ada alokasi tumpukan, sehingga semua data non-primitif yang bekerja dengan Anda harus ada di heap dan melalui pengumpulan sampah (beberapa JIT baru-baru ini melakukan alokasi tumpukan di belakang layar dalam kasus tertentu).
  • Karena tidak ada tipe referensi agregat, tidak ada tumpukan yang melewati tipe referensi agregat. (Pikirkan lewat efisien dari argumen Vektor)
  • Pengumpulan sampah dapat merusak konten cache L1 / L2, dan GC stop-the-world menghentikan sementara interaktivitas yang merugikan.
  • Konversi antara tipe data selalu membutuhkan penyalinan; Anda tidak dapat mengambil pointer ke sekelompok byte yang Anda dapatkan dari soket dan menafsirkannya sebagai float.

Beberapa dari hal-hal ini adalah pengorbanan (tidak harus melakukan manajemen memori manual layak memberikan banyak kinerja bagi kebanyakan orang), beberapa mungkin merupakan hasil dari mencoba untuk menjaga Java tetap sederhana, dan beberapa kesalahan desain (walaupun mungkin hanya di belakang) , yaitu UTF-16 adalah penyandian panjang tetap ketika Java dibuat, yang membuat keputusan untuk memilihnya lebih mudah dimengerti).

Perlu dicatat bahwa banyak dari pengorbanan ini sangat berbeda untuk Java / JVM daripada untuk C # / CIL. NET CIL memiliki struct tipe referensi, alokasi stack / passing, array array struct, dan generik tipe-instantiated.

Michael Borgwardt
sumber
37
+1 - secara keseluruhan, ini adalah jawaban yang bagus. Namun, saya tidak yakin titik peluru "tidak ada alokasi tumpukan" sepenuhnya akurat. Java JIT sering melakukan analisis pelarian untuk memungkinkan alokasi tumpukan jika memungkinkan - mungkin apa yang harus Anda katakan adalah bahwa bahasa Java tidak memungkinkan pemrogram untuk memutuskan kapan objek dialokasikan-tumpukan versus dialokasikan-tumpukan. Selain itu, jika pengumpul sampah generasi (yang menggunakan semua JVM modern) sedang digunakan, "alokasi tumpukan" berarti hal yang sama sekali berbeda (dengan karakteristik kinerja yang sama sekali berbeda) daripada di lingkungan C ++.
Daniel Pryden
5
Saya akan berpikir ada dua hal lain tetapi saya kebanyakan bekerja dengan hal-hal di tingkat yang lebih tinggi jadi katakan apakah saya salah. Anda tidak dapat benar-benar menulis C ++ tanpa mengembangkan kesadaran yang lebih umum tentang apa yang sebenarnya terjadi dalam memori dan bagaimana kode mesin benar-benar berfungsi sedangkan scripting atau bahasa mesin virtual mengabstraksi semua hal yang jauh dari perhatian Anda. Anda juga memiliki kendali yang jauh lebih baik atas cara kerja berbagai hal, sedangkan dalam VM atau bahasa yang ditafsirkan Anda mengandalkan apa yang mungkin dioptimalkan oleh penulis pustaka inti untuk skenario yang terlalu spesifik.
Erik Reppen
18
+1. Satu hal lagi yang akan saya tambahkan (tetapi saya tidak mau mengirimkan jawaban baru untuk): pengindeksan array di Jawa selalu melibatkan pemeriksaan batas. Dengan C dan C ++, ini tidak terjadi.
riwalk
7
Perlu dicatat bahwa alokasi heap Java secara signifikan lebih cepat daripada versi naif dengan C ++ (karena penyatuan internal dan hal-hal lain), tetapi alokasi memori dalam C ++ dapat secara signifikan lebih baik jika Anda tahu apa yang Anda lakukan.
Brendan Long
10
@ BrendanLong, benar .. tetapi hanya jika memori bersih - setelah aplikasi berjalan untuk sementara waktu, alokasi memori akan lebih lambat karena kebutuhan untuk GC yang memperlambat segalanya secara dramatis karena harus membebaskan memori, menjalankan finalis kemudian padat. Ini merupakan trade off yang menguntungkan benchmark tetapi (IMHO) secara keseluruhan memperlambat aplikasi.
gbjbaanb
67

Apakah itu hanya karena C ++ dikompilasi ke kode assembly / mesin sedangkan Java / C # masih memiliki overhead pemrosesan kompilasi JIT saat runtime?

Sebagian, tetapi secara umum, dengan asumsi kompiler JIT state-of-the-art yang benar-benar fantastis, kode C ++ yang tepat masih cenderung berkinerja lebih baik daripada kode Java karena DUA alasan utama:

1) C ++ templat menyediakan fasilitas yang lebih baik untuk menulis kode yang generik DAN efisien . Template menyediakan bagi para programmer C ++ abstraksi yang sangat berguna yang memiliki overhead NOL runtime. (Template pada dasarnya mengkompilasi waktu bebek-mengetik.) Sebaliknya, yang terbaik yang Anda dapatkan dengan Java generics pada dasarnya adalah fungsi virtual. Fungsi virtual selalu memiliki overhead runtime, dan umumnya tidak dapat digariskan.

Secara umum, sebagian besar bahasa, termasuk Java, C # dan bahkan C, membuat Anda memilih antara efisiensi dan generalisasi / abstraksi. Templat C ++ memberi Anda keduanya (dengan biaya waktu kompilasi yang lebih lama).

2) Fakta bahwa standar C ++ tidak banyak bicara tentang tata letak biner dari program C ++ yang dikompilasi memberikan kompiler C ++ lebih banyak kelonggaran daripada kompiler Java, memungkinkan untuk optimasi yang lebih baik (dengan biaya terkadang lebih sulit dalam debugging kadang-kadang. ) Faktanya, sifat spesifik dari spesifikasi bahasa Jawa memberlakukan penalti kinerja di area tertentu. Misalnya, Anda tidak bisa memiliki array Objek yang berdekatan di Jawa. Anda hanya dapat memiliki array yang berdekatan dengan pointer Object(Referensi), yang berarti bahwa iterasi pada array di Jawa selalu menimbulkan biaya tipuan. Namun semantik nilai C ++, memungkinkan array yang berdekatan. Perbedaan lain adalah kenyataan bahwa C ++ memungkinkan objek untuk dialokasikan pada stack, sedangkan Java tidak, yang berarti bahwa, dalam praktiknya, karena sebagian besar program C ++ cenderung mengalokasikan objek pada stack, biaya alokasi seringkali mendekati nol.

Satu area di mana C ++ mungkin tertinggal di belakang Java adalah situasi di mana banyak objek kecil perlu dialokasikan di heap. Dalam hal ini, sistem pengumpulan sampah Jawa mungkin akan menghasilkan kinerja yang lebih baik daripada standar newdan deletedalam C ++ karena Java GC memungkinkan deallokasi massal. Tetapi sekali lagi, seorang programmer C ++ dapat mengkompensasi hal ini dengan menggunakan kumpulan memori atau pengalokasi slab, sedangkan seorang programmer Java tidak memiliki jalan lain ketika dihadapkan dengan pola alokasi memori yang runtime Java tidak dioptimalkan untuk.

Juga, lihat jawaban yang sangat bagus ini untuk informasi lebih lanjut tentang topik ini.

Charles Salvia
sumber
6
Jawaban yang bagus tetapi satu poin minor: "C ++ templates memberi Anda berdua (dengan biaya waktu kompilasi yang lebih lama)." Saya juga akan menambahkan dengan biaya ukuran program yang lebih besar. Mungkin tidak selalu menjadi masalah, tetapi jika berkembang untuk perangkat seluler, itu pasti bisa.
Leo
9
@luiscubal: tidak, dalam hal ini, generik C # sangat mirip-Java (dalam hal jalur kode "generik" yang sama diambil, apa pun jenisnya yang dilewati.) Trik untuk templat C ++ adalah bahwa kode tersebut dipakai sekali untuk setiap jenis itu diterapkan. Jadi std::vector<int>adalah array dinamis yang dirancang hanya untuk ints, dan kompiler dapat mengoptimalkannya. AC # List<int>masih tetap a List.
Jalf
12
@jalf C # List<int>menggunakan int[], bukan Object[]seperti Java. Lihat stackoverflow.com/questions/116988/...
luiscubal
5
@luiscubal: terminologi Anda tidak jelas. JIT tidak bertindak pada apa yang saya anggap sebagai "waktu kompilasi". Anda benar, tentu saja, mengingat kompiler JIT yang cukup pintar dan agresif, tidak ada batasan apa yang bisa dilakukan. Tetapi C ++ membutuhkan perilaku ini. Lebih lanjut, template C ++ memungkinkan programmer untuk menentukan spesialisasi eksplisit, memungkinkan optimasi eksplisit tambahan jika berlaku. C # tidak memiliki padanan untuk itu. Sebagai contoh, di C ++, saya dapat mendefinisikan di vector<N>mana, untuk kasus spesifik vector<4>, implementasi SIMD kode tangan saya harus digunakan
jalf
5
@ Leo: Code bloat through templates adalah masalah 15 tahun yang lalu. Dengan templatization berat dan inlining, ditambah kompiler kemampuan yang diambil sejak (seperti melipat contoh identik), banyak kode menjadi lebih kecil melalui templat saat ini.
sbi
46

Apa yang jawaban lain (6 sejauh ini) tampaknya lupa untuk disebutkan, tetapi apa yang saya anggap sangat penting untuk menjawab ini, adalah salah satu filosofi desain dasar C ++, yang dirumuskan dan digunakan oleh Stroustrup sejak hari pertama:

Anda tidak membayar apa yang tidak Anda gunakan.

Ada beberapa prinsip desain mendasar yang penting lainnya yang sangat membentuk C ++ (seperti Anda tidak harus dipaksa ke dalam paradigma tertentu), tetapi Anda tidak membayar untuk apa yang tidak Anda gunakan ada di sana di antara yang paling penting.


Dalam bukunya The Design and Evolution of C ++ (biasanya disebut sebagai [D&E]), Stroustrup menjelaskan kebutuhan apa yang dia miliki yang membuatnya muncul dengan C ++. Dalam kata-kata saya sendiri: Untuk tesis PhD-nya (ada hubungannya dengan simulasi jaringan, IIRC), ia menerapkan sistem dalam SIMULA, yang sangat disukainya, karena bahasa itu sangat baik dalam memungkinkan dia untuk mengekspresikan pikirannya langsung dalam kode. Namun, program yang dihasilkan berjalan terlalu lambat, dan untuk mendapatkan gelar, ia menulis ulang hal itu di BCPL, pendahulu C. Menulis kode dalam BCPL yang ia gambarkan sebagai sesuatu yang menyakitkan, tetapi program yang dihasilkan cukup cepat untuk dikirim. hasil, yang memungkinkan dia untuk menyelesaikan gelar PhD.

Setelah itu, dia ingin bahasa yang memungkinkan untuk menerjemahkan masalah dunia nyata ke dalam kode secara langsung, tetapi juga memungkinkan kode menjadi sangat efisien.
Dalam mengejar itu, ia menciptakan apa yang nantinya menjadi C ++.


Jadi tujuan yang dikutip di atas bukan hanya salah satu dari beberapa prinsip desain dasar yang mendasar, itu sangat dekat dengan raison d'etre untuk C ++. Dan itu dapat ditemukan hampir di mana saja dalam bahasa ini: Fungsi hanya virtualketika Anda menginginkannya (karena memanggil fungsi virtual disertai sedikit overhead) POD hanya diinisialisasi secara otomatis ketika Anda secara eksplisit meminta ini, pengecualian hanya membuat Anda kehilangan kinerja ketika Anda benar-benar membuangnya (padahal itu tujuan desain eksplisit untuk memungkinkan pengaturan / pembersihan stackframe menjadi sangat murah), tidak ada GC yang berjalan kapan pun rasanya, dll.

C ++ secara eksplisit memilih untuk tidak memberi Anda kenyamanan ("apakah saya harus membuat metode ini virtual di sini?") Sebagai imbalan atas kinerja ("tidak, saya tidak, dan sekarang kompiler dapat inlinedan mengoptimalkan cara keluar dari semuanya! "), dan, tidak mengherankan, ini memang menghasilkan peningkatan kinerja dibandingkan dengan bahasa yang lebih nyaman.

sbi
sumber
4
Anda tidak membayar apa yang tidak Anda gunakan. => dan kemudian mereka menambahkan RTTI :(
Matthieu M.
11
@Matthieu: Walaupun saya memahami perasaan Anda, saya tidak bisa tidak memperhatikan bahwa bahkan itu telah ditambahkan dengan hati-hati mengenai kinerja. RTTI ditentukan sehingga dapat diimplementasikan menggunakan tabel virtual, dan dengan demikian menambahkan sedikit overhead jika Anda tidak menggunakannya. Jika Anda tidak menggunakan polimorfisme, tidak ada biaya sama sekali. Apakah saya melewatkan sesuatu?
sbi
9
@ Matthieu: Tentu saja, ada alasannya. Tetapi apakah alasan ini rasional? Dari apa yang saya lihat, "biaya RTTI", jika tidak digunakan, adalah penunjuk tambahan dalam tabel virtual setiap kelas polimorfik, menunjuk pada beberapa objek RTTI yang dialokasikan secara statis di suatu tempat. Kecuali jika Anda ingin memprogram chip dalam pemanggang saya, bagaimana ini bisa relevan?
sbi
4
@Aaronaught: Saya bingung harus menjawab apa. Apakah Anda benar-benar mengabaikan jawaban saya karena ini menunjukkan filosofi dasar yang membuat Stroustrup dkk menambahkan fitur dengan cara yang memungkinkan kinerja, alih-alih mencantumkan cara dan fitur ini secara individual?
sbi
9
@Aaronaught: Anda mendapatkan simpati saya.
sbi
29

Apakah Anda tahu makalah penelitian Google tentang topik itu?

Dari kesimpulan:

Kami menemukan bahwa dalam hal kinerja, C ++ menang dengan margin besar. Namun, itu juga memerlukan upaya tuning paling luas, banyak di antaranya dilakukan pada tingkat kecanggihan yang tidak akan tersedia untuk programmer rata-rata.

Ini setidaknya sebagian penjelasan, dalam arti "karena dunia nyata C + + kompiler menghasilkan kode lebih cepat daripada kompiler Jawa dengan langkah-langkah empiris".

Doc Brown
sumber
4
Selain perbedaan penggunaan memori dan cache, salah satu yang paling penting adalah jumlah optimasi yang dilakukan. Bandingkan berapa banyak optimasi yang dilakukan GCC / LLVM (dan mungkin Visual C ++ / ICC) dibandingkan dengan kompiler Java HotSpot: lebih banyak, terutama mengenai loop, menghilangkan cabang yang berlebihan dan alokasi register. Kompiler JIT biasanya tidak punya waktu untuk optimasi agresif ini, bahkan berpikir mereka bisa mengimplementasikannya lebih baik menggunakan informasi run-time yang tersedia.
Gratian Lup
2
@ GratianLup: Saya ingin tahu apakah itu (masih) benar dengan KPP.
Deduplicator
2
@GratianLup: Jangan lupa optimasi yang dipandu profil untuk C ++ ...
Deduplicator
23

Ini bukan duplikat dari pertanyaan Anda, tetapi jawaban yang diterima menjawab sebagian besar pertanyaan Anda: Tinjauan modern tentang Jawa

Untuk menyimpulkan:

Pada dasarnya, semantik Java menyatakan bahwa itu adalah bahasa yang lebih lambat daripada C ++.

Jadi, tergantung pada bahasa lain yang Anda bandingkan C ++, Anda mungkin mendapatkan atau tidak jawaban yang sama.

Di C ++ Anda memiliki:

  • Kapasitas untuk melakukan inlining cerdas,
  • pembuatan kode generik yang memiliki lokalitas kuat (templat)
  • sekecil dan seringkas mungkin data
  • peluang untuk menghindari tipuan
  • perilaku memori yang dapat diprediksi
  • optimisasi kompiler hanya dimungkinkan karena penggunaan abstraksi tingkat tinggi (templat)

Ini adalah fitur atau efek samping dari definisi bahasa yang membuatnya secara teori lebih efisien pada memori dan kecepatan daripada bahasa apa pun yang:

  • menggunakan tipuan secara besar-besaran ("semuanya adalah referensi yang dikelola / pointer" bahasa): tipuan berarti bahwa CPU harus melompat dalam memori untuk mendapatkan data yang diperlukan, meningkatkan kegagalan cache CPU, yang berarti memperlambat pemrosesan - C menggunakan juga tipuan a banyak bahkan jika itu dapat memiliki data kecil seperti C ++;
  • menghasilkan objek ukuran besar yang anggota diakses secara tidak langsung: ini adalah konsekuensi dari memiliki referensi secara default, anggota adalah pointer sehingga ketika Anda mendapatkan anggota Anda mungkin tidak mendapatkan data yang dekat dengan inti dari objek induk, lagi-lagi memicu kehilangan cache.
  • gunakan kolektor garbarge: itu hanya membuat prediktabilitas kinerja tidak mungkin (sesuai desain).

C ++ inlining agresif dari kompiler mengurangi atau menghilangkan banyak tipuan. Kapasitas untuk menghasilkan sekumpulan kecil data ringkas membuatnya ramah-cache jika Anda tidak menyebarkan data ini ke seluruh memori alih-alih dikemas bersama-sama (keduanya dimungkinkan, C ++ membiarkan Anda memilih). RAII membuat perilaku memori C ++ dapat diprediksi, menghilangkan banyak masalah jika simulasi real-time atau semi-real-time, yang membutuhkan kecepatan tinggi. Masalah lokalitas, secara umum dapat disimpulkan dengan ini: semakin kecil program / data, semakin cepat eksekusi. C ++ menyediakan beragam cara untuk memastikan data Anda berada di tempat yang Anda inginkan (di pool, array, atau apa pun) dan data itu kompak.

Jelas, ada bahasa lain yang dapat melakukan hal yang sama, tetapi mereka kurang populer karena mereka tidak menyediakan alat abstraksi sebanyak C ++, sehingga mereka kurang berguna dalam banyak kasus.

Klaim
sumber
7

Ini terutama tentang memori (seperti kata Michael Borgwardt) dengan sedikit inefisiensi JIT.

Satu hal yang tidak disebutkan adalah cache - untuk menggunakan cache sepenuhnya, Anda perlu data Anda ditata secara bersamaan (yaitu semuanya bersama-sama). Sekarang dengan sistem GC, memori dialokasikan pada heap GC, yang cepat, tetapi ketika memori digunakan GC akan menendang secara teratur dan menghapus blok yang tidak lagi digunakan dan kemudian kompak sisanya. Sekarang terlepas dari lambatnya memindahkan blok yang digunakan bersama-sama, ini berarti bahwa data yang Anda gunakan mungkin tidak terjebak bersama. Jika Anda memiliki larik 1000 elemen, kecuali jika Anda mengalokasikan semuanya sekaligus (dan kemudian memperbarui isinya daripada menghapus dan membuat yang baru - yang akan dibuat di akhir heap) ini akan menjadi tersebar di seluruh heap, sehingga membutuhkan beberapa memori hit untuk membacanya semuanya ke dalam cache CPU. Aplikasi AC / C ++ kemungkinan besar akan mengalokasikan memori untuk elemen-elemen ini dan kemudian Anda memperbarui blok dengan data. (ok, ada struktur data seperti daftar yang berperilaku lebih seperti alokasi memori GC, tetapi orang tahu ini lebih lambat daripada vektor).

Anda dapat melihat ini beroperasi hanya dengan mengganti objek StringBuilder dengan String ... Stringbuilders bekerja dengan mengalokasikan memori dan mengisinya, dan merupakan trik kinerja yang dikenal untuk sistem java / .NET.

Jangan lupa bahwa paradigma 'hapus lama dan alokasikan salinan baru' sangat banyak digunakan di Java / C #, hanya karena orang diberitahu bahwa alokasi memori sangat cepat karena GC, dan model memori yang tersebar digunakan di mana-mana ( kecuali untuk pembuat string, tentu saja) jadi semua perpustakaan Anda cenderung boros memori dan menggunakan banyak, tidak ada yang mendapat manfaat dari persentuhan. Salahkan hype di sekitar GC untuk ini - mereka bilang memori gratis, lol.

GC itu sendiri jelas merupakan hit perf lain - ketika dijalankan, tidak hanya harus menyapu tumpukan, tetapi juga harus membebaskan semua blok yang tidak digunakan, dan kemudian harus menjalankan finalis apa pun (meskipun ini harus dilakukan secara terpisah dengan kali berikutnya dengan aplikasi dihentikan) (Saya tidak tahu apakah itu masih hit perf, tapi semua dokumen yang saya baca katakan hanya menggunakan finalis jika benar-benar diperlukan) dan kemudian harus memindahkan blok-blok ke posisi sehingga tumpukan adalah dipadatkan, dan perbarui referensi ke lokasi baru blok. Seperti yang Anda lihat, ini banyak pekerjaan!

Perf hit untuk memori C ++ turun ke alokasi memori - ketika Anda membutuhkan blok baru, Anda harus berjalan mencari tumpukan ruang kosong berikutnya yang cukup besar, dengan tumpukan sangat terfragmentasi, ini tidak hampir secepat GC 'hanya mengalokasikan blok lain di akhir' tapi saya pikir itu tidak selambat semua pekerjaan yang dilakukan pemadatan GC, dan dapat dikurangi dengan menggunakan beberapa tumpukan blok ukuran tetap (atau dikenal sebagai kumpulan memori).

Masih ada lagi ... seperti memuat rakitan dari GAC yang memerlukan pemeriksaan keamanan, jalur pemeriksaan (nyalakan sxstrace dan lihat saja apa yang sedang terjadi!) Dan rekayasa umum lainnya yang tampaknya jauh lebih populer dengan java / .net dari C / C ++.

gbjbaanb
sumber
2
Banyak hal yang Anda tulis tidak benar untuk pengumpul sampah generasi modern.
Michael Borgwardt
3
@MichaelBorgwardt seperti? Saya mengatakan "GC berjalan secara teratur" dan "itu memadatkan tumpukan". Sisa jawaban saya menyangkut bagaimana struktur data aplikasi menggunakan memori.
gbjbaanb
6

"Apakah itu hanya karena C ++ dikompilasi ke kode assembly / mesin sedangkan Java / C # masih memiliki overhead pemrosesan kompilasi JIT saat runtime?" Pada dasarnya ya!

Catatan singkat, Java memiliki overhead lebih dari sekedar kompilasi JIT. Sebagai contoh, ia melakukan lebih banyak pengecekan untuk Anda (seperti itulah ia melakukan hal-hal seperti ArrayIndexOutOfBoundsExceptionsdan NullPointerExceptions). Pengumpul sampah adalah overhead penting lainnya.

Ada perbandingan yang cukup rinci di sini .

vaughandroid
sumber
2

Ingatlah bahwa yang berikut ini hanya membandingkan perbedaan antara kompilasi asli dan JIT, dan tidak mencakup spesifikasi bahasa atau kerangka kerja tertentu. Mungkin ada alasan yang sah untuk memilih platform tertentu di luar ini.

Ketika kami mengklaim bahwa kode asli lebih cepat, kita berbicara tentang kasus penggunaan khas dari kode yang dikompilasi secara native versus kode yang dikompilasi JIT, di mana penggunaan khas dari aplikasi yang dikompilasi JIT akan dijalankan oleh pengguna, dengan hasil langsung (misalnya, tidak ada tunggu dulu kompiler). Dalam hal ini, saya tidak berpikir siapa pun dapat mengklaim dengan wajah lurus, bahwa kode yang dikompilasi JIT dapat cocok atau mengalahkan kode asli.

Mari kita asumsikan kita memiliki program yang ditulis dalam beberapa bahasa X, dan kita dapat mengkompilasinya dengan kompiler asli, dan lagi dengan kompiler JIT. Setiap alur kerja memiliki tahapan yang sama, yang dapat digeneralisasi sebagai (Kode -> Representasi Menengah -> Kode Mesin -> Eksekusi). Perbedaan besar antara dua adalah tahapan mana yang dilihat oleh pengguna dan mana yang dilihat oleh programmer. Dengan kompilasi asli, programmer melihat semua kecuali tahap eksekusi, tetapi dengan solusi JIT, kompilasi ke kode mesin dilihat oleh pengguna, di samping eksekusi.

Klaim bahwa A lebih cepat dari B mengacu pada waktu yang dibutuhkan untuk menjalankan program, seperti yang terlihat oleh pengguna . Jika kita mengasumsikan bahwa kedua keping kode bekerja secara identik dalam tahap Eksekusi, kita harus mengasumsikan bahwa alur kerja JIT lebih lambat untuk pengguna, karena ia juga harus melihat waktu T dari kompilasi ke kode mesin, di mana T> 0. Jadi , untuk setiap kemungkinan alur kerja JIT untuk melakukan hal yang sama seperti alur kerja asli, kepada pengguna, kita harus mengurangi waktu Eksekusi kode, sehingga Eksekusi + Kompilasi ke kode mesin, lebih rendah daripada hanya tahap Eksekusi dari alur kerja asli. Ini berarti kita harus mengoptimalkan kode lebih baik dalam kompilasi JIT daripada dalam kompilasi asli.

Namun, ini agak tidak layak, karena untuk melakukan optimasi yang diperlukan untuk mempercepat Eksekusi, kita harus menghabiskan lebih banyak waktu dalam mengkompilasi ke tahap kode mesin, dan dengan demikian, setiap kali kita menyimpan karena kode yang dioptimalkan benar-benar hilang, karena kami menambahkannya ke kompilasi. Dengan kata lain, "kelambatan" dari solusi berbasis JIT bukan hanya karena waktu tambahan untuk kompilasi JIT, tetapi kode yang dihasilkan oleh kompilasi tersebut bekerja lebih lambat daripada solusi asli.

Saya akan menggunakan contoh: Daftar alokasi. Karena akses memori beberapa ribu kali lebih lambat daripada akses register, idealnya kami ingin menggunakan register sedapat mungkin dan memiliki akses memori sesedikit mungkin, tetapi kami memiliki jumlah register yang terbatas, dan kami harus menumpahkan keadaan ke dalam memori ketika kami membutuhkan sebuah register. Jika kita menggunakan algoritma alokasi register yang membutuhkan 200ms untuk menghitung, dan sebagai hasilnya kita menghemat waktu eksekusi 2ms - kita tidak memanfaatkan waktu terbaik untuk kompiler JIT. Solusi seperti algoritma Chaitin, yang dapat menghasilkan kode yang sangat optimal tidak cocok.

Peran kompiler JIT adalah untuk mencapai keseimbangan terbaik antara waktu kompilasi dan kualitas kode yang dihasilkan, namun, dengan bias besar pada waktu kompilasi yang cepat, karena Anda tidak ingin membiarkan pengguna menunggu. Kinerja kode yang dieksekusi lebih lambat dalam kasus JIT, karena kompiler asli tidak terikat (banyak) berdasarkan waktu dalam mengoptimalkan kode, jadi bebas menggunakan algoritma terbaik. Kemungkinan keseluruhan kompilasi + eksekusi untuk kompiler JIT hanya dapat mengalahkan waktu eksekusi untuk kode yang dikompilasi secara asli adalah 0

Tetapi VM kami tidak hanya terbatas pada kompilasi JIT. Mereka menggunakan teknik kompilasi sebelumnya, caching, hot swapping, dan optimisasi adaptif. Jadi mari kita modifikasi klaim kami bahwa kinerja adalah apa yang dilihat pengguna, dan batasi sampai waktu yang diperlukan untuk pelaksanaan program (anggap kami telah mengkompilasi AOT). Kita dapat secara efektif membuat kode eksekusi setara dengan kompiler asli (atau mungkin lebih baik?). Klaim besar untuk VM adalah bahwa mereka mungkin dapat menghasilkan kode kualitas yang lebih baik daripada kompiler asli, karena ia memiliki akses ke lebih banyak informasi - bahwa dari proses yang berjalan, seperti seberapa sering fungsi tertentu dapat dieksekusi. VM kemudian dapat menerapkan optimasi adaptif ke kode yang paling penting melalui hot swapping.

Ada masalah dengan argumen ini - ia mengasumsikan bahwa optimasi yang dipandu profil dan sejenisnya adalah sesuatu yang unik untuk VM, yang tidak benar. Kita juga dapat menerapkannya pada kompilasi asli - dengan mengkompilasi aplikasi kita dengan profiling diaktifkan, merekam informasi, dan kemudian mengkompilasi ulang aplikasi dengan profil itu. Mungkin juga patut menunjukkan bahwa hot swapping kode bukanlah sesuatu yang hanya dapat dilakukan oleh kompiler JIT, kita dapat melakukannya untuk kode asli - meskipun solusi berbasis JIT untuk melakukan ini lebih mudah tersedia, dan jauh lebih mudah bagi pengembang. Jadi pertanyaan besarnya adalah: Dapatkah seorang VM menawarkan kami beberapa informasi yang tidak dapat dikompilasi oleh native, yang dapat meningkatkan kinerja kode kami?

Saya tidak bisa melihatnya sendiri. Kami dapat menerapkan sebagian besar teknik VM khas untuk kode asli juga - meskipun prosesnya lebih terlibat. Demikian pula, kita dapat menerapkan optimasi apa pun dari kompiler asli kembali ke VM yang menggunakan kompilasi AOT atau optimisasi adaptif. Kenyataannya adalah bahwa perbedaan antara kode asli yang dijalankan, dan yang dijalankan dalam VM tidak sebesar yang kami yakini. Mereka pada akhirnya mengarah pada hasil yang sama, tetapi mereka mengambil pendekatan yang berbeda untuk sampai ke sana. VM menggunakan pendekatan berulang untuk menghasilkan kode yang dioptimalkan, di mana kompiler asli mengharapkannya dari awal (dan dapat ditingkatkan dengan pendekatan berulang).

Seorang programmer C ++ mungkin berpendapat bahwa ia membutuhkan optimisasi sejak awal, dan seharusnya tidak menunggu seorang VM untuk mengetahui bagaimana melakukannya, jika memang ada. Ini mungkin titik yang valid dengan teknologi kami saat ini, karena tingkat optimisasi saat ini di VM kami lebih rendah daripada yang ditawarkan kompiler asli - tetapi itu tidak selalu menjadi masalah jika solusi AOT di VM kami meningkat, dll.

Markus H
sumber
0

Artikel ini adalah ringkasan dari serangkaian posting blog yang mencoba membandingkan kecepatan c ++ vs c # dan masalah yang harus Anda atasi dalam kedua bahasa untuk mendapatkan kode kinerja tinggi. Rangkumannya adalah 'masalah perpustakaan Anda lebih dari apa pun, tetapi jika Anda menggunakan c ++ Anda dapat mengatasinya.' atau 'bahasa modern memiliki perpustakaan yang lebih baik dan karenanya mendapatkan hasil yang lebih cepat dengan upaya yang lebih rendah' ​​tergantung pada kecenderungan filosofis Anda.

Jeff Gates
sumber
0

Saya pikir pertanyaan sebenarnya di sini bukanlah "mana yang lebih cepat?" tetapi "yang memiliki potensi terbaik untuk kinerja yang lebih tinggi?". Dilihat dari persyaratan tersebut, C ++ jelas menang - itu dikompilasi ke kode asli, tidak ada JITting, ini adalah abstraksi level yang lebih rendah, dll.

Itu jauh dari cerita lengkapnya.

Karena C ++ dikompilasi, setiap optimisasi kompiler harus dilakukan pada waktu kompilasi, dan optimisasi kompiler yang sesuai untuk satu mesin mungkin sepenuhnya salah untuk yang lain. Ini juga merupakan kasus bahwa setiap optimisasi kompiler global dapat dan akan lebih menyukai algoritma atau pola kode tertentu daripada yang lain.

Di sisi lain, program JITted akan mengoptimalkan pada waktu JIT, sehingga dapat menarik beberapa trik bahwa program yang dikompilasi tidak dapat dan dapat membuat optimasi yang sangat spesifik untuk mesin yang benar-benar berjalan dan kode yang sebenarnya dijalankan. Setelah Anda melewati overhead awal JIT, dalam beberapa kasus berpotensi untuk lebih cepat.

Dalam kedua kasus implementasi yang masuk akal dari algoritma dan contoh lain dari programmer yang tidak bodoh kemungkinan akan menjadi faktor yang jauh lebih signifikan, namun - misalnya, sangat mungkin untuk menulis kode string yang benar-benar mati otak dalam C ++ yang akan ditempa oleh bahkan bahasa scripting yang ditafsirkan.

Maximus Minimus
sumber
3
"optimisasi kompiler yang sesuai untuk satu mesin mungkin benar-benar salah untuk yang lain" Yah, itu tidak benar-benar menyalahkan bahasa. Benar-benar kode kritis-kinerja dapat dikompilasi secara terpisah untuk setiap mesin yang akan dijalankannya, yang merupakan no-brainer jika Anda mengkompilasi secara lokal dari sumber ( -march=native). - "ini level abstraksi yang lebih rendah" tidak sepenuhnya benar. C ++ menggunakan abstraksi tingkat tinggi seperti halnya Java (atau, pada kenyataannya, abstraksi yang lebih tinggi: pemrograman fungsional? Templat metaprogramming?), Ia hanya mengimplementasikan abstraksi yang kurang "bersih" daripada Java.
leftaroundabout
"Benar-benar kode kinerja-kritis dapat dikompilasi secara terpisah untuk setiap mesin yang akan dijalankan, yang merupakan no-brainer jika Anda mengkompilasi secara lokal dari sumber" - ini gagal karena asumsi yang mendasari bahwa pengguna akhir juga seorang programmer.
Maximus Minimus
Tidak harus pengguna akhir, hanya orang yang bertanggung jawab untuk menginstal program. Pada desktop dan perangkat seluler, yang biasanya adalah pengguna akhir, tetapi ini bukan satu-satunya aplikasi yang ada, tentu saja bukan yang paling kritis terhadap kinerja. Dan Anda tidak benar-benar perlu menjadi seorang programmer untuk membangun sebuah program dari sumber, jika ia telah membangun skrip yang ditulis dengan benar seperti semua proyek perangkat lunak bebas / terbuka yang baik.
leftaroundabout
1
Sementara dalam teori ya, JIT dapat menarik lebih banyak trik daripada kompiler statis, dalam prakteknya (untuk. NET setidaknya, saya tidak tahu java juga), itu sebenarnya tidak melakukan semua ini. Saya telah melakukan banyak pembongkaran kode. NET JIT baru-baru ini, dan ada segala macam optimasi seperti mengangkat kode keluar dari loop, menghilangkan kode mati, dll, bahwa. NET JIT tidak bisa dilakukan. Saya berharap demikian, tapi hei, tim windows di dalam microsoft telah mencoba untuk membunuh .NET selama bertahun-tahun, jadi saya tidak menahan nafas saya
Orion Edwards
-1

Kompilasi JIT sebenarnya memiliki dampak negatif pada kinerja. Jika Anda mendesain kompiler "sempurna" dan kompiler JIT "sempurna", opsi pertama akan selalu menang dalam kinerja.

Baik Java dan C # ditafsirkan ke dalam bahasa perantara, dan kemudian dikompilasi ke kode asli saat runtime, yang mengurangi kinerja.

Tapi sekarang perbedaannya tidak begitu jelas untuk C #: Microsoft CLR menghasilkan kode asli yang berbeda untuk CPU yang berbeda, sehingga membuat kode lebih efisien untuk mesin yang sedang berjalan, yang tidak selalu dilakukan oleh kompiler C ++.

PS C # ditulis dengan sangat efisien dan tidak memiliki banyak lapisan abstraksi. Ini tidak benar untuk Java, yang tidak efisien. Jadi, dalam hal ini, dengan CLR greate-nya, program C # sering menunjukkan kinerja yang lebih baik daripada program C ++. Untuk selengkapnya tentang .Net dan CLR, lihat "CLR via C #" karya Jeffrey Richter .

superM
sumber
8
Jika JIT benar-benar berdampak negatif pada kinerja, tentunya itu tidak akan digunakan?
Zavior
2
@Zavior - Saya tidak bisa memikirkan jawaban yang baik untuk pertanyaan Anda, tetapi saya tidak melihat bagaimana JIT tidak dapat menambahkan overhead kinerja ekstra - JIT adalah proses ekstra yang harus diselesaikan pada saat run time yang membutuhkan sumber daya yang dibutuhkan ' tidak dihabiskan untuk pelaksanaan program itu sendiri, sedangkan bahasa yang dikompilasi penuh adalah 'siap untuk pergi'.
Anonim
3
JIT memiliki efek positif pada kinerja, bukan negatif, jika Anda memasukkannya ke dalam konteks - Ini mengkompilasi kode byte ke dalam kode mesin sebelum menjalankannya. Hasilnya juga bisa di-cache, memungkinkannya berjalan lebih cepat dari byte-code yang diartikan.
Casey Kuball
3
JIT (atau lebih tepatnya, pendekatan bytecode) tidak digunakan untuk kinerja, tetapi untuk kenyamanan. Alih-alih membuat pra-binari untuk setiap platform (atau subset umum, yang sub-optimal untuk masing-masing platform), Anda mengkompilasi hanya setengah dan membiarkan kompiler JIT melakukan sisanya. 'Menulis sekali, sebarkan di mana saja' karena itu dilakukan dengan cara ini. Kemudahan tersebut dapat diperoleh hanya dengan penerjemah bytecode, tetapi JIT membuatnya lebih cepat dari penerjemah mentah (meskipun tidak selalu cukup cepat untuk mengalahkan solusi yang sudah dikompilasi sebelumnya; Kompilasi JIT memang membutuhkan waktu, dan hasilnya tidak selalu dapat menghasilkan untuk itu).
tdammers
4
@Damdamers, sebenarnya ada komponen kinerja juga. Lihat java.sun.com/products/hotspot/whitepaper.html . Optimalisasi dapat mencakup hal-hal seperti penyesuaian dinamis untuk meningkatkan prediksi cabang dan hit cache, inlining dinamis, de-virtualisasi, penonaktifan pemeriksaan batas, dan lilitan loop. Klaimnya adalah bahwa dalam banyak kasus ini lebih dari cukup untuk membayar biaya JIT.
Charles E. Grant