C ++ sınıfında sanal bir yönteme sahip olmanın performans maliyeti nedir?


107

Bir C ++ sınıfında (veya üst sınıflarından herhangi birinde) en az bir sanal yönteme sahip olmak, sınıfın sanal bir tabloya sahip olacağı ve her örneğin sanal bir işaretçiye sahip olacağı anlamına gelir.

Yani hafıza maliyeti oldukça açık. En önemlisi, örneklerdeki bellek maliyetidir (özellikle örnekler küçükse, örneğin yalnızca bir tamsayı içermeleri amaçlanıyorsa: bu durumda her örnekte bir sanal işaretçiye sahip olmak örneklerin boyutunu ikiye katlayabilir. sanal tablolar tarafından kullanılan bellek alanı, gerçek yöntem kodu tarafından kullanılan alana kıyasla genellikle ihmal edilebilir.

Bu beni soruma getiriyor: Bir yöntemi sanal yapmak için ölçülebilir bir performans maliyeti (yani hız etkisi) var mı? Çalışma zamanında, her yöntem çağrısında sanal tabloda bir arama olacaktır, bu nedenle bu yönteme çok sık çağrılar varsa ve bu yöntem çok kısaysa, ölçülebilir bir performans vuruşu olabilir mi? Sanırım bu platforma bağlı, ancak herhangi biri bazı kriterler uyguladı mı?

Sormamın nedeni, bir programcının sanal bir yöntemi tanımlamayı unutmasından kaynaklanan bir hatayla karşılaşmam. Bu tür bir hatayı ilk kez görmüyorum. Düşündüm: neden biz do eklemek sanal anahtar kelime yerine gerektiğinde çıkarmadan kesinlikle emin olduğunu zaman sanal anahtar kelime değil gerekli? Performans maliyeti düşükse, ekibimde aşağıdakileri tavsiye edeceğimi düşünüyorum: her sınıfta yıkıcı dahil olmak üzere her yöntemi varsayılan olarak sanal yapın ve yalnızca ihtiyacınız olduğunda kaldırın. Bu sana çılgınca geliyor mu?



7
Sanal ve sanal olmayan aramaları karşılaştırmak önemli değildir. Farklı işlevsellik sağlarlar. Sanal işlev çağrılarını C eşdeğeriyle karşılaştırmak istiyorsanız, sanal işlevin eşdeğer özelliğini uygulayan kodun maliyetini eklemeniz gerekir.
Martin York

Bu ya bir switch ifadesi ya da büyük bir if ifadesidir. Eğer zekiyseniz, bir fonksiyon gösterici tablosu kullanarak yeniden uygulayabilirsiniz, ancak yanlış yapma olasılıkları çok daha yüksektir.
Martin York


7
Soru, sanal olması gerekmeyen işlev çağrıları hakkındadır, dolayısıyla karşılaştırma anlamlıdır.
Mark Ransom

Yanıtlar:


104

Ben bazı zamanlamaları ran 3 GHz içinde sipariş PowerPC işlemcisi üzerinde. Bu mimaride, bir sanal işlev çağrısı, doğrudan (sanal olmayan) bir işlev çağrısından 7 nanosaniye daha uzundur.

Bu nedenle, işlev, satır içi dışında herhangi bir şeyin boşa gittiği önemsiz bir Get () / Set () erişimcisi gibi bir şey olmadığı sürece, maliyet konusunda gerçekten endişelenmeye değmez. 0,5 ns'ye satır içi olan bir işlevin 7ns ek yükü şiddetlidir; 500ms süren bir fonksiyonun 7ns'lik ek yükü anlamsızdır.

Sanal işlevlerin büyük maliyeti, gerçekte vtable'daki bir işlev işaretçisinin aranması değildir (bu genellikle yalnızca tek bir döngüdür), ancak dolaylı sıçrama genellikle dallanma ile tahmin edilemez. Bu, işlemci dolaylı atlama (işlev göstergesinden çağrı) kullanımdan kalkana ve yeni bir komut işaretçisi hesaplanana kadar herhangi bir talimatı alamayacağından büyük bir boru hattı balonuna neden olabilir. Yani, bir sanal işlev çağrısının maliyeti, montaja bakıldığında göründüğünden çok daha fazla ... ama yine de sadece 7 nanosaniye.

Düzenleme: Andrew, Emin Değilim ve diğerleri, sanal bir işlev çağrısının bir talimat önbelleğinin kaçırılmasına neden olabileceği konusunda çok iyi bir noktaya işaret ediyor: önbellekte olmayan bir kod adresine atlarsanız, o zaman tüm program durma noktasına gelirken talimatlar ana bellekten alınır. Bu her zaman önemli bir duraklamadır: Xenon'da yaklaşık 650 döngü (testlerime göre).

Ancak bu, sanal işlevlere özgü bir sorun değildir, çünkü doğrudan bir işlev çağrısı bile, önbellekte olmayan talimatlara atlarsanız bir ıskalamaya neden olur. Önemli olan, işlevin yakın zamanda çalıştırılıp çalıştırılmadığı (önbellekte olma olasılığını artırarak) ve mimarinizin statik (sanal olmayan) dalları tahmin edip edemeyeceği ve bu talimatları önceden önbelleğe alıp alamayacağıdır. Benim PPC'm yok, ancak Intel'in en son donanımı olabilir.

Zamanlamalarım icache ıskalamalarının yürütme üzerindeki etkisini kontrol ediyor (kasıtlı olarak, CPU hattını tek başına incelemeye çalıştığım için), bu yüzden bu maliyeti düşürüyorlar.


3
Çevrimlerdeki maliyet, kabaca, getirme ile şubeden emekli olmanın sonu arasındaki boru hattı aşamalarının sayısına eşittir. Bu önemsiz bir maliyet değildir ve artabilir, ancak sıkı, yüksek performanslı bir döngü yazmaya çalışmadığınız sürece, muhtemelen kızartmanız için daha büyük perf balık vardır.
Crashworks

Neyden 7 nano saniye daha uzun. Normal bir arama 1 nano saniyeyse ve normal bir arama 70 nano saniye ise önemliyse o zaman değildir.
Martin York

Zamanlamalara bakarsanız, satır içi 0.66ns'ye mal olan bir işlev için, doğrudan işlev çağrısının diferansiyel ek yükünün 4.8ns ve sanal bir işlev 12.3ns (satır içi ile karşılaştırıldığında) olduğunu buldum. İşlevin kendisi bir milisaniyeye mal oluyorsa, 7 ns'nin hiçbir şey ifade etmediğini iyi bir noktaya değindin.
Crashworks

2
Daha çok 600 döngü gibi, ama bu iyi bir nokta. Bunu zamanlamaların dışında bıraktım çünkü boru hattı balonu ve prolog / epilog nedeniyle sadece genel giderlerle ilgileniyordum. İcache kaçırma, doğrudan bir işlev çağrısı için aynı kolaylıkta gerçekleşir (Xenon'un icache dalı öngörücüsü yoktur).
Crashworks

2
Küçük ayrıntı, ancak "Ancak bu özel bir sorun değil ..." ile ilgili olarak, önbellekte olması gereken fazladan bir sayfa (veya sayfa sınırının dışına çıkarsa iki) olduğu için sanal gönderim için biraz daha kötü - sınıfın Sanal Sevk Tablosu için.
Tony Delroy

19

Bir sanal işlev çağrılırken kesinlikle ölçülebilir ek yük vardır - çağrı, işlevin bu tür nesneye yönelik adresini çözmek için vtable'ı kullanmalıdır. Ekstra talimatlar endişelerinizin en küçüğüdür. Vtables yalnızca birçok potansiyel derleyici optimizasyonunu engellemekle kalmaz (tür, derleyici polimorfik olduğundan), ayrıca I-Cache'inizi de atabilir.

Elbette bu cezaların önemli olup olmadığı, uygulamanıza, bu kod yollarının ne sıklıkla yürütüldüğüne ve kalıtım modelinize bağlıdır.

Yine de bana göre, her şeyin varsayılan olarak sanal olması, başka yollarla çözebileceğiniz bir soruna genel bir çözümdür.

Belki sınıfların nasıl tasarlandığına / belgelendiğine / yazıldığına bakabilirsiniz. Genel olarak, bir sınıfın başlığı, türetilmiş sınıflar tarafından hangi işlevlerin geçersiz kılınabileceğini ve nasıl çağrıldıklarını açıkça belirtmelidir. Programcıların bu dokümantasyonu yazmasını sağlamak, doğru şekilde sanal olarak işaretlenmelerini sağlamaya yardımcı olur.

Ayrıca, her işlevi sanal olarak bildirmenin, bir şeyi sanal olarak işaretlemeyi unutmaktan daha fazla hataya yol açabileceğini söyleyebilirim. Tüm işlevler sanalsa, her şey temel sınıflarla değiştirilebilir - genel, korumalı, özel - her şey adil oyun haline gelir. Kaza veya kasıtlı olarak alt sınıflar, temel uygulamada kullanıldığında daha sonra sorunlara neden olan işlevlerin davranışını değiştirebilir.


En büyük kayıp optimizasyon, özellikle sanal işlev genellikle küçük veya boşsa satır içi yapmaktır.
Zan Lynx

@Andrew: ilginç bir bakış açısı. Yine de son paragrafınıza biraz katılmıyorum: eğer bir temel sınıf, temel sınıftaki bir işlevin savebelirli bir uygulamasına dayanan bir işleve sahipse write, o zaman bana ya savekötü kodlanmış ya writeda özel olması gerektiği anlaşılıyor .
MiniQuark

2
Yazmanın özel olması onun geçersiz kılınmasını engellemez. Bu, varsayılan olarak bir şeyleri sanal yapmamak için başka bir argümandır. Her halükarda tam tersini düşünüyordum - genel ve iyi yazılmış bir uygulamanın yerini, belirli ve uyumlu olmayan davranışa sahip bir şey alıyor.
Andrew Grant

Önbelleğe alma ile oylanır - herhangi bir büyük nesne yönelimli kod tabanında, kod yerelliği performans uygulamalarını takip etmiyorsanız, sanal aramalarınızın önbellek kaçırmalarına ve bir durmaya neden olması çok kolaydır.
Değil Emin

Ve bir buz ağrısı durması gerçekten ciddi olabilir: testlerimde 600 döngü.
Crashworks

9

Değişir. :) (Başka bir şey beklemiş miydin?)

Bir sınıf sanal bir işlev aldığında, artık bir POD veri türü olamaz (daha önce de olmayabilir, bu durumda bu bir fark yaratmaz) ve bu, tüm optimizasyonları imkansız hale getirir.

Düz POD türlerinde std :: copy () basit bir memcpy rutinine başvurabilir, ancak POD olmayan türlerin daha dikkatli kullanılması gerekir.

Vtable'ın başlatılması gerektiğinden inşaat çok daha yavaş hale geliyor. En kötü durumda, POD ve POD olmayan veri türleri arasındaki performans farkı önemli olabilir.

En kötü durumda, 5 kat daha yavaş yürütme görebilirsiniz (bu sayı, birkaç standart kütüphane sınıfını yeniden uygulamak için yakın zamanda yaptığım bir üniversite projesinden alınmıştır. Konteynerimizin, depoladığı veri türü bir veri türü alır almaz inşa edilmesi yaklaşık 5 kat daha uzun sürdü. vtable)

Tabii ki, çoğu durumda, ölçülebilir bir performans farkı görmeniz pek olası değildir, bu sadece bazı sınır durumlarında maliyetli olabileceğine işaret etmek içindir.

Ancak, performans burada göz önünde bulundurmanız gereken birincil nokta olmamalıdır. Her şeyi sanal yapmak, başka nedenlerden dolayı mükemmel bir çözüm değildir.

Türetilmiş sınıflarda her şeyin geçersiz kılınmasına izin vermek, sınıf değişmezlerini korumayı çok daha zor hale getirir. Bir sınıf, yöntemlerinden herhangi biri herhangi bir zamanda yeniden tanımlanabilecekken, tutarlı bir durumda kalmasını nasıl garanti eder?

Her şeyi sanal yapmak birkaç olası hatayı ortadan kaldırabilir, ancak yenilerini de beraberinde getirir.


7

Sanal gönderim işlevselliğine ihtiyacınız varsa, bedelini ödemeniz gerekir. C ++ 'ın avantajı, kendi uyguladığınız muhtemelen verimsiz bir sürüm yerine, derleyici tarafından sağlanan çok verimli bir sanal dağıtım uygulamasını kullanabilmenizdir.

Bununla birlikte, eğer ihtiyacınız yoksa, kendinizi ek yüke boğmak, muhtemelen biraz fazla ileri gidiyor. Ve çoğu sınıftan miras alınacak şekilde tasarlanmamıştır - iyi bir temel sınıf oluşturmak, işlevlerini sanal yapmaktan fazlasını gerektirir.


İyi yanıt ama IMO, 2. yarıda yeterince vurgulayıcı değil: İhtiyacınız yoksa kendinizi ek yüke boğmak, açıkçası, çılgınlıktır - özellikle mantrası "Yaptıklarınızın bedelini ödemeyin" olan bu dili kullanırken Kullanmayın. " Birisi neden sanal olamayacağını / olması gerektiğini gerekçelendirene kadar her şeyi varsayılan olarak sanal yapmak, iğrenç bir ilkedir.
underscore_d

5

Sanal gönderim, bazı alternatiflere göre çok daha yavaş bir sıralamadır - dolaylı yoldan değil, satır içi yazmanın engellenmesinden dolayı değil. Aşağıda, sanal gönderimi, nesnelere bir "tip (-tanımlama) numarası" yerleştiren bir uygulama ile karşılaştırarak ve türe özgü kodu seçmek için bir switch deyimi kullanarak gösteriyorum. Bu, işlev çağrısı ek yükünü tamamen önler - sadece yerel bir sıçrama yapmak. Türe özgü işlevselliğin zorunlu yerelleştirilmesi (anahtarda) yoluyla sürdürülebilirlik, yeniden derleme bağımlılıkları vb. İçin potansiyel bir maliyet vardır.


UYGULAMA

#include <iostream>
#include <vector>

// virtual dispatch model...

struct Base
{
    virtual int f() const { return 1; }
};

struct Derived : Base
{
    virtual int f() const { return 2; }
};

// alternative: member variable encodes runtime type...

struct Type
{
    Type(int type) : type_(type) { }
    int type_;
};

struct A : Type
{
    A() : Type(1) { }
    int f() const { return 1; }
};

struct B : Type
{
    B() : Type(2) { }
    int f() const { return 2; }
};

struct Timer
{
    Timer() { clock_gettime(CLOCK_MONOTONIC, &from); }
    struct timespec from;
    double elapsed() const
    {
        struct timespec to;
        clock_gettime(CLOCK_MONOTONIC, &to);
        return to.tv_sec - from.tv_sec + 1E-9 * (to.tv_nsec - from.tv_nsec);
    }
};

int main(int argc)
{
  for (int j = 0; j < 3; ++j)
  {
    typedef std::vector<Base*> V;
    V v;

    for (int i = 0; i < 1000; ++i)
        v.push_back(i % 2 ? new Base : (Base*)new Derived);

    int total = 0;

    Timer tv;

    for (int i = 0; i < 100000; ++i)
        for (V::const_iterator i = v.begin(); i != v.end(); ++i)
            total += (*i)->f();

    double tve = tv.elapsed();

    std::cout << "virtual dispatch: " << total << ' ' << tve << '\n';

    // ----------------------------

    typedef std::vector<Type*> W;
    W w;

    for (int i = 0; i < 1000; ++i)
        w.push_back(i % 2 ? (Type*)new A : (Type*)new B);

    total = 0;

    Timer tw;

    for (int i = 0; i < 100000; ++i)
        for (W::const_iterator i = w.begin(); i != w.end(); ++i)
        {
            if ((*i)->type_ == 1)
                total += ((A*)(*i))->f();
            else
                total += ((B*)(*i))->f();
        }

    double twe = tw.elapsed();

    std::cout << "switched: " << total << ' ' << twe << '\n';

    // ----------------------------

    total = 0;

    Timer tw2;

    for (int i = 0; i < 100000; ++i)
        for (W::const_iterator i = w.begin(); i != w.end(); ++i)
            total += (*i)->type_;

    double tw2e = tw2.elapsed();

    std::cout << "overheads: " << total << ' ' << tw2e << '\n';
  }
}

PERFORMANS SONUÇLARI

Linux sistemimde:

~/dev  g++ -O2 -o vdt vdt.cc -lrt
~/dev  ./vdt                     
virtual dispatch: 150000000 1.28025
switched: 150000000 0.344314
overhead: 150000000 0.229018
virtual dispatch: 150000000 1.285
switched: 150000000 0.345367
overhead: 150000000 0.231051
virtual dispatch: 150000000 1.28969
switched: 150000000 0.345876
overhead: 150000000 0.230726

Bu, satır içi tür numarası anahtarlamalı bir yaklaşımın yaklaşık (1.28 - 0.23) / (0.344 - 0.23) = 9.2 kat daha hızlı olduğunu göstermektedir. Elbette, bu test edilen tam sistem / derleyici bayrakları ve sürümü vb. İle ilgilidir, ancak genellikle gösterge niteliğindedir.


YORUMLAR GERİ SANAL GÖNDERİM

Sanal işlev çağrısı genel giderlerinin nadiren önemli olan bir şey olduğu ve daha sonra yalnızca önemsiz olarak adlandırılan işlevler (alıcılar ve ayarlayıcılar gibi) için olduğu söylenmelidir. O zaman bile, birçok şeyi aynı anda elde etmek ve ayarlamak için tek bir işlev sunabilir ve maliyeti en aza indirebilirsiniz. İnsanlar sanal gönderim konusunda çok fazla endişeleniyorlar - bu yüzden garip alternatifler bulmadan önce profil oluşturma yapın. Onlarla ilgili temel sorun, hat dışı bir işlev çağrısı gerçekleştirmeleridir, ancak aynı zamanda önbellek kullanım modellerini değiştiren (daha iyi veya (daha sık) daha kötü) çalıştırılan kodu yerelleştirirler.


Kodunuzla ilgili bir soru sordum çünkü g++/ clangve kullanarak bazı "garip" sonuçlar aldım -lrt. Gelecekteki okuyucular için burada bahsetmeye değer olduğunu düşündüm.
Holt

@Holt: şaşırtıcı sonuçlar verildiğinde iyi bir soru! Yarım şansım olursa birkaç gün içinde ona daha yakından bakacağım. Şerefe.
Tony Delroy

3

Çoğu senaryoda ekstra maliyet neredeyse hiçbir şeydir. (kelime oyununu bağışlayın). ejac zaten makul göreceli önlemler yayınladı.

Vazgeçtiğiniz en büyük şey, satır içi kullanım nedeniyle olası optimizasyonlardır. İşlev sabit parametrelerle çağrılırsa özellikle iyi olabilirler. Bu nadiren gerçek bir fark yaratır, ancak bazı durumlarda bu çok büyük olabilir.


Optimizasyonlarla ilgili olarak:
Dilinizin yapılarının göreceli maliyetini bilmek ve dikkate almak önemlidir. Büyük O notasyonu hikayenin yalnızca yarısıdır - uygulamanız nasıl ölçeklenir ? Diğer yarısı, önündeki sabit faktördür.

Genel bir kural olarak, bunun bir şişe boynu olduğuna dair net ve spesifik göstergeler olmadıkça, sanal işlevlerden kaçınmak için yolumdan çekilmem. Temiz bir tasarım her zaman önce gelir - ancak başkalarına gereksiz yere zarar vermemesi gereken yalnızca bir paydaştır .


Yapılmış Örnek: Bir milyon küçük öğeden oluşan bir dizi üzerindeki boş bir sanal yıkıcı, önbelleğinizi çöpe atarak en az 4 MB veriyi geçebilir. Bu yıkıcı satır içine alınabiliyorsa, verilere dokunulmayacaktır.

Kütüphane kodu yazarken, bu tür düşünceler erken olmaktan uzaktır. İşlevinizin etrafına kaç döngü konulacağını asla bilemezsiniz.


2

Herkes sanal yöntemlerin performansı konusunda haklıyken, bence asıl sorun takımın C ++ 'daki sanal anahtar kelimenin tanımını bilip bilmemesi.

Bu kodu düşünün, çıktı nedir?

#include <stdio.h>

class A
{
public:
    void Foo()
    {
        printf("A::Foo()\n");
    }
};

class B : public A
{
public:
    void Foo()
    {
        printf("B::Foo()\n");
    }
};

int main(int argc, char** argv)
{    
    A* a = new A();
    a->Foo();

    B* b = new B();
    b->Foo();

    A* a2 = new B();
    a2->Foo();

    return 0;
}

Burada şaşırtıcı bir şey yok:

A::Foo()
B::Foo()
A::Foo()

Hiçbir şey sanal olmadığı için. Sanal anahtar kelime hem A hem de B sınıflarında Foo'nun önüne eklenirse, çıktı için şunu elde ederiz:

A::Foo()
B::Foo()
B::Foo()

Hemen hemen herkesin beklediği gibi.

Şimdi, birisinin sanal anahtar kelime eklemeyi unutması nedeniyle hatalar olduğundan bahsettiniz. Öyleyse bu kodu göz önünde bulundurun (sanal anahtar kelimenin A'ya eklendiği ancak B sınıfına eklenmediği). Çıktı ne o zaman?

#include <stdio.h>

class A
{
public:
    virtual void Foo()
    {
        printf("A::Foo()\n");
    }
};

class B : public A
{
public:
    void Foo()
    {
        printf("B::Foo()\n");
    }
};

int main(int argc, char** argv)
{    
    A* a = new A();
    a->Foo();

    B* b = new B();
    b->Foo();

    A* a2 = new B();
    a2->Foo();

    return 0;
}

Cevap: Sanal anahtar kelimenin B'ye eklenmesi ile aynı mı? Bunun nedeni, B :: Foo'nun imzasının A :: Foo () ile tam olarak eşleşmesidir ve A'nın Foo'su sanal olduğundan, B'ler de öyle.

Şimdi, B'nin Foo'sunun sanal olduğu ve A'nın olmadığı durumu düşünün. Çıktı ne o zaman? Bu durumda çıktı

A::Foo()
B::Foo()
A::Foo()

Sanal anahtar kelime, hiyerarşide yukarı doğru değil aşağı doğru çalışır. Asla temel sınıf yöntemlerini sanal yapmaz. Hiyerarşide ilk kez sanal bir yöntemle karşılaşıldığında, polimorfizmin başladığı zamandır. Daha sonraki sınıfların önceki sınıfların sanal yöntemlere sahip olmasını sağlamanın bir yolu yoktur.

Sanal yöntemlerin, bu sınıfın gelecekteki sınıflara bazı davranışlarını geçersiz kılma / değiştirme yeteneği verdiği anlamına geldiğini unutmayın.

Dolayısıyla, sanal anahtar kelimeyi kaldırmak için bir kuralınız varsa, amaçlanan etkiye sahip olmayabilir.

C ++ 'daki sanal anahtar kelime güçlü bir kavramdır. Ekibin her üyesinin tasarlandığı gibi kullanılabilmesi için bu kavramı gerçekten bildiğinden emin olmalısınız.


Merhaba Tommy, eğitim için teşekkürler. Karşılaştığımız hata, temel sınıfın bir yöntemindeki eksik bir "sanal" anahtar sözcükten kaynaklanıyordu. BTW, tüm işlevleri sanal yapın (tersini değil), o zaman açıkça gerekli olmadığında "sanal" anahtar sözcüğü kaldırın diyorum .
MiniQuark

@MiniQuark: Tommy Hui, tüm fonksiyonları sanal yaparsanız, bir programcının anahtar kelimeyi türetilmiş bir sınıfta kaldırabileceğini ve bunun hiçbir etkisi olmadığını fark edemeyeceğini söylüyor. Sanal anahtar kelimenin kaldırılmasının her zaman temel sınıfta gerçekleştiğinden emin olmak için bir yola ihtiyacınız olacaktır.
M. Dudley

1

Platformunuza bağlı olarak, sanal bir aramanın ek yükü çok istenmeyen olabilir. Her işlevi sanal olarak bildirerek, aslında hepsini bir işlev işaretçisi aracılığıyla çağırırsınız. En azından bu fazladan bir başvurudur, ancak bazı PPC platformlarında bunu gerçekleştirmek için mikro kodlu veya başka türlü yavaş talimatlar kullanır.

Bu nedenle önerinize karşı tavsiye ederim, ancak böcekleri önlemenize yardımcı oluyorsa, takas etmeye değer olabilir. Yardım edemem ama bulmaya değer bir orta yol olması gerektiğini düşünüyorum.


-1

Sanal yöntemi çağırmak için sadece birkaç ekstra asm talimatı gerekecektir.

Ama eğlencenin (int a, int b), fun () 'ye kıyasla fazladan birkaç' itme 'komutu içerdiğinden endişeleneceğinizi sanmıyorum. Bu nedenle, özel bir duruma gelene kadar sanallar için de endişelenmeyin ve bunun gerçekten sorunlara yol açtığını görün.

Not: Sanal bir yönteminiz varsa, sanal bir yıkıcıya sahip olduğunuzdan emin olun. Bu şekilde olası sorunları önleyeceksiniz


'Xtofl' ve 'Tom' yorumlarına yanıt olarak. 3 işlevli küçük testler yaptım:

  1. Gerçek
  2. Normal
  3. 3 int parametreli normal

Testim basit bir yinelemeydi:

for(int it = 0; it < 100000000; it ++) {
    test.Method();
}

Ve işte sonuçlar:

  1. 3.913 saniye
  2. 3,873 saniye
  3. 3.970 saniye

Hata ayıklama modunda VC ++ tarafından derlenmiştir. Metot başına sadece 5 test yaptım ve ortalama değeri hesapladım (bu nedenle sonuçlar oldukça yanlış olabilir) ... Her şekilde, değerler 100 milyon çağrı varsayarsak neredeyse eşit. Ve 3 ekstra itme / patlatma yöntemi daha yavaştı.

Ana nokta şudur: push / pop benzetmesini beğenmezseniz, kodunuzda ekstra if / else olduğunu mu düşünüyorsunuz? Fazladan if / else eklediğinizde CPU pipeline'ı düşünüyor musunuz ;-) Ayrıca, kodun hangi CPU'yu çalıştıracağını asla bilemezsiniz ... Her zamanki derleyici, bir CPU için daha uygun ve diğeri için daha az optimal kod üretebilir ( Intel C ++ Derleyici )


2
ekstra asm yalnızca bir sayfa hatasını tetikleyebilir (bu, sanal olmayan işlevler için geçerli değildir) - Bence sorunu fazlasıyla basitleştiriyorsunuz.
xtofl

2
Xtofl'un yorumuna +1. Sanal işlevler, ardışık düzen "kabarcıklarını" ortaya çıkaran ve önbelleğe alma davranışını etkileyen indireksiyonu sunar.
Tom

1
Hata ayıklama modunda herhangi bir şeyi zamanlamak anlamsızdır. MSVC, hata ayıklama modunda çok yavaş kod yapar ve döngü ek yükü muhtemelen farkın çoğunu gizler. Eğer yüksek performans için hedef konum, evet gerektiğini hızlı yolu if / else dalları minimize düşünün. Düşük seviyeli x86 performans optimizasyonu hakkında daha fazla bilgi için agner.org/optimize sayfasına bakın . (Ayrıca x86 etiket wiki'sindeki
Peter Cordes 9'17

1
@Tom: Buradaki kilit nokta, sanal olmayan işlevlerin sıralı olabileceği, ancak sanal olarak yapılamayacağıdır (derleyici, geçersiz kılmada kullandıysanız finalve türetilmiş türe yönelik bir işaretçiniz yoksa, derleyici dev sanallaştıramazsa, temel tür yerine ). Bu test her seferinde aynı sanal işlevi çağırdı, dolayısıyla mükemmel bir öngörüde bulundu; sınırlı calliş hacmi dışında hiçbir boru hattı kabarcığı yok . Ve bu dolaylı callbirkaç şey daha olabilir. Şube tahmini, dolaylı şubeler için bile, özellikle de her zaman aynı hedefteyse, işe yarar.
Peter Cordes

Bu, mikro ölçütlerin ortak tuzağına düşüyor: Dal belirleyicileri sıcak olduğunda ve başka hiçbir şey olmadığında hızlı görünüyor. Yanlış tahmin ek yükü, dolaylı için doğrudan callolduğundan daha yüksektir call. (Ve evet, normal callkomutların da tahmin edilmesi gerekir. Getirme aşaması, bu bloğun kodu çözülmeden önce alınacak bir sonraki adresi bilmek zorundadır, bu nedenle bir sonraki getirme bloğunu komut adresi yerine mevcut blok adresine göre tahmin etmek zorundadır. bu blokta nerede bir dal talimatı olduğunu tahmin etmek için ...)
Peter Cordes 9'17
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.