代码之家  ›  专栏  ›  技术社区  ›  Navaneeth K N

虚拟函数与性能——C++

  •  110
  • Navaneeth K N  · 技术社区  · 16 年前

    在我的类设计中,我广泛使用抽象类和虚拟函数。我觉得虚拟功能会影响性能。这是真的吗?但我认为这种性能差异并不明显,看起来我正在做过早的优化。对吗?

    15 回复  |  直到 9 年前
        1
  •  86
  •   Greg Hewgill    16 年前

    一个好的经验法则是:

    这不是性能问题,除非你能证明。

    使用虚拟函数对性能的影响很小,但不太可能影响应用程序的整体性能。在算法和I/O中寻找性能改进的更好地方。

    一篇讨论虚拟函数(以及更多)的优秀文章是 Member Function Pointers and the Fastest Possible C++ Delegates .

        2
  •  160
  •   Crashworks    9 年前

    你的问题让我很好奇,所以我在3GHz上进行了一些计时,以便我们使用PowerPCCPU。我运行的测试是用get/set函数生成一个简单的4d向量类

    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 
    }
    

    然后,我设置了三个数组,每个数组包含1024个这些向量(足够小以适合l1),并运行了一个循环,将它们相互添加(a.x=b.x+c.x)1000次。我用定义为 inline , virtual 和常规函数调用。结果如下:

    • 内联:8ms(每次呼叫0.65ns)
    • 直接:68ms(每次通话5.53ns)
    • 虚拟:160ms(每次通话13ns)

    因此,在这种情况下(所有东西都放在缓存中),虚拟函数调用比内联调用慢20倍。但这到底意味着什么?每次通过回路都会导致 3 * 4 * 1024 = 12,288 函数调用(1024个向量乘以四个分量乘以每个加法三个调用),因此这些时间表示 1000 * 12,288 = 12,288,000 函数调用。虚拟循环比直接循环耗时92ms,因此每次调用的额外开销为7 纳秒 每个函数。

    由此我得出结论: ,虚拟函数比直接函数慢得多,并且 除非你计划每秒给他们打1000万次电话,否则没关系。

    参见: comparison of the generated assembly.

        3
  •  40
  •   Chuck    16 年前

    当objective-c(所有方法都是虚拟的)是iphone和freakin的主要语言时 爪哇 是Android的主要语言,我认为在我们的3 GHz双核塔上使用C++虚拟功能是相当安全的。

        4
  •  32
  •   Mark James    16 年前

    在性能非常关键的应用程序(如视频游戏)中,虚拟函数调用可能太慢。对于现代硬件,最大的性能问题是缓存丢失。如果数据不在缓存中,可能需要数百个周期才能使用。

    当CPU获取新函数的第一条指令且不在缓存中时,正常的函数调用可能会生成指令缓存未命中。

    虚拟函数调用首先需要从对象加载vtable指针。这可能导致数据缓存未命中。然后它从vtable加载函数指针,这可能导致另一个数据缓存未命中。然后它调用函数,这会导致指令缓存丢失,就像非虚拟函数一样。

    在许多情况下,两个额外的缓存未命中不是一个问题,但是在性能关键的代码上,通过一个紧密的循环,它可以显著降低性能。

        5
  •  27
  •   Boojum    16 年前

    从第44页 Agner Fog's "Optimizing Software in C++" manual :

    调用虚拟成员函数所需的时间比调用非虚拟成员函数所需的时间长几个时钟周期,前提是函数调用语句始终调用相同版本的虚拟函数。如果版本更改,您将得到10-30个时钟周期的预测失误惩罚。虚拟函数调用的预测和预测失误的规则与switch语句的规则相同…

        6
  •  6
  •   gbjbaanb    16 年前

    当然。当计算机以100MHz运行时,这是一个问题,因为在调用vtable之前,每个方法调用都需要查找它。但是今天…在3GHz的CPU上,第一级缓存的内存比我的第一台计算机多?一点也不。从主RAM分配内存将比所有函数都是虚拟的花费更多的时间。

    就像以前人们说结构化编程很慢的时候,因为所有的代码都被分解成函数,每个函数都需要堆栈分配和函数调用!

    唯一一次我会考虑考虑虚拟函数的性能影响的时候,是如果它被大量使用,并以模板化的代码进行实例化,最终在所有事情中结束。即使那样,我也不会花太多精力去做!

    PS想到了其他“易于使用”的语言——他们所有的方法都是虚拟的,而且现在他们不爬行。

        7
  •  6
  •   Community CDub    8 年前

    除了执行时间,还有另一个性能标准。vtable也占用内存空间,在某些情况下可以避免:atl使用编译时” simulated dynamic binding templates 要得到“静态多态性”的效果,这有点难以解释;您基本上将派生类作为参数传递给基类模板,因此在编译时,基类“知道”其派生类在每个实例中是什么。不会让您将多个不同的派生类存储在一个基类型集合中(这是运行时多态性),但从静态意义上讲,如果您想使一个类Y与一个先前存在的模板类X相同,该模板类X具有用于这种重写的挂钩,您只需重写您关心的方法,然后获得BASe类的方法,无需vtable。

    在内存占用较大的类中,单个vtable指针的成本不高,但是COM中的一些atl类非常小,如果永远不会发生运行时多态性的情况,那么值得节省vtable。

    也见 this other SO question .

    顺便问一下,这是 a posting I found 这将讨论CPU时间性能方面。

        8
  •  4
  •   Serge    16 年前

    是的,您是对的,如果您对虚拟函数调用的成本感兴趣,您可能会发现 this post 有趣。

        9
  •  3
  •   Dominik Grabiec    16 年前

    我唯一能看到虚拟函数会成为性能问题的方法就是在一个紧密的循环中调用许多虚拟函数,以及 当且仅当 它们会导致页面错误或发生其他“重”内存操作。

    尽管像其他人所说的,在现实生活中,这几乎永远不会成为你的问题。如果您认为是这样,请运行一个分析器,进行一些测试,并在尝试“取消签名”代码以获得性能优势之前验证这是否真的是一个问题。

        10
  •  3
  •   Evgueny Sedov    13 年前

    当类方法不是虚拟的时,编译器通常在内联中执行。相反,当使用指向带有虚函数的类的指针时,只有在运行时才知道实际地址。

    这一点在测试中得到了很好的说明,时差约为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;
    }
    

    虚拟函数调用的影响很大程度上取决于情况。 如果在函数内部只有很少的调用和大量的工作,则可以忽略不计。

    或者,当它是一个多次重复使用的虚拟调用时,同时执行一些简单的操作——它可能非常大。

        11
  •  2
  •   It'sPete    12 年前

    在我的特定项目中,我已经反复讨论了至少20次。虽然那里 可以 在代码重用、清晰性、可维护性和可读性方面取得了巨大的进步,另一方面,性能仍然受到影响。 具有虚拟功能。

    在现代笔记本电脑/台式机/平板电脑上,性能的冲击会很明显吗?大概不会吧!但是,在某些嵌入式系统的情况下,性能下降可能是导致代码效率低下的原因,特别是在循环中反复调用虚拟函数的情况下。

    这里有一篇关于在嵌入式系统环境中C/C++的最佳实践的日期纸: http://www.open-std.org/jtc1/sc22/wg21/docs/ESC_Boston_01_304_paper.pdf

    总而言之:由程序员来理解使用某个构造胜过另一个构造的优缺点。除非你是超级性能驱动的,你可能不关心性能命中,并且应该使用C++中所有整洁的OO东西来帮助你的代码尽可能的使用。

        12
  •  2
  •   Hurkyl    10 年前

    在我的经验中,最主要的相关问题是内联函数的能力。如果您有性能/优化需求,要求一个函数需要内联,那么您就不能使该函数成为虚拟的,因为它会阻止这种情况发生。否则,您可能不会注意到差异。

        13
  •  1
  •   nikdeapen    9 年前

    需要注意的一点是:

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

    可能比这快:

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

    这是因为第一个方法只调用一个函数,而第二个方法可能调用许多不同的函数。这适用于任何语言的任何虚拟函数。

    我说“可能”,因为这取决于编译器、缓存等。

        14
  •  0
  •   Koen    16 年前

    使用虚拟函数的性能损失永远不会超过您在设计级别所获得的优势。假设对虚拟函数的调用比直接调用静态函数效率低25%。这是因为通过vmt有一个间接级别。但是,与实际执行函数所花费的时间相比,进行调用所花费的时间通常非常小,因此总的性能成本将很低,特别是在当前硬件性能方面。 此外,编译器有时可以优化并看到不需要虚拟调用,并将其编译为静态调用。所以不要担心使用虚拟函数和抽象类。

        15
  •  -1
  •   christianparpart    14 年前

    我一直在质疑自己这一点,尤其是几年前,我还做了这样一个测试,将标准成员方法调用的时间与虚拟方法调用的时间进行了比较,当时我对结果非常愤怒,因为空的虚拟调用比非虚拟调用慢8倍。

    今天,我必须决定是否使用一个虚拟函数在我的缓冲区类、一个性能非常关键的应用程序中分配更多的内存,所以我在谷歌上搜索(找到了你),最后再次进行了测试。

    // 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;
    }
    

    我真的很惊讶它——事实上——真的不再重要了。 虽然拥有比非虚拟更快的入口是有道理的,而且它们比虚拟更快,但通常涉及到计算机的总体负载,不管您的缓存是否有必要的数据,而且尽管您可能能够在缓存级别进行优化,我认为这应该由编译器开发人员完成,而不是由应用程序完成。开发者。