我是不是遗漏了什么,或者虚拟电话的表现不如人们想象的那么糟糕

Am I missing something or are Virtual calls not as bad performance as people make of them

我一直在为嵌入式环境开发一个简单的框架。我做出了是使用虚拟调用、CRTP 还是 switch 语句的设计决定。有人告诉我 vtables 在嵌入式中表现不佳。
从这个问题跟进 vftable performance penalty vs. switch statement 我决定 运行 我自己的测试。我运行调用成员函数的三种不同方式。

  1. 使用 etl 库的 etl::function,一个旨在模仿 stl 库但用于嵌入式环境的库。(无动态分配)。
  2. 使用将根据对象的 int ID 调用对象的主 switch 语句
  3. 使用对基的纯虚拟调用class

我从来没有用基本的 CRTP 模式尝试过,但是 etl::function 应该是用于该模式的机制的变体。 我在 ARM Cortex M4 上获得 MSVC 和类似性能的时间是

  1. etl:4 亿纳秒
  2. 开关:4.2 亿纳秒
  3. 虚拟:2.9 亿纳秒

纯虚拟调用明显更快。 我是不是遗漏了什么,或者虚拟电话并没有人们想象的那么糟糕。这是用于测试的代码。

 class testetlFunc
{
public:
    uint32_t a;

    testetlFunc() { a = 0; };

    void foo();
};

class testetlFunc2
{
public:
    uint32_t a;

    testetlFunc2() { a = 0; };

    virtual void foo() = 0;
};

void testetlFunc::foo()
{
    a++; 
}


class testetlFuncDerived : public testetlFunc2
{
public:
    testetlFuncDerived(); 

    void foo() override;
};

testetlFuncDerived::testetlFuncDerived()
{ 
}

void testetlFuncDerived::foo()
{
    a++; 
}


etl::ifunction<void>* timer1_callback1;
etl::ifunction<void>* timer1_callback2;
etl::ifunction<void>* timer1_callback3;
etl::ifunction<void>* timer1_callback4;
etl::ifunction<void>* etlcallbacks[4];

testetlFunc ttt;
testetlFunc ttt2;
testetlFunc ttt3;
testetlFunc ttt4;
testetlFuncDerived tttd1;
testetlFuncDerived tttd2;
testetlFuncDerived tttd3;
testetlFuncDerived tttd4;
testetlFunc2* tttarr[4];

static void MasterCallingFunction(uint16_t ID) {
    switch (ID)
    {
    case 1:
        ttt.foo();
        break;
    case 2:
        ttt2.foo();
        break;
    case 3:
        ttt3.foo();
        break;
    case 4:
        ttt4.foo();
        break;
    default:
        break;
    }
};






int main()
{

    tttarr[0] = (testetlFunc2*)&tttd1;
    tttarr[1] = (testetlFunc2*)&tttd2;
    tttarr[2] = (testetlFunc2*)&tttd3;
    tttarr[3] = (testetlFunc2*)&tttd4;

    etl::function_imv<testetlFunc, ttt, &testetlFunc::foo> k;
    timer1_callback1 = &k;
    etl::function_imv<testetlFunc, ttt2, &testetlFunc::foo> k2;
    timer1_callback2 = &k2;
    etl::function_imv<testetlFunc, ttt3, &testetlFunc::foo> k3;
    timer1_callback3 = &k3;
    etl::function_imv<testetlFunc, ttt4, &testetlFunc::foo> k4;
    timer1_callback4 = &k4;
etlcallbacks[0] = timer1_callback1;
    etlcallbacks[1] = timer1_callback2;
    etlcallbacks[2] = timer1_callback3;
    etlcallbacks[3] = timer1_callback4;

    //results for etl::function --------------
    int rng;
    srand(time(0));
    StartTimer(1)
    for (uint32_t i = 0; i < 2000000; i++)
    {
        rng = rand() % 4 + 0;
        for (uint16_t j= 0; j < 4; j++)
        {
            (*etlcallbacks[rng])();
        }
    }
    StopTimer(1)

    //results for switch --------------
    StartTimer(2)
    for (uint32_t i = 0; i < 2000000; i++)
    {
        rng = rand() % 4 + 0;
        for (uint16_t j = 0; j < 4; j++)
        {
            MasterCallingFunction(rng);
        }
    }
    StopTimer(2)
        //results for virtual vtable --------------
        StartTimer(3)
        for (uint32_t i = 0; i < 2000000; i++)
        {
            rng = rand() % 4 + 0;
            for (uint16_t j = 0; j < 4; j++)
            {
                tttarr[rng]->foo();
                //ttt.foo();
            }
        }
    StopTimer(3)
PrintAllTimerDuration
}

如果您真正需要的是虚拟分派,C++ 的虚拟调用可能是您可以获得的最高效的实现,您应该使用它们。许多编译器工程师致力于优化它们以获得最佳性能。

人们说要避免虚拟方法背后的原因是根据我的经验,当你不需要它们时。避免在可以静态分派的方法和代码中的热点上使用 virtual 关键字。

每次调用对象的虚方法时,都会访问对象的 v-table(可能会搞砸内存局部性并刷新一两个缓存),然后指针是 de-referenced获取实际的函数地址,然后实际的函数调用发生。这只是慢了几分之一秒,但如果你在一个循环中慢了足够多的几分之一秒,它就会突然有所不同。

调用静态方法时,会发生 none 之前的操作。实际的函数调用刚刚发生。如果调用的函数和被调用的函数在内存中彼此靠近,则所有缓存都可以保持原样。

因此,避免在 high-performance 或 low-CPU-power 紧密循环的情况下进行虚拟分派(例如,您可以打开成员变量并调用包含整个循环的方法)。

但是有一句话"premature optimization is the root of all evil"。事先测量性能。 "Embedded" CPU 比几年前变得更快、更强大。针对流行 CPU 的编译器比只适应新的或异国情调的 CPU 的编译器优化得更好。可能只是因为您的编译器有一个优化器可以缓解任何问题,或者您的 CPU 与普通桌面 CPU 足够相似,可以从为更受欢迎的 CPU 所做的工作中获益s.

或者您的 RAM 等可能比告诉您避免虚拟呼叫的人多。

所以,配置文件,如果配置文件说没问题,那就没问题。还要确保您的测试具有代表性。您的测试代码可能只是以网络请求进入 pre-empted switch 语句并使其看起来比实际速度慢的方式编写,或者虚拟方法调用受益于 [= 加载的缓存35=] 来电。