我正在评估将一个实时软件从 C/汇编语言重写为 C++/汇编语言(由于与代码的问题部分无关的原因在汇编中是绝对必要的)。
一个中断具有 3 kHz 的频率,对于每个中断,大约有 200 个不同的事情要按顺序完成。处理器以 300 MHz 运行,为我们提供了 100,000 个周期来完成这项工作。这已在 C 中通过函数指针数组解决:
// Each function does a different thing, all take one parameter being a pointer
// to a struct, each struct also being different.
void (*todolist[200])(void *parameters);
// Array of pointers to structs containing each function's parameters.
void *paramlist[200];
void realtime(void)
{
int i;
for (i = 0; i < 200; i++)
(*todolist[i])(paramlist[i]);
}
速度很重要。以上 200 次迭代每秒完成 3,000 次,因此实际上我们每秒进行 600,000 次迭代。上述 for 循环每次迭代编译为 5 个循环,产生每秒 3,000,000 个循环的总成本,即 1% 的 CPU 负载。汇编器优化可能会将其减少到四个指令,但是我担心我们可能会由于内存访问彼此接近等而导致一些额外的延迟。简而言之,我相信这五个周期是非常理想的。
现在到 C++ 重写。我们所做的那 200 件事是相互关联的。有一个参数子集,它们都需要和使用,并且在它们各自的结构中都有。因此,在 C++ 实现中,它们可以巧妙地被视为继承自一个公共基类:
class Base
{
virtual void Execute();
int something_all_things_need;
}
class Derived1 : Base
{
void Execute() { /* Do something */ }
int own_parameter;
// Other own parameters
}
class Derived2 : Base { /* Etc. */ }
Base *todolist[200];
void realtime(void)
{
for (int i = 0; i < 200; i++)
todolist[i]->Execute(); // vtable look-up! 20+ cycles.
}
我的问题是 vtable 查找。我不能每秒进行 600,000 次查找;这将占 CPU 负载浪费的 4% 以上。此外,todolist 在运行时永远不会改变,它只在启动时设置一次,因此查找要调用的函数的努力确实是浪费。在问自己“可能的最佳最终结果是什么”这个问题时,我查看了 C 解决方案给出的汇编代码,并重新找到了一个函数指针数组......
在 C++ 中执行此操作的干净且正确的方法是什么?当最终出于性能原因再次选择函数指针时,制作一个好的基类、派生类等感觉毫无意义。
更新(包括更正循环开始的位置):
处理器是ADSP-214xx,编译器是VisualDSP++ 5.0。启用#pragma optimize_for_speed
时,C 循环为 9 个周期。在我看来,对其进行装配优化会产生 4 个周期,但是我没有对其进行测试,因此无法保证。C++ 循环为 14 个循环。我知道编译器可以做得更好,但是我不想将其视为编译器问题 - 在嵌入式环境中,不使用多态性仍然是可取的,并且设计选择仍然让我感兴趣。作为参考,这里生成的程序集:
C:
i3=0xb27ba;
i5=0xb28e6;
r15=0xc8;
这是实际的循环:
r4=dm(i5,m6);
i12=dm(i3,m6);
r2=i6;
i6=i7;
jump (m13,i12) (db);
dm(i7,m7)=r2;
dm(i7,m7)=0x1279de;
r15=r15-1;
if ne jump (pc, 0xfffffff2);
C++:
i5=0xb279a;
r15=0xc8;
这是实际的循环:
i5=modify(i5,m6);
i4=dm(m7,i5);
r2=i4;
i4=dm(m6,i4);
r1=dm(0x3,i4);
r4=r2+r1;
i12=dm(0x5,i4);
r2=i6;
i6=i7;
jump (m13,i12) (db);
dm(i7,m7)=r2;
dm(i7,m7)=0x1279e2;
r15=r15-1;
if ne jump (pc, 0xffffffe7);
与此同时,我想我已经找到了答案。通过尽可能少的操作来实现最少的循环次数。我必须获取数据指针,获取函数指针,然后以数据指针作为参数调用函数。当获取一个指针时,索引寄存器会自动被一个常量修改,也可以让这个常量等于 1。所以再次发现自己拥有一个函数指针数组和一个数据指针数组。
自然,限制是在组装中可以做的事情,现在已经探索过了。考虑到这一点,我现在明白,尽管引入基类是很自然的,但它并不是真正符合要求的。所以我想答案是,如果一个人想要一个函数指针数组,应该让自己成为一个函数指针数组......