我正在评估将一部分实时软件从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循环每次迭代编译为五个周期,每秒总成本为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%以上。而且,待办事项列表在运行时不会改变,它在启动时只设置了一次,因此真正浪费了查找要调用的函数的工作。问自己一个问题“最可能的最佳结果是什么”后,我看一下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。因此,人们再次找到了带有函数指针数组和数据指针数组的自己。
自然,限制是可以在组装中完成的工作,现在已经进行了探索。考虑到这一点,我现在理解,即使引入基类是很自然的,但这并不是真正合适的方案。所以我想答案是,如果想要一个函数指针数组,就应该使自己成为一组函数指针...
最佳答案
是什么让您认为vtable查找开销是20个周期?如果确实如此,则需要更好的C++编译器。
我在Intel机器上尝试了此操作,对所使用的处理器一无所知,并且正如预期的那样,C调度代码和C++ vtable调度之间的差异是一条指令,与vtable涉及额外的事实有关间接。
C代码(基于OP):
void (*todolist[200])(void *parameters);
void *paramlist[200];
void realtime(void)
{
int i;
for (i = 0; i < 200; i++)
(*todolist[i])(paramlist[i]);
}
C++代码:
class Base {
public:
Base(void* unsafe_pointer) : unsafe_pointer_(unsafe_pointer) {}
virtual void operator()() = 0;
protected:
void* unsafe_pointer_;
};
Base* todolist[200];
void realtime() {
for (int i = 0; i < 200; ++i)
(*todolist[i])();
}
两者都用gcc 4.8,-O3编译:
realtime: |_Z8realtimev:
.LFB0: |.LFB3:
.cfi_startproc | .cfi_startproc
pushq %rbx | pushq %rbx
.cfi_def_cfa_offset 16 | .cfi_def_cfa_offset 16
.cfi_offset 3, -16 | .cfi_offset 3, -16
xorl %ebx, %ebx | movl $todolist, %ebx
.p2align 4,,10 | .p2align 4,,10
.p2align 3 | .p2align 3
.L3: |.L3:
movq paramlist(%rbx), %rdi | movq (%rbx), %rdi
call *todolist(%rbx) | addq $8, %rbx
addq $8, %rbx | movq (%rdi), %rax
| call *(%rax)
cmpq $1600, %rbx | cmpq $todolist+1600, %rbx
jne .L3 | jne .L3
popq %rbx | popq %rbx
.cfi_def_cfa_offset 8 | .cfi_def_cfa_offset 8
ret | ret
在C++代码中,第一个
movq
获取vtable的地址,然后call
对其进行索引。这就是指令开销。根据OP的说法,DSP的C++编译器生成以下代码。我基于对正在发生的事情的理解(可能是错误的)插入了评论。请注意,(IMO)循环在OP指示之前开始一个位置;否则,这对我来说毫无意义。
# Initialization.
# i3=todolist; i5=paramlist | # i5=todolist holds paramlist
i3=0xb27ba; | # No paramlist in C++
i5=0xb28e6; | i5=0xb279a;
# r15=count
r15=0xc8; | r15=0xc8;
# Loop. We need to set up r4 (first parameter) and figure out the branch address.
# In C++ by convention, the first parameter is 'this'
# Note 1:
r4=dm(i5,m6); # r4 = *paramlist++; | i5=modify(i5,m6); # i4 = *todolist++
| i4=dm(m7,i5); # ..
# Note 2:
| r2=i4; # r2 = obj
| i4=dm(m6,i4); # vtable = *(obj + 1)
| r1=dm(0x3,i4); # r1 = vtable[3]
| r4=r2+r1; # param = obj + r1
i12=dm(i3,m6); # i12 = *todolist++; | i12=dm(0x5,i4); # i12 = vtable[5]
# Boilerplate call. Set frame pointer, push return address and old frame pointer.
# The two (push) instructions after jump are actually executed before the jump.
r2=i6; | r2=i6;
i6=i7; | i6=i7;
jump (m13,i12) (db); | jump (m13,i12) (db);
dm(i7,m7)=r2; | dm(i7,m7)=r2;
dm(i7,m7)=0x1279de; | dm(i7,m7)=0x1279e2;
# if (count--) loop
r15=r15-1; | r15=r15-1;
if ne jump (pc, 0xfffffff2); | if ne jump (pc, 0xffffffe7);
注意:
i
寄存器中,而不是在r4
中。无疑这与下面的问题有关。 i4
作为临时指令。vtable查找本身占用一条指令。 因此:问题不是vtable查找,它可以在一条额外的指令中完成(但实际上需要两条指令)。问题在于,编译器感到需要“查找”对象。但是,为什么gcc/i86不需要这样做?
答案是:它曾经是,但现在不再。在许多情况下(例如,没有多重继承),将派生类的指针转换为基类的指针不需要修改指针。因此,当我们调用派生类的方法时,我们可以只给它基类指针作为
this
参数。但是在其他情况下,这是行不通的,我们必须在执行转换时调整指针,然后在进行调用时将指针调整回去。有(至少)两种方法可以执行第二次调整。一种方法是生成的DSP代码显示的方法,其中将调整存储在vtable中(即使它为0),然后在调用期间应用。另一种方法(称为
vtable-thunks
)是创建thunk
(一点点可执行代码),该命令会调整this
指针,然后跳转到方法的入口点,并将指向此thunk的指针放入vtable中。 (这可以全部在编译时完成。)thunk解决方案的优点是,在不需要进行任何调整的常见情况下,我们可以优化thunk,并且没有调整代码。 (缺点是,如果需要调整,我们会产生一个额外的分支。)据我了解,VisualDSP++是基于gcc的,它可能具有
-fvtable-thunks
和-fno-vtable-thunks
选项。因此,您也许可以使用-fvtable-thunks
进行编译。但是,如果这样做,则将需要编译使用该选项的所有C++库,因为您不能混合使用两种调用样式。此外,(15年前)gcc的vtable-thunks实现中存在各种错误,因此,如果VisualDSP++使用的gcc版本足够旧,您也可能会遇到这些问题(IIRC,它们都涉及多重继承,因此它们可能不适用于您的用例。)(原始测试,更新前):
我尝试了以下简单情况(没有多重继承,这会使速度变慢):
class Base {
public:
Base(int val) : val_(val) {}
virtual int binary(int a, int b) = 0;
virtual int unary(int a) = 0;
virtual int nullary() = 0;
protected:
int val_;
};
int binary(Base* begin, Base* end, int a, int b) {
int accum = 0;
for (; begin != end; ++begin) { accum += begin->binary(a, b); }
return accum;
}
int unary(Base* begin, Base* end, int a) {
int accum = 0;
for (; begin != end; ++begin) { accum += begin->unary(a); }
return accum;
}
int nullary(Base* begin, Base* end) {
int accum = 0;
for (; begin != end; ++begin) { accum += begin->nullary(); }
return accum;
}
并使用-O3在gcc(4.8)中进行编译。如我所料,它产生的代码与您的C发行代码完全一样。例如,这是
for
函数的unary
循环:.L9:
movq (%rbx), %rax
movq %rbx, %rdi
addq $16, %rbx
movl %r13d, %esi
call *8(%rax)
addl %eax, %ebp
cmpq %rbx, %r12
jne .L9
关于c++ - C++中vtable查找的性能影响,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/18404988/