我是否错过了某些内容?还是虚拟通话的效果不如人们对它们的评价

Had*_*ber 4 c++ embedded virtual vtable

我一直在为嵌入式环境开发一个简单的框架。我决定要使用虚拟调用,CRTP还是使用switch语句。有人告诉我vtables在嵌入式中的性能很差。
从这个问题开始, vvable性能下降vs. switch语句, 我决定运行自己的测试。我用三种不同的方法来调用成员函数。

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

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

  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
}
Run Code Online (Sandbox Code Playgroud)

uli*_*ess 8

如果您真正需要的是虚拟调度,则C ++的虚拟调用可能是您可以获得的性能最高的实现,应该使用它们。数十位编译器工程师一直致力于优化它们,以使其能够获得最佳性能。

根据我的经验,当人们不需要虚拟方法时,他们会说避免使用虚拟方法。避免在可以静态分派的方法上以及代码中的热点上使用virtual关键字。

每次调用对象的虚拟方法时,都会发生这样的情况:访问对象的v表(可能会破坏内存位置并刷新一两个缓存),然后将取消引用指针以获取实际函数地址,并且那么实际的函数调用就会发生。这仅慢了几分之一秒,但是如果您在循环中将分数慢得足够慢,它会突然有所作为。

当您调用静态方法时,没有任何较早的操作发生。实际的函数调用只是发生。如果调用的函数和被调用的函数在内存中彼此接近,则所有缓存都可以保持原样。

因此,避免在紧密循环中的高性能或低CPU功耗情况下进行虚拟调度(例如,可以打开成员变量并调用包含整个循环的方法)。

但是有句俗话:“过早的优化是万恶之源”。事先评估性能。与几年前相比,“嵌入式” CPU变得更快,更强大。与仅适用于新的或特殊的CPU相比,用于流行CPU的编译器的优化效果更好。可能仅仅是编译器具有可减轻任何问题的优化器,或者您的CPU与普通的台式机CPU足够相似,从而可以从更流行的CPU中受益。

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

因此,进行概要分析,如果概要分析器说还可以,那就很好。还要确保您的测试具有代表性。您的测试代码的编写方式可能是:网络请求先于switch语句被抢占,并使它看起来比实际速度慢,或者虚拟方法调用受益于非虚拟调用加载的缓存。 。