gpt4 book ai didi

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

转载 作者:塔克拉玛干 更新时间:2023-11-03 01:21:37 24 4
gpt4 key购买 nike

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

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

我从来没有用基本的 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
}

最佳答案

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

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

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

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

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

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

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

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

关于c++ - 我是不是遗漏了什么,或者虚拟电话的表现不如人们想象的那么糟糕,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/57135141/

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