Fungsi dan kinerja virtual - C ++


125

Dalam desain kelas saya, saya menggunakan kelas abstrak dan fungsi virtual secara luas. Saya merasa bahwa fungsi virtual mempengaruhi kinerja. Apakah ini benar? Tapi saya pikir perbedaan kinerja ini tidak terlihat dan sepertinya saya melakukan optimasi prematur. Baik?


Sebagai jawaban saya, saya sarankan menutup ini sebagai duplikat dari stackoverflow.com/questions/113830
Suma


2
Jika Anda melakukan komputasi kinerja tinggi dan angka-angka, jangan gunakan virtualitas apa pun di inti perhitungan: itu pasti membunuh semua kinerja dan mencegah optimisasi pada waktu kompilasi. Untuk inisialisasi atau finalisasi program itu tidak penting. Saat bekerja dengan antarmuka, Anda dapat menggunakan virtualitas sesuai keinginan.
Vincent

Jawaban:


90

Aturan praktis yang baik adalah:

Ini bukan masalah kinerja sampai Anda bisa membuktikannya.

Penggunaan fungsi virtual akan memiliki efek yang sangat kecil pada kinerja, tetapi itu tidak akan mempengaruhi kinerja keseluruhan aplikasi Anda. Tempat yang lebih baik untuk mencari peningkatan kinerja adalah dalam algoritme dan I / O.

Artikel yang luar biasa yang membahas tentang fungsi virtual (dan lebih banyak lagi) adalah Pointer Fungsi Anggota dan Delegasi C ++ yang Tercepat Mungkin .


Bagaimana dengan fungsi virtual murni? Apakah mereka memengaruhi kinerja dengan cara apa pun? Hanya ingin tahu karena tampaknya mereka ada hanya untuk menegakkan implementasi.
thomthom

2
@thomthom: Benar, tidak ada perbedaan kinerja antara fungsi virtual murni dan virtual biasa.
Greg Hewgill

168

Pertanyaan Anda membuat saya penasaran, jadi saya melanjutkan dan menjalankan beberapa pengaturan waktu pada CPU PowerPC berurutan 3GHz yang bekerja dengan kami. Tes yang saya jalankan adalah membuat kelas vektor 4d sederhana dengan fungsi get / set

class TestVec 
{
    float x,y,z,w; 
public:
    float GetX() { return x; }
    float SetX(float to) { return x=to; }  // and so on for the other three 
}

Lalu saya mengatur tiga array masing-masing berisi 1024 vektor ini (cukup kecil untuk muat di L1) dan menjalankan loop yang menambahkannya satu sama lain (Ax = Bx + Cx) 1000 kali. Aku berlari ini dengan fungsi didefinisikan sebagai inline, virtual, dan panggilan fungsi biasa. Inilah hasilnya:

  • sebaris: 8ms (0,65ns per panggilan)
  • langsung: 68ms (5,53ns per panggilan)
  • virtual: 160ms (13ns per panggilan)

Jadi, dalam hal ini (di mana semuanya sesuai dalam cache) panggilan fungsi virtual sekitar 20x lebih lambat dari panggilan inline. Tapi apa maksudnya ini? Setiap perjalanan melalui loop disebabkan tepat3 * 4 * 1024 = 12,288 panggilan fungsi dengan (1024 vektor kali empat komponen kali tiga panggilan per penambahan), jadi kali ini mewakili 1000 * 12,288 = 12,288,000panggilan fungsi. Virtual loop memakan waktu 92ms lebih lama dari loop langsung, sehingga overhead tambahan per panggilan adalah 7 nanodetik per fungsi.

Dari sini saya menyimpulkan: ya , fungsi virtual jauh lebih lambat daripada fungsi langsung, dan tidak , kecuali Anda berencana memanggilnya sepuluh juta kali per detik, itu tidak masalah.

Lihat juga: perbandingan rakitan yang dihasilkan.


Tetapi jika jika mereka dipanggil beberapa kali, mereka seringkali bisa lebih murah daripada ketika hanya dipanggil satu kali. Lihat blog saya yang tidak relevan: phresnel.org/blog , tulisan berjudul "Fungsi virtual dianggap tidak berbahaya", tetapi tentu saja itu tergantung pada kompleksitas codepath Anda
Sebastian Mach

22
Pengujian saya mengukur sekumpulan kecil fungsi virtual yang disebut berulang kali. Posting blog Anda mengasumsikan bahwa biaya waktu kode dapat diukur dengan menghitung operasi, tetapi itu tidak selalu benar; biaya utama vfunc pada prosesor modern adalah gelembung pipa yang disebabkan oleh kesalahan prediksi cabang.
Crashworks

10
ini akan menjadi tolok ukur yang bagus untuk gcc LTO (Link Time Optimization); coba kompilasi ini lagi dengan lto diaktifkan: gcc.gnu.org/wiki/LinkTimeOptimization dan lihat apa yang terjadi dengan faktor 20x
lurscher

1
Jika suatu kelas memiliki satu fungsi virtual dan satu inline, apakah kinerja metode non-virtual juga akan terpengaruh? Cukup dengan sifat kelas yang virtual?
thomthom

4
@thomthom Tidak, virtual / non-virtual adalah atribut per fungsi. Fungsi hanya perlu didefinisikan melalui vtable jika ditandai sebagai virtual atau jika mengesampingkan kelas dasar yang menjadikannya sebagai virtual. Anda akan sering melihat kelas yang memiliki sekelompok fungsi virtual untuk antarmuka publik, dan kemudian banyak pengakses inline dan sebagainya. (Secara teknis, ini khusus implementasi dan kompiler dapat menggunakan ponter virtual bahkan untuk fungsi yang ditandai 'inline', tetapi orang yang menulis kompiler seperti itu akan menjadi gila.)
Crashworks

42

Ketika Objective-C (di mana semua metode virtual) adalah bahasa utama untuk iPhone dan Java freakin adalah bahasa utama untuk Android, saya pikir cukup aman untuk menggunakan fungsi virtual C ++ pada menara dual-core 3 GHz kami.


4
Saya tidak yakin iPhone adalah contoh kode performan yang bagus: youtube.com/watch?v=Pdk2cJpSXLg
Crashworks

13
@ Crashworks: iPhone bukan contoh kode sama sekali. Ini adalah contoh perangkat keras - khususnya perangkat keras yang lambat , yang merupakan poin yang saya buat di sini. Jika bahasa yang konon "lambat" ini cukup baik untuk perangkat keras yang kekurangan tenaga, fungsi virtual tidak akan menjadi masalah besar.
Chuck

52
IPhone berjalan pada prosesor ARM. Prosesor ARM yang digunakan untuk iOS dirancang untuk MHz rendah dan penggunaan daya rendah. Tidak ada silikon untuk prediksi cabang pada CPU dan karenanya tidak ada overhead kinerja dari prediksi cabang yang terlewatkan dari panggilan fungsi virtual. Perangkat keras MHz untuk iOS juga cukup rendah sehingga cache miss tidak menghentikan prosesor selama 300 jam siklus saat mengambil data dari RAM. Kehilangan cache kurang penting pada MHz yang lebih rendah. Singkatnya, tidak ada overhead dari menggunakan fungsi virtual pada perangkat iOS, tetapi ini adalah masalah perangkat keras dan tidak berlaku untuk CPU desktop.
HaltingState

4
Sebagai programmer Java yang lama menjadi C ++, saya ingin menambahkan bahwa kompiler JIT Java dan pengoptimal run-time memiliki kemampuan untuk mengkompilasi, memprediksi dan bahkan menjalankan beberapa fungsi pada saat run-time setelah jumlah loop yang telah ditentukan. Namun saya tidak yakin apakah C ++ memiliki fitur seperti itu pada waktu kompilasi dan tautan karena tidak memiliki pola panggilan runtime. Jadi dalam C ++ kita mungkin perlu sedikit lebih hati-hati.
Alex Suo

@AlexSuo Saya tidak yakin dengan maksud Anda? Sedang dikompilasi, C ++ tentu saja tidak dapat mengoptimalkan berdasarkan apa yang mungkin terjadi pada saat runtime, jadi prediksi dll harus dilakukan oleh CPU itu sendiri ... tetapi kompiler C ++ yang baik (jika diperintahkan) berusaha keras untuk mengoptimalkan fungsi dan loop jauh sebelum runtime.
underscore_d

34

Dalam aplikasi yang sangat kritis kinerja (seperti video game) panggilan fungsi virtual bisa terlalu lambat. Dengan perangkat keras modern, kekhawatiran kinerja terbesar adalah cache miss. Jika data tidak ada dalam cache, mungkin ratusan siklus sebelum tersedia.

Panggilan fungsi normal dapat menghasilkan cache instruksi yang hilang ketika CPU mengambil instruksi pertama dari fungsi yang baru dan itu tidak ada dalam cache.

Panggilan fungsi virtual terlebih dahulu perlu memuat pointer vtable dari objek. Ini dapat menyebabkan kehilangan cache data. Kemudian itu memuat pointer fungsi dari vtable yang dapat mengakibatkan kehilangan cache data lain. Kemudian ia memanggil fungsi yang dapat mengakibatkan cache instruksi ketinggalan seperti fungsi non-virtual.

Dalam banyak kasus, dua kesalahan cache tambahan tidak menjadi masalah, tetapi dalam loop ketat pada kode kritis kinerja, ini dapat secara dramatis mengurangi kinerja.


6
Benar, tetapi kode apa pun (atau vtable) yang dipanggil berulang kali dari loop ketat akan (tentu saja) jarang mengalami kesalahan cache. Selain itu, pointer vtable biasanya berada di baris cache yang sama dengan data lain dalam objek yang akan diakses oleh metode yang dipanggil, sehingga sering kali kita berbicara tentang hanya satu cache miss tambahan.
Qwertie

5
@ Qwertie, saya pikir itu tidak perlu benar. Tubuh loop (jika lebih besar dari L1 cache) dapat "pensiun" vtable pointer, fungsi pointer dan iterasi selanjutnya harus menunggu akses L2 cache (atau lebih) pada setiap iterasi
Ghita

30

Dari halaman 44 dari manual "Mengoptimalkan Perangkat Lunak dalam C ++" Agner Fog :

Waktu yang diperlukan untuk memanggil fungsi anggota virtual adalah beberapa jam siklus lebih dari yang dibutuhkan untuk memanggil fungsi anggota non-virtual, asalkan pernyataan panggilan fungsi selalu memanggil versi yang sama dari fungsi virtual. Jika versi berubah maka Anda akan mendapatkan penalti misprediksi siklus 10 - 30 jam. Aturan untuk prediksi dan kesalahan prediksi panggilan fungsi virtual sama seperti untuk pernyataan switch ...


Terima kasih untuk referensi ini. Manual optimasi Agner Fog adalah standar utama untuk memanfaatkan perangkat keras secara optimal.
Arto Bendiken

Berdasarkan ingatan saya dan pencarian cepat - stackoverflow.com/questions/17061967/c-switch-and-jump-tables - Saya ragu ini selalu benar untuk switch. Dengan casenilai yang sepenuhnya arbitrer , tentu saja. Tetapi jika semua caseberurutan, kompiler mungkin dapat mengoptimalkan ini menjadi lompatan-tabel (ah, yang mengingatkan saya pada hari-hari Z80 yang baik), yang seharusnya (untuk jangka waktu yang lebih baik) waktu-konstan. Bukannya saya sarankan mencoba mengganti vfuncs dengan switch, yang menggelikan. ;)
underscore_d

7

benar. Itu masalah jalan kembali ketika komputer berjalan pada 100Mhz, karena setiap panggilan metode memerlukan pencarian pada vtable sebelum dipanggil. Tapi hari ini .. pada CPU 3Ghz yang memiliki cache level 1 dengan lebih banyak memori daripada komputer pertama saya? Tidak semuanya. Mengalokasikan memori dari RAM utama akan menghabiskan lebih banyak waktu daripada jika semua fungsi Anda virtual.

Ini seperti masa lalu, di mana orang mengatakan pemrograman terstruktur lambat karena semua kode dipecah menjadi fungsi, setiap fungsi memerlukan alokasi stack dan pemanggilan fungsi!

Satu-satunya waktu saya bahkan berpikir untuk mempertimbangkan dampak kinerja dari fungsi virtual, adalah jika itu sangat banyak digunakan dan dipakai dalam kode templated yang berakhir di seluruh segalanya. Meski begitu, saya tidak akan menghabiskan terlalu banyak upaya untuk itu!

PS memikirkan bahasa 'mudah digunakan' lainnya - semua metode mereka virtual di bawah selimut dan mereka tidak merangkak saat ini.


4
Yah, bahkan hari ini menghindari panggilan fungsi penting untuk aplikasi dengan perfoma tinggi. Perbedaannya adalah, kompiler hari ini secara andal menggarisbawahi fungsi-fungsi kecil sehingga kita tidak mengalami penalti cepat untuk menulis fungsi-fungsi kecil. Adapun fungsi virtual, CPU pintar dapat melakukan prediksi cabang cerdas pada mereka. Fakta bahwa komputer lama lebih lambat, saya pikir, tidak benar-benar masalah - ya, mereka jauh lebih lambat, tapi saat itu kami tahu itu, jadi kami memberi mereka beban kerja yang jauh lebih kecil. Pada tahun 1992 jika kami memutar MP3, kami tahu kami mungkin harus mendedikasikan lebih dari setengah CPU untuk tugas itu.
Qwertie

6
tanggal mp3 dari tahun 1995. di 92 kami hampir tidak memiliki 386, tidak ada cara mereka bisa memainkan mp3, dan 50% dari waktu cpu mengasumsikan OS multi-tugas yang baik, proses idle, dan penjadwal preemptive. Semua ini tidak ada di pasar konsumen saat itu. 100% dari saat power ON, akhir cerita.
v.oddou

7

Ada kriteria kinerja lain selain waktu eksekusi. Vtable juga memakan ruang memori, dan dalam beberapa kasus dapat dihindari: ATL menggunakan waktu kompilasi " simulasi ikatan dinamis " dengan templatuntuk mendapatkan efek "polimorfisme statis", yang agak sulit dijelaskan; Anda pada dasarnya meneruskan kelas turunan sebagai parameter ke templat kelas dasar, jadi pada waktu kompilasi, kelas dasar "tahu" apa kelas turunannya dalam setiap instance. Tidak akan membiarkan Anda menyimpan beberapa kelas turunan yang berbeda dalam kumpulan tipe dasar (itu run-time polymorphism) tetapi dari pengertian statis, jika Anda ingin membuat kelas Y yang sama dengan templat kelas X yang sudah ada sebelumnya yang memiliki kait untuk jenis overriding ini, Anda hanya perlu menimpa metode yang Anda pedulikan, dan kemudian Anda mendapatkan metode dasar kelas X tanpa harus memiliki vtable.

Di kelas dengan jejak memori yang besar, biaya satu pointer vtable tidak banyak, tetapi beberapa kelas ATL di COM sangat kecil, dan itu layak penghematan vtable jika kasus run-time polimorfisme tidak akan pernah terjadi.

Lihat juga pertanyaan SO lainnya ini .

Ngomong-ngomong, inilah postingan yang saya temukan yang berbicara tentang aspek kinerja waktu CPU.



4

Ya, Anda benar dan jika Anda ingin tahu tentang biaya panggilan fungsi virtual Anda mungkin menemukan posting ini menarik.


1
Artikel yang ditautkan tidak mempertimbangkan bagian yang sangat penting dari panggilan virtual, dan itu mungkin salah duga cabang.
Suma

4

Satu-satunya cara saya dapat melihat bahwa fungsi virtual akan menjadi masalah kinerja adalah jika banyak fungsi virtual dipanggil dalam loop ketat, dan jika dan hanya jika mereka menyebabkan kesalahan halaman atau operasi memori "berat" lainnya terjadi.

Meskipun seperti orang lain katakan itu cukup banyak tidak akan menjadi masalah bagi Anda dalam kehidupan nyata. Dan jika menurut Anda, jalankan profiler, lakukan beberapa tes, dan verifikasi apakah ini benar-benar masalah sebelum mencoba "mendesain" kode Anda untuk keuntungan kinerja.


2
memanggil apa pun di loop ketat cenderung menyimpan semua kode dan data panas dalam cache ...
Greg Rogers

2
Ya, tetapi jika loop kanan iterasi melalui daftar objek maka setiap objek berpotensi memanggil fungsi virtual pada alamat yang berbeda melalui pemanggilan fungsi yang sama.
Daemin

3

Ketika metode kelas bukan virtual, kompiler biasanya melakukan in-lining. Sebaliknya, ketika Anda menggunakan pointer ke beberapa kelas dengan fungsi virtual, alamat sebenarnya hanya akan diketahui saat runtime.

Ini diilustrasikan dengan baik oleh tes, perbedaan waktu ~ 700% (!):

#include <time.h>

class Direct
{
public:
    int Perform(int &ia) { return ++ia; }
};

class AbstrBase
{
public:
    virtual int Perform(int &ia)=0;
};

class Derived: public AbstrBase
{
public:
    virtual int Perform(int &ia) { return ++ia; }
};


int main(int argc, char* argv[])
{
    Direct *pdir, dir;
    pdir = &dir;

    int ia=0;
    double start = clock();
    while( pdir->Perform(ia) );
    double end = clock();
    printf( "Direct %.3f, ia=%d\n", (end-start)/CLOCKS_PER_SEC, ia );

    Derived drv;
    AbstrBase *ab = &drv;

    ia=0;
    start = clock();
    while( ab->Perform(ia) );
    end = clock();
    printf( "Virtual: %.3f, ia=%d\n", (end-start)/CLOCKS_PER_SEC, ia );

    return 0;
}

Dampak panggilan fungsi virtual sangat tergantung pada situasi. Jika ada beberapa panggilan dan sejumlah besar pekerjaan di dalam fungsi - itu bisa diabaikan.

Atau, ketika itu adalah panggilan virtual berulang kali digunakan berkali-kali, sambil melakukan beberapa operasi sederhana - itu bisa sangat besar.


4
Panggilan fungsi virtual lebih mahal dibandingkan dengan ++ia. Terus?
Bo Persson

2

Saya telah bolak-balik pada ini setidaknya 20 kali pada proyek khusus saya. Meskipun ada bisa ada beberapa keuntungan besar dalam hal penggunaan kembali kode, kejelasan, rawatan, dan mudah dibaca, di sisi lain, kinerja hit masih melakukan eksis dengan fungsi virtual.

Apakah hit kinerja akan terlihat pada laptop / desktop / tablet modern ... mungkin tidak! Namun, dalam kasus tertentu dengan sistem tertanam, hit kinerja mungkin menjadi faktor pendorong inefisiensi kode Anda, terutama jika fungsi virtual dipanggil berulang-ulang dalam satu lingkaran.

Berikut adalah makalah tertanggal yang menjelaskan praktik terbaik untuk C / C ++ dalam konteks sistem tertanam: http://www.open-std.org/jtc1/sc22/wg21/docs/ESC_Boston_01_304_paper.pdf

Untuk menyimpulkan: tergantung pada programmer untuk memahami pro / kontra dari menggunakan konstruksi tertentu atas yang lain. Kecuali jika Anda didorong oleh kinerja super, Anda mungkin tidak peduli dengan hit kinerja dan harus menggunakan semua hal-hal OO yang rapi di C ++ untuk membantu membuat kode Anda dapat digunakan sebaik mungkin.


2

Dalam pengalaman saya, hal yang relevan utama adalah kemampuan untuk sebaris fungsi. Jika Anda memiliki kebutuhan kinerja / pengoptimalan yang menentukan suatu fungsi perlu diuraikan, maka Anda tidak dapat membuat fungsi tersebut virtual karena akan mencegahnya. Kalau tidak, Anda mungkin tidak akan melihat perbedaannya.


1

Satu hal yang perlu diperhatikan adalah bahwa ini:

boolean contains(A element) {
    for (A current: this)
        if (element.equals(current))
            return true;
    return false;
}

mungkin lebih cepat dari ini:

boolean contains(A element) {
    for (A current: this)
        if (current.equals(equals))
            return true;
    return false;
}

Ini karena metode pertama hanya memanggil satu fungsi sementara yang kedua mungkin memanggil banyak fungsi yang berbeda. Ini berlaku untuk fungsi virtual apa pun dalam bahasa apa pun.

Saya katakan "boleh" karena ini tergantung pada kompiler, cache, dll.


0

Hukuman kinerja menggunakan fungsi virtual tidak akan pernah bisa mengalahkan kelebihan yang Anda dapatkan di tingkat desain. Seharusnya panggilan ke fungsi virtual akan 25% kurang efisien daripada panggilan langsung ke fungsi statis. Ini karena ada tingkat tipuan melalui VMT. Namun waktu yang dibutuhkan untuk melakukan panggilan biasanya sangat kecil dibandingkan dengan waktu yang dibutuhkan dalam pelaksanaan fungsi Anda yang sebenarnya sehingga total biaya kinerja tidak dapat diabaikan, terutama dengan kinerja perangkat keras saat ini. Lebih jauh lagi, kompiler terkadang dapat mengoptimalkan dan melihat bahwa tidak ada panggilan virtual yang diperlukan dan mengkompilasinya menjadi panggilan statis. Jadi jangan khawatir gunakan fungsi virtual dan kelas abstrak sebanyak yang Anda butuhkan.


2
tidak pernah, sekecil apa pun komputer target?
zumalifeguard

Saya mungkin setuju seandainya Anda mengutarakannya sebagai The performance penalty of using virtual functions can sometimes be so insignificant that it is completely outweighed by the advantages you get at the design level.Perbedaan utama dikatakan sometimes, bukan never.
underscore_d

-1

Saya selalu mempertanyakan hal ini pada diri saya sendiri, terutama karena - beberapa tahun yang lalu - Saya juga melakukan tes seperti membandingkan waktu panggilan metode anggota standar dengan yang virtual dan benar-benar marah tentang hasilnya pada waktu itu, memiliki panggilan virtual kosong menjadi 8 kali lebih lambat daripada non-virtual.

Hari ini saya harus memutuskan apakah akan menggunakan fungsi virtual atau tidak untuk mengalokasikan lebih banyak memori di kelas buffer saya, dalam aplikasi yang sangat kritis kinerja, jadi saya googled (dan menemukan Anda), dan pada akhirnya, melakukan tes lagi.

// g++ -std=c++0x -o perf perf.cpp -lrt
#include <typeinfo>    // typeid
#include <cstdio>      // printf
#include <cstdlib>     // atoll
#include <ctime>       // clock_gettime

struct Virtual { virtual int call() { return 42; } }; 
struct Inline { inline int call() { return 42; } }; 
struct Normal { int call(); };
int Normal::call() { return 42; }

template<typename T>
void test(unsigned long long count) {
    std::printf("Timing function calls of '%s' %llu times ...\n", typeid(T).name(), count);

    timespec t0, t1;
    clock_gettime(CLOCK_REALTIME, &t0);

    T test;
    while (count--) test.call();

    clock_gettime(CLOCK_REALTIME, &t1);
    t1.tv_sec -= t0.tv_sec;
    t1.tv_nsec = t1.tv_nsec > t0.tv_nsec
        ? t1.tv_nsec - t0.tv_nsec
        : 1000000000lu - t0.tv_nsec;

    std::printf(" -- result: %d sec %ld nsec\n", t1.tv_sec, t1.tv_nsec);
}

template<typename T, typename Ua, typename... Un>
void test(unsigned long long count) {
    test<T>(count);
    test<Ua, Un...>(count);
}

int main(int argc, const char* argv[]) {
    test<Inline, Normal, Virtual>(argc == 2 ? atoll(argv[1]) : 10000000000llu);
    return 0;
}

Dan sangat terkejut bahwa itu - pada kenyataannya - benar-benar tidak penting lagi. Meskipun masuk akal untuk memiliki inline lebih cepat daripada non-virtual, dan mereka lebih cepat daripada virtual, sering terjadi pada beban komputer secara keseluruhan, apakah cache Anda memiliki data yang diperlukan atau tidak, dan sementara Anda mungkin dapat mengoptimalkan pada tingkat cache, saya pikir, bahwa ini harus dilakukan oleh pengembang kompiler lebih dari oleh pengembang aplikasi.


12
Saya pikir sangat mungkin bahwa kompiler Anda dapat mengatakan bahwa panggilan fungsi virtual dalam kode Anda hanya dapat memanggil Virtual :: call. Dalam hal ini hanya bisa sebaris itu. Juga tidak ada yang mencegah kompiler dari inlining panggilan Normal :: meskipun Anda tidak memintanya. Jadi saya pikir sangat mungkin Anda mendapatkan waktu yang sama untuk 3 operasi karena kompiler menghasilkan kode yang sama untuk mereka.
Bjarke H. Roune
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.