gpt4 book ai didi

c++ - 虚函数性能: one large class vs many smaller subclasses

转载 作者:行者123 更新时间:2023-12-03 18:56:17 27 4
gpt4 key购买 nike

我正在重构我制作的c++ OpenGL应用程序(从技术上讲,该应用程序大量使用了Qt的QQuickItem类中的瘦OpenGL包装器)。我的应用程序运行正常,但可能会更好。

我很好奇的问题之一是在时间敏感型(帧速率)算法中使用virtual函数。我的OpenGL绘图代码在需要绘图的各种对象上调用了许多virtual函数。由于这种情况每秒发生多次,因此我想知道virtual调度是否可以降低帧速率。

我正在考虑更改为这种结构,从而通过将所有内容保留在一个基类中来避免继承,但是以前的virtual函数现在仅包含switch语句,以根据该类的“类型”调用适当的例程,而这实际上只是一个typedef enum:

之前:

struct Base{
virtual void a()=0;
virtual void b()=0;
}

struct One : public Base{
void a(){...}
void b(){...}
}

考虑:
struct Combined{

MyEnumTypeDef t; //essentially holds what "type" of object this is

void a(){

switch (t){

case One:
....
break;

case Two:
....
break;
}
}
}

当在OpenGL绘图例程中经常调用 a()函数时,我倾向于认为 Combined类的效率将大大提高,因为它不需要在虚拟表上进行动态分派(dispatch)。

如果这是明智的选择,我将不胜感激。

最佳答案

就您而言,这可能并不重要。我说这可能是因为,并且我有 build 性的意思是,您没有指定性能要求,也没有指定调用该函数的频率,这一事实表明您现在可能没有足够的信息来做出判断-“不要” t推测:“概要”一揽子响应实际上仅旨在确保您拥有所需的所有必要信息,因为过早的微优化非常普遍,我们的真正目标是在全局上帮助您。

杰里米·弗里斯纳(Jeremy Friesner)的his comment on another answer here确实打在了头上:

If you don't understand why it is slow you won't be able to speed it up.



因此,考虑到所有这些,假设以下条件之一:A)您的性能要求已经得到满足(例如,您要达到4000 FPS-远远高于任何显示刷新率)或B)您正在努力满足性能要求,并且此功能是或C)您正在努力满足性能要求,并且经常调用此函数,但做了很多其他重要工作(因此,函数调用开销可忽略不计),然后:

使用 virtual函数最多可能最终会在某个地方的表中进行一次额外的查找(并且可能会丢失一些高速缓存,但是如果在一个内部循环中重复访问它,则不会那么多),这需要几个CPU时钟。循环最坏的情况(虽然实际上没有实际意义,但最有可能仍小于 switch),与目标帧速率,渲染帧所需的工作量以及您所使用的任何其他算法和逻辑相比, 完全不重要表演。 如果您想向自己证明,请配置文件。

您应该做的是使用能导致最清晰,最干净,最易维护且易读的代码的任何技术。诸如此类的微优化将不会产生效果,并且代码可维护性的成本(即使很小)也不值得受益,因为该成本基本上为零。

您还应该做的就是坐下并掌握实际情况。您是否需要提高性能?该功能是否足以引起实际影响?或者您应该专注于其他技术(例如,更高级别的算法,其他设计策略,将计算任务卸载到GPU还是使用特定于机器的优化,例如,使用SSE进行批量操作等)? )?

在没有具体信息的情况下,您可以做的一件事就是尝试两种方法。尽管不同机器的性能会有所不同,但是您至少可以大致了解一下这段特定代码对整体性能的影响(例如,如果您拍摄的是60 FPS,那么这两个选项可为您提供23.2 FPS与。23.6 FPS,那么这不是您要关注的地方,通过选择其中一种策略而不是另一种策略而可能做出的牺牲可能是不值得的)。

还可以考虑使用调用列表,顶点索引缓冲区等。OpenGL提供了许多工具,可以在某些方面保持不变的情况下优化对象的绘制。例如,如果您有一个巨大的表面模型,其顶点坐标经常变化的小零件,则可以使用调用列表将模型划分为多个部分,并且仅在自上次重绘后更改的部分更改后才更新其调用列表。离开例如如果调用列表经常更改,请在调用列表之外进行着色和纹理处理(或使用坐标数组)。这样,您可以避免完全调用函数。

如果您感到好奇,这里有一个测试程序(它可能并不代表您的实际用法,同样,也无法用给出的信息来回答-该测试是以下注释中要求的测试)。这并不意味着这些结果将反射(reflect)在您的程序中,并且再次,您需要具有有关实际需求的具体信息。但是,这只是傻笑:

该测试程序将基于开关的操作与基于虚函数的操作与指针到成员(其中成员从另一个类成员函数调用)与指针到成员(其中成员直接从测试中调用)进行比较环形)。它还执行三种类型的测试:在仅具有一个运算符的数据集上运行,在两个运算符之间来回交替的运行以及使用两个运算符的随机混合的运行。

使用 gcc -O0编译时的输出,进行1,000,000,000次迭代:
$ g++ -O0 tester.cpp
$ ./a.out
--------------------
Test: time=6.34 sec (switch add) [-358977076]
Test: time=6.44 sec (switch subtract) [358977076]
Test: time=6.96 sec (switch alternating) [-281087476]
Test: time=18.98 sec (switch mixed) [-314721196]
Test: time=6.11 sec (virtual add) [-358977076]
Test: time=6.19 sec (virtual subtract) [358977076]
Test: time=7.88 sec (virtual alternating) [-281087476]
Test: time=19.80 sec (virtual mixed) [-314721196]
Test: time=10.96 sec (ptm add) [-358977076]
Test: time=10.83 sec (ptm subtract) [358977076]
Test: time=12.53 sec (ptm alternating) [-281087476]
Test: time=24.24 sec (ptm mixed) [-314721196]
Test: time=6.94 sec (ptm add (direct)) [-358977076]
Test: time=6.89 sec (ptm subtract (direct)) [358977076]
Test: time=9.12 sec (ptm alternating (direct)) [-281087476]
Test: time=21.19 sec (ptm mixed (direct)) [-314721196]

使用 gcc -O3编译时的输出,进行1,000,000,000次迭代:
$ g++ -O3 tester.cpp ; ./a.out
--------------------
Test: time=0.87 sec (switch add) [372023620]
Test: time=1.28 sec (switch subtract) [-372023620]
Test: time=1.29 sec (switch alternating) [101645020]
Test: time=7.71 sec (switch mixed) [855607628]
Test: time=2.95 sec (virtual add) [372023620]
Test: time=2.95 sec (virtual subtract) [-372023620]
Test: time=14.74 sec (virtual alternating) [101645020]
Test: time=9.39 sec (virtual mixed) [855607628]
Test: time=4.20 sec (ptm add) [372023620]
Test: time=4.21 sec (ptm subtract) [-372023620]
Test: time=13.11 sec (ptm alternating) [101645020]
Test: time=9.32 sec (ptm mixed) [855607628]
Test: time=3.37 sec (ptm add (direct)) [372023620]
Test: time=3.37 sec (ptm subtract (direct)) [-372023620]
Test: time=13.08 sec (ptm alternating (direct)) [101645020]
Test: time=9.74 sec (ptm mixed (direct)) [855607628]

请注意, -O3发挥了很多作用,并且在不查看汇编程序的情况下,我们不能将其用作当前问题的100%准确表示。

在未优化的情况下,我们注意到:
  • 在单个运算符(operator)的运行中,虚拟性能胜过切换。
  • 在使用多个运算符的情况下,
  • Switch的性能优于虚拟。
  • 直接调用成员时指向成员的指针(object->*ptm_)与virtual相似,但要慢于virtual。
  • 通过另一种方法(object->doit()doit()称为this->*ptm_)调用成员时指向成员的指针花费的时间不到两倍。
  • 如预期的那样,由于分支预测失败,“混合”案例性能会受到影响。

  • 在优化的情况下:
  • 交换机在所有情况下均优于虚拟交换机。
  • 指向成员的指针的特性与未优化的情况类似。
  • 由于我不了解的原因,所有在某些时候涉及函数指针的“交替”情况都比-O0慢,并且比“mixed”慢。在家里的PC上不会发生这种情况。

  • 这里特别重要的是例如分支预测胜过“虚拟”与“切换”的任何选择。同样,请确保您了解您的代码并正在优化正确的东西。

    另一个重要的事情是,这表示每次操作的时间差约为1-14纳秒。这种差异对于大量的操作而言可能是显着的,但与您正在执行的其他操作相比,可以忽略不计(请注意,这些功能仅执行单个算术运算,任何其他操作都将使虚拟与切换的影响相形见))。

    还要注意,虽然通过其他类成员调用指向成员的指针直接显示了“改进”,但这会对整体设计(如此类实现)产生重大影响(至少在这种情况下,类外部的调用由于调用指针到成员函数的语法不同( ->->*),因此不能将其直接替换为另一种实现。例如,我不得不创建一整套单独的测试用例来进行处理。

    结论

    即使有几个额外的算术运算,也很容易使性能差异相形见.。还要注意,分支预测在除带有-O3的“虚拟交替”情况以外的所有影响中都具有更为重要的影响。但是,测试也不大可能代表实际应用(OP一直对其保密),并且 -O3引入了更多变量,因此结果必须要花点时间,并且不太可能适用于其他情况(换句话说,测试可能很有趣,但并不特别有意义。

    来源:
    // === begin timing ===
    #ifdef __linux__
    # include <sys/time.h>
    typedef struct timeval Time;
    static void tick (Time &t) {
    gettimeofday(&t, 0);
    }
    static double delta (const Time &a, const Time &b) {
    return
    (double)(b.tv_sec - a.tv_sec) +
    (double)(b.tv_usec - a.tv_usec) / 1000000.0;
    }
    #else // windows; untested, working from memory; sorry for compile errors
    # include <windows.h>
    typedef LARGE_INTEGER Time;
    static void tick (Time &t) {
    QueryPerformanceCounter(&t);
    }
    static double delta (const Time &a, const Time &b) {
    LARGE_INTEGER freq;
    QueryPerformanceFrequency(&freq);
    return (double)(b.QuadPart - a.QuadPart) / (double)freq.QuadPart;
    }
    #endif
    // === end timing

    #include <cstdio>
    #include <cstdlib>
    #include <ctime>

    using namespace std;

    // Size of dataset.
    static const size_t DATASET_SIZE = 10000000;

    // Repetitions per test.
    static const unsigned REPETITIONS = 100;


    // Class performs operations with a switch statement.
    class OperatorSwitch {
    public:
    enum Op { Add, Subtract };
    explicit OperatorSwitch (Op op) : op_(op) { }
    int perform (int a, int b) const {
    switch (op_) {
    case Add: return a + b;
    case Subtract: return a - b;
    }
    }
    private:
    Op op_;
    };


    // Class performs operations with pointer-to-member.
    class OperatorPTM {
    public:
    enum Op { Add, Subtract };
    explicit OperatorPTM (Op op) {
    perform_ = (op == Add) ?
    &OperatorPTM::performAdd :
    &OperatorPTM::performSubtract;
    }
    int perform (int a, int b) const { return (this->*perform_)(a, b); }
    int performAdd (int a, int b) const { return a + b; }
    int performSubtract (int a, int b) const { return a - b; }
    //private:
    int (OperatorPTM::*perform_) (int, int) const;
    };


    // Base class for virtual-function test operator.
    class OperatorBase {
    public:
    virtual ~OperatorBase () { }
    virtual int perform (int a, int b) const = 0;
    };

    // Addition
    class OperatorAdd : public OperatorBase {
    public:
    int perform (int a, int b) const { return a + b; }
    };

    // Subtraction
    class OperatorSubtract : public OperatorBase {
    public:
    int perform (int a, int b) const { return a - b; }
    };


    // No base

    // Addition
    class OperatorAddNoBase {
    public:
    int perform (int a, int b) const { return a + b; }
    };

    // Subtraction
    class OperatorSubtractNoBase {
    public:
    int perform (int a, int b) const { return a - b; }
    };



    // Processes the dataset a number of times, using 'oper'.
    template <typename T>
    static void test (const int *dataset, const T *oper, const char *name) {

    int result = 0;
    Time start, stop;

    tick(start);

    for (unsigned n = 0; n < REPETITIONS; ++ n)
    for (size_t i = 0; i < DATASET_SIZE; ++ i)
    result = oper->perform(result, dataset[i]);

    tick(stop);

    // result is computed and printed so optimizations do not discard it.
    printf("Test: time=%.2f sec (%s) [%i]\n", delta(start, stop), name, result);
    fflush(stdout);

    }


    // Processes the dataset a number of times, alternating between 'oper[0]'
    // and 'oper[1]' per element.
    template <typename T>
    static void testalt (const int *dataset, const T * const *oper, const char *name) {

    int result = 0;
    Time start, stop;

    tick(start);

    for (unsigned n = 0; n < REPETITIONS; ++ n)
    for (size_t i = 0; i < DATASET_SIZE; ++ i)
    result = oper[i&1]->perform(result, dataset[i]);

    tick(stop);

    // result is computed and printed so optimizations do not discard it.
    printf("Test: time=%.2f sec (%s) [%i]\n", delta(start, stop), name, result);
    fflush(stdout);

    }


    // Processes the dataset a number of times, choosing between 'oper[0]'
    // and 'oper[1]' randomly (based on value in dataset).
    template <typename T>
    static void testmix (const int *dataset, const T * const *oper, const char *name) {

    int result = 0;
    Time start, stop;

    tick(start);

    for (unsigned n = 0; n < REPETITIONS; ++ n)
    for (size_t i = 0; i < DATASET_SIZE; ++ i) {
    int d = dataset[i];
    result = oper[d&1]->perform(result, d);
    }

    tick(stop);

    // result is computed and printed so optimizations do not discard it.
    printf("Test: time=%.2f sec (%s) [%i]\n", delta(start, stop), name, result);
    fflush(stdout);

    }


    // Same as test() but calls perform_() pointer directly.
    static void test_ptm (const int *dataset, const OperatorPTM *oper, const char *name) {

    int result = 0;
    Time start, stop;

    tick(start);

    for (unsigned n = 0; n < REPETITIONS; ++ n)
    for (size_t i = 0; i < DATASET_SIZE; ++ i)
    result = (oper->*(oper->perform_))(result, dataset[i]);

    tick(stop);

    // result is computed and printed so optimizations do not discard it.
    printf("Test: time=%.2f sec (%s) [%i]\n", delta(start, stop), name, result);
    fflush(stdout);

    }


    // Same as testalt() but calls perform_() pointer directly.
    static void testalt_ptm (const int *dataset, const OperatorPTM * const *oper, const char *name) {

    int result = 0;
    Time start, stop;

    tick(start);

    for (unsigned n = 0; n < REPETITIONS; ++ n)
    for (size_t i = 0; i < DATASET_SIZE; ++ i) {
    const OperatorPTM *op = oper[i&1];
    result = (op->*(op->perform_))(result, dataset[i]);
    }

    tick(stop);

    // result is computed and printed so optimizations do not discard it.
    printf("Test: time=%.2f sec (%s) [%i]\n", delta(start, stop), name, result);
    fflush(stdout);

    }


    // Same as testmix() but calls perform_() pointer directly.
    static void testmix_ptm (const int *dataset, const OperatorPTM * const *oper, const char *name) {

    int result = 0;
    Time start, stop;

    tick(start);

    for (unsigned n = 0; n < REPETITIONS; ++ n)
    for (size_t i = 0; i < DATASET_SIZE; ++ i) {
    int d = dataset[i];
    const OperatorPTM *op = oper[d&1];
    result = (op->*(op->perform_))(result, d);
    }

    tick(stop);

    // result is computed and printed so optimizations do not discard it.
    printf("Test: time=%.2f sec (%s) [%i]\n", delta(start, stop), name, result);
    fflush(stdout);

    }


    int main () {

    int *dataset = new int[DATASET_SIZE];
    srand(time(NULL));
    for (int n = 0; n < DATASET_SIZE; ++ n)
    dataset[n] = rand();

    OperatorSwitch *switchAdd = new OperatorSwitch(OperatorSwitch::Add);
    OperatorSwitch *switchSub = new OperatorSwitch(OperatorSwitch::Subtract);
    OperatorSwitch *switchAlt[2] = { switchAdd, switchSub };
    OperatorBase *virtAdd = new OperatorAdd();
    OperatorBase *virtSub = new OperatorSubtract();
    OperatorBase *virtAlt[2] = { virtAdd, virtSub };
    OperatorPTM *ptmAdd = new OperatorPTM(OperatorPTM::Add);
    OperatorPTM *ptmSub = new OperatorPTM(OperatorPTM::Subtract);
    OperatorPTM *ptmAlt[2] = { ptmAdd, ptmSub };

    while (true) {
    printf("--------------------\n");
    test(dataset, switchAdd, "switch add");
    test(dataset, switchSub, "switch subtract");
    testalt(dataset, switchAlt, "switch alternating");
    testmix(dataset, switchAlt, "switch mixed");
    test(dataset, virtAdd, "virtual add");
    test(dataset, virtSub, "virtual subtract");
    testalt(dataset, virtAlt, "virtual alternating");
    testmix(dataset, virtAlt, "virtual mixed");
    test(dataset, ptmAdd, "ptm add");
    test(dataset, ptmSub, "ptm subtract");
    testalt(dataset, ptmAlt, "ptm alternating");
    testmix(dataset, ptmAlt, "ptm mixed");
    test_ptm(dataset, ptmAdd, "ptm add (direct)");
    test_ptm(dataset, ptmSub, "ptm subtract (direct)");
    testalt_ptm(dataset, ptmAlt, "ptm alternating (direct)");
    testmix_ptm(dataset, ptmAlt, "ptm mixed (direct)");
    }

    }

    关于c++ - 虚函数性能: one large class vs many smaller subclasses,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/22975959/

    27 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com