我是否错过了什么,或者虚拟呼叫的性能并不像人们所说的那样糟糕



我一直在为嵌入式环境开发一个简单的框架。我就是否使用虚拟调用、CRTP 或 switch 语句做出了设计决策。有人告诉我,vtables在嵌入式中表现不佳。
跟进这个问题 vftable 性能损失与开关语句 我决定运行自己的测试。我运行了三种不同的方法来调用成员函数。

使用 ETL 库
  1. 的 ETL::函数,该库旨在模仿 STL 库,但适用于嵌入式环境。无动态分配)。
  2. 使用 master switch 语句,该语句将根据对象的 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
}

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

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

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

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

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

但有句话说"过早优化是万恶之源"。事先衡量绩效。"嵌入式"CPU比几年前变得更快,更强大。流行 CPU 的编译器比仅适应新的或异国情调的 CPU 的编译器优化得更好。可能只是因为您的编译器具有缓解任何问题的优化器,或者您的 CPU 与普通桌面 CPU 足够相似,可以从为更流行的 CPU 完成的工作中获益。

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

所以,配置文件,如果探查器说没问题,那就没问题了。还要确保您的测试具有代表性。您的测试代码可能只是以一种方式编写,即传入的网络请求抢占了 switch 语句并使其看起来比实际慢,或者虚拟方法调用受益于非虚拟调用加载的缓存。

相关内容

最新更新