Adakah yang bisa menjelaskan secara rinci, bagaimana sebenarnya tabel virtual bekerja dan pointer apa yang dikaitkan ketika fungsi virtual dipanggil.
Jika mereka sebenarnya lebih lambat, dapatkah Anda menunjukkan waktu yang diperlukan untuk menjalankan fungsi virtual lebih dari metode kelas normal? Sangat mudah untuk kehilangan jejak bagaimana / apa yang terjadi tanpa melihat beberapa kode.
Jawaban:
Metode virtual biasanya diimplementasikan melalui apa yang disebut tabel metode virtual (singkatnya vtable), di mana pointer fungsi disimpan. Ini menambahkan tipuan ke panggilan aktual (harus mengambil alamat fungsi untuk memanggil dari vtable, lalu memanggilnya - bukan hanya memanggilnya di depan). Tentu saja, ini membutuhkan waktu dan beberapa kode lagi.
Namun, itu belum tentu menjadi penyebab utama kelambatan. Masalah sebenarnya adalah bahwa kompiler (umumnya / biasanya) tidak dapat mengetahui fungsi mana yang akan dipanggil. Jadi itu tidak dapat menyejajarkan atau melakukan optimasi lainnya. Ini saja mungkin menambah selusin instruksi yang tidak berguna (menyiapkan register, memanggil, kemudian mengembalikan status setelahnya), dan mungkin menghambat optimasi lain yang tampaknya tidak terkait. Selain itu, jika Anda bercabang gila dengan memanggil banyak implementasi berbeda, Anda mengalami hit yang sama dengan yang Anda menderita percabangan seperti gila dengan cara lain: Cache dan prediktor cabang tidak akan membantu Anda, cabang-cabangnya akan memakan waktu lebih lama dari yang dapat diprediksi dengan sempurna cabang.
Besar tetapi : Hits kinerja ini biasanya terlalu kecil untuk diperhitungkan. Mereka patut dipertimbangkan jika Anda ingin membuat kode berkinerja tinggi dan mempertimbangkan untuk menambahkan fungsi virtual yang akan dipanggil pada frekuensi yang mengkhawatirkan. Namun, juga perlu diingat bahwa mengganti panggilan fungsi virtual dengan cara lain bercabang (
if .. else
,switch
, fungsi pointer, dll) tidak akan memecahkan masalah mendasar - mungkin sangat baik menjadi lebih lambat. Masalahnya (jika ada sama sekali) bukan fungsi virtual tetapi (tidak perlu) tipuan.Sunting: Perbedaan dalam instruksi panggilan dijelaskan dalam jawaban lain. Pada dasarnya, kode untuk panggilan statis ("normal") adalah:
Panggilan virtual melakukan hal yang persis sama, kecuali bahwa alamat fungsi tidak diketahui pada waktu kompilasi. Sebagai gantinya, beberapa instruksi ...
Adapun cabang: Cabang adalah apa pun yang melompat ke instruksi lain, bukan hanya membiarkan instruksi berikutnya dijalankan. Ini termasuk
if
,,switch
bagian dari berbagai loop, pemanggilan fungsi, dll. Dan kadang-kadang kompiler mengimplementasikan hal-hal yang tampaknya tidak bercabang dengan cara yang benar-benar membutuhkan cabang di bawah tenda. Lihat Mengapa memproses array yang diurutkan lebih cepat daripada array yang tidak disortir? untuk alasan ini mungkin lambat, apa yang CPU lakukan untuk mengatasi perlambatan ini, dan bagaimana ini bukan obat untuk semua.sumber
virtual
.Berikut ini beberapa kode dibongkar yang sebenarnya dari panggilan fungsi virtual dan panggilan non-virtual, masing-masing:
Anda dapat melihat bahwa panggilan virtual memerlukan tiga instruksi tambahan untuk mencari alamat yang benar, sedangkan alamat panggilan non-virtual dapat dikompilasi.
Namun, perhatikan bahwa sebagian besar waktu bahwa waktu pencarian tambahan dapat dianggap diabaikan. Dalam situasi di mana waktu pencarian akan signifikan, seperti dalam satu lingkaran, nilainya biasanya dapat di-cache dengan melakukan tiga instruksi pertama sebelum loop.
Situasi lain di mana waktu pencarian menjadi signifikan adalah jika Anda memiliki koleksi objek dan Anda mengulang melalui memanggil fungsi virtual pada masing-masing. Namun, dalam hal ini, Anda akan memerlukan beberapa cara untuk memilih fungsi mana yang akan dipanggil, dan pencarian tabel virtual sama baiknya dengan cara apa pun. Bahkan, karena kode pencarian vtable sangat banyak digunakan, sangat dioptimalkan, jadi mencoba untuk mengatasinya secara manual memiliki peluang bagus untuk menghasilkan kinerja yang lebih buruk .
sumber
-0x8(%rbp)
. oh my ... itu sintaks AT&T.Lebih lambat dari apa ?
Fungsi virtual memecahkan masalah yang tidak dapat diselesaikan dengan panggilan fungsi langsung. Secara umum, Anda hanya dapat membandingkan dua program yang menghitung hal yang sama. "Pelacak ray ini lebih cepat daripada kompiler itu" tidak masuk akal, dan prinsip ini menggeneralisasi bahkan untuk hal-hal kecil seperti fungsi individu atau konstruksi bahasa pemrograman.
Jika Anda tidak menggunakan fungsi virtual untuk secara dinamis beralih ke sepotong kode berdasarkan datum, seperti jenis objek, maka Anda harus menggunakan sesuatu yang lain, seperti
switch
pernyataan untuk mencapai hal yang sama. Sesuatu yang lain memiliki overhead sendiri, ditambah implikasi pada organisasi program yang memengaruhi kelestariannya dan kinerja global.Perhatikan bahwa dalam C ++, panggilan ke fungsi virtual tidak selalu dinamis. Ketika panggilan dilakukan pada objek yang tipe pastinya diketahui (karena objek tersebut bukan pointer atau referensi, atau karena tipenya dapat disimpulkan secara statis), maka panggilan tersebut hanyalah panggilan fungsi anggota biasa. Itu tidak hanya berarti bahwa tidak ada overhead pengiriman, tetapi juga bahwa panggilan ini dapat digariskan dengan cara yang sama seperti panggilan biasa.
Dengan kata lain, kompiler C ++ Anda dapat berfungsi ketika fungsi virtual tidak memerlukan pengiriman virtual, jadi biasanya tidak ada alasan untuk khawatir tentang kinerjanya relatif terhadap fungsi non-virtual.
Baru: Juga, kita tidak boleh melupakan perpustakaan bersama. Jika Anda menggunakan kelas yang ada di pustaka bersama, panggilan ke fungsi anggota biasa tidak hanya berupa urutan instruksi yang bagus
callq 0x4007aa
. Itu harus melalui beberapa simpai, seperti tidak langsung melalui "tabel tautan program" atau struktur semacam itu. Oleh karena itu, tipuan perpustakaan bersama agak bisa (jika tidak sepenuhnya) tingkat perbedaan biaya antara panggilan virtual (benar-benar tidak langsung) dan panggilan langsung. Jadi alasan tentang pengorbanan fungsi virtual harus mempertimbangkan bagaimana program dibangun: apakah kelas objek target secara monolitik terhubung ke program yang melakukan panggilan.sumber
karena panggilan virtual setara dengan
di mana dengan fungsi non-virtual kompiler dapat secara konstan melipat baris pertama, ini adalah dereferensi tambahan dan panggilan dinamis diubah menjadi hanya panggilan statis
ini juga memungkinkan fungsi inline (dengan semua konsekuensi optimasi karena)
sumber