C++ 在c+中最快地实现简单、虚拟、观察者式的模式+;?
我正在努力实现一个vtables的替代方案,使用枚举和大量的宏魔法,这真的开始扰乱我的大脑。我开始认为我没有走上正确的道路,因为代码越来越难看,无论如何都不适合生产 如何以最少的重定向/操作实现以下代码的模式C++ 在c+中最快地实现简单、虚拟、观察者式的模式+;?,c++,enums,virtual-functions,dispatch,micro-optimization,sse,x86,C++,Enums,Virtual Functions,Dispatch,Micro Optimization,Sse,X86,我正在努力实现一个vtables的替代方案,使用枚举和大量的宏魔法,这真的开始扰乱我的大脑。我开始认为我没有走上正确的道路,因为代码越来越难看,无论如何都不适合生产 如何以最少的重定向/操作实现以下代码的模式 必须在标准C++中进行,最多可达17。 class A{ virtual void Update() = 0; // A is so pure *¬* }; class B: public A { override void Update() final {
必须在标准C++中进行,最多可达17。
class A{
virtual void Update() = 0; // A is so pure *¬*
};
class B: public A
{
override void Update() final
{
// DO B STUFF
}
}
class C: public A
{
override void Update() final
{
// DO C STUFF
}
}
// class...
int main()
{
std::vector<A*> vecA{};
// Insert instances of B, C, ..., into vecA
for(auto a: vecA) // This for will be inside a main loop
a->Update(); // Ridiculous amount of calls per unit of time
// Free memory
}
A类{
虚空更新()=0;//A是如此纯净**
};
B类:公共A
{
覆盖void Update()final
{
//做B类事情
}
}
C类:公共A类
{
覆盖void Update()final
{
//做C类的事情
}
}
//类。。。
int main()
{
std::向量向量向量{};
//将B、C、…、的实例插入到vecA中
for(auto a:vecA)//此for将位于主循环内
a->Update();//每单位时间调用的数量
//空闲内存
}
PS:如果enum、switch和macros确实是最好的选择,我想我会尝试刷新缓存,并提出更好的设计
我知道这是微观优化。。。见鬼,我需要nano甚至pico对此进行优化(打个比方),因此我将忽略可能出现的任何实用性响应。正如第一条评论所说,您这里有一个XY问题。这样就可以了,您有很多对象,而不是大量的不同类,并且不需要支持代码在编译时不知道的类型多态+虚拟继承是错误的选择 相反,使用N个不同的容器,每种类型的对象对应一个容器,没有间接寻址让编译器内联
B::Update()
到所有B
对象的循环中更好。(对于下面增加一个成员int
,我通过查看asm进行的静态性能分析表明,在L1D缓存中数据热的Skylake上,AVX2自动矢量化与循环中的调用相比,速度快了约24倍。这真是太大了。)
如果对象之间(包括不同类型的对象之间)存在某种必需的顺序,那么某种多态性或手动调度将是合适的。(例如,如果您处理vecA
的顺序很重要,那么将所有B
对象与所有C
对象分开是不等价的。)
如果您关心性能,那么您必须意识到,将源代码变大可能会简化asm输出中编译器的工作根据内部循环中每个对象的类型进行检查/分派是昂贵的。使用任何类型的函数指针或枚举在每个对象的基础上进行分派,在混合使用不同对象时,很容易出现分支预测失误
在多个容器上分别循环可以有效地将类型check从内部循环中提升出来,并让编译器实现设备化。(或者更好的做法是,将每个对象收缩为首先不需要vtable指针、枚举或函数指针,因为其类型是静态已知的。)
为每个不同类型的容器编写一个单独的循环,有点像在将类型从内部循环中提升出来之后,在不同类型上完全展开一个循环。这是编译器内联调用所必需的,如果每种类型有很多对象,则需要内联调用。内联使它能够跨对象在寄存器中保留常量,支持跨多个对象的SIMD自动矢量化,并简单地避免实际函数调用的开销。(调用本身和寄存器溢出/重新加载。)
<>你是正确的,<强>如果你需要每个对象调度< /St>,当你使用<代码>最终< /COD>重写时,C++虚拟函数是一个昂贵的获取方法。您所付出的运行时成本与您的代码支持在编译时不知道的任意大小的新派生类的运行时成本相同,但没有从中获得任何好处
虚拟分派仅适用于间接级别(例如,您正在使用的指针向量),这意味着您需要以某种方式管理指向的对象,例如,通过从向量池B
和向量池C
分配它们。虽然我不确定大多数vector
的实现在需要增长时使用realloc()
;new/delete
API没有realloc
,因此vector
实际上可能会在每次增长时进行复制,而不是尝试扩展现有的分配。看看你的C++实现了什么,因为它可能比你能用MalC/ReLoC./P>做的更糟。
顺便说一句,只要您的所有类都是可破坏的,就应该可以使用RAII执行新建
/删除
,而无需额外的分配/解除分配开销。(但请注意,对于使用指针向量)。警告说,通过指向基类的指针来销毁它是不可能的,因此您可能必须自己滚动。不过,在x86-64上的gcc上,sizeof(unique_ptr)
只有8个,因此它只有一个指针成员。但是不管怎样,单独分配无数的小对象很糟糕,所以首先不要这样做
如果你真的需要像标题要求的那样发送邮件
如果对象的大小都相似,那么您确实希望在对象上循环,而不是指向对象的指针。这将避免指针向量的额外缓存占用,并避免无序执行必须隐藏以保持执行单元繁忙的额外指针跟踪延迟<但是,C++虚拟继承不提供任何符合标准的方式来获得代码< >联合uPule{bb;c的多态性。
class A{
public:
virtual void Update() = 0; // A is so pure *¬*
};
struct C : public A {
int m_c = 0;
public:
void Update() override final
{ m_c++; }
};
int SC = sizeof(C); // 16 bytes because of the vtable pointer
C global_c; // to instantiate a definition for C::Update();
// not inheriting at all gives equivalent asm to making Update non-virtual
struct nonvirt_B //: public A
{
int m_b = 0;
void Update() //override final
{ m_b++; }
};
int SB = sizeof(nonvirt_B); // only 4 bytes per object with no vtable pointer
void separate_containers(std::vector<nonvirt_B> &vecB, std::vector<C> &vecC)
{
for(auto &b: vecB) b.Update();
for(auto &c: vecC) c.Update();
}
std::vector<A*> vecA{};
void vec_virtual_pointers() {
for(auto a: vecA)
a->Update();
}
# rbx = &vecA[0]
.LBB2_1: # do{
mov rdi, qword ptr [rbx] # load a pointer from the vector (will be the this pointer for Update())
mov rax, qword ptr [rdi] # load the vtable pointer
call qword ptr [rax] # memory-indirect call using the first entry in the vtable
add rbx, 8 # pointers are 8 bytes
cmp r14, rbx
jne .LBB2_1 # }while(p != vecA.end())
C::Update(): # @C::Update()
inc dword ptr [rdi + 8]
ret
union upoly {
upoly() {} // needs an explicit constructor for compilers not to choke
B b;
C c;
} poly_array[1024];
void union_polymorph() {
upoly *p = &poly_array[0];
upoly *endp = &poly_array[1024];
for ( ; p != endp ; p++) {
A *base = reinterpret_cast<A*>(p);
base->Update(); // virtual dispatch
}
}
lea rdi, [rbx + poly_array] ; this pointer
mov rax, qword ptr [rbx + poly_array] ; load it too, first "member" is the vtable pointer
call qword ptr [rax]
add rbx, 16 ; stride is 16 bytes per object
cmp rbx, 16384 ; 16 * 1024
jne .LBB4_1
#include <functional>
// pretty crappy: checks for being possibly unset to see if it should throw().
std::vector<std::function<void()>> vecF{};
void vec_functional() {
for(auto f: vecF) f();
}
# do {
.LBB6_2: # =>This Inner Loop Header: Depth=1
mov qword ptr [rsp + 16], 0 # store a 0 to a local on the stack?
mov rax, qword ptr [rbx + 16]
test rax, rax
je .LBB6_5 # throw on pointer==0 (nullptr)
mov edx, 2 # third arg: 2
mov rdi, r14 # first arg: pointer to local stack memory (r14 = rsp outside the loop)
mov rsi, rbx # second arg: point to current object in the vector
call rax # otherwise call into it with 2 args
mov rax, qword ptr [rbx + 24] # another pointer from the std::function<>
mov qword ptr [rsp + 24], rax # store it to a local
mov rcx, qword ptr [rbx + 16] # load the first pointer again
mov qword ptr [rsp + 16], rcx
test rcx, rcx
je .LBB6_5 # check the first pointer for null again (and throw if null)
mov rdi, r14
call rax # call through the 2nd pointer
mov rax, qword ptr [rsp + 16]
test rax, rax
je .LBB6_12 # optionally skip a final call
mov edx, 3
mov rdi, r14
mov rsi, r14
call rax
.LBB6_12: # in Loop: Header=BB6_2 Depth=1
add rbx, 32
cmp r15, rbx
jne .LBB6_2
.LBB6_13: # return
add rsp, 32
pop rbx
pop r14
pop r15
ret
.LBB6_5:
call std::__throw_bad_function_call()
jmp .LBB6_16
mov rdi, rax
call __clang_call_terminate
void virtual_call_unswitch(std::vector<A*>& vec) {
// first create a bitmap which specifies whether each element is B or C type
std::vector<uint64_t> bitmap(vec.size() / 64);
for (size_t block = 0; block < bitmap.size(); block++) {
uint64_t blockmap = 0;
for (size_t idx = block * 64; idx < block * 64 + 64; idx++) {
blockmap >>= 1;
blockmap |= (uint64_t)vec[idx + 0]->typecode_ << 63;
}
bitmap[block] = blockmap;
}
// now loop over the bitmap handling all the B elements, and then again for all the C elements
size_t blockidx;
// B loop
blockidx = 0;
for (uint64_t block : bitmap) {
block = ~block;
while (block) {
size_t idx = blockidx + __builtin_ctzl(block);
B* obj = static_cast<B*>(vec[idx]);
obj->Update();
block &= (block - 1);
}
blockidx += 64;
}
// C loop
blockidx = 0;
for (uint64_t block : bitmap) {
while (block) {
size_t idx = blockidx + __builtin_ctzl(block);
C* obj = static_cast<C*>(vec[idx]);
obj->Update();
block &= (block - 1);
}
blockidx += 64;
}
}
-----------------------------------------------------------------------------
Benchmark Time CPU Iterations
-----------------------------------------------------------------------------
BenchWithFixture/VirtualDispatchTrue 30392 ns 30364 ns 23033 128.646M items/s
BenchWithFixture/VirtualDispatchFakeB 3564 ns 3560 ns 196712 1097.34M items/s
BenchWithFixture/StaticBPtr 3496 ns 3495 ns 200506 1117.6M items/s
BenchWithFixture/UnswitchTypes 8573 ns 8571 ns 80437 455.744M items/s
BenchWithFixture/StaticB 1981 ns 1981 ns 352397 1.9259G items/s
for (A *a : vecA) {
a->Update();
}
for (A *a : vecA) {
((B *)a)->Update();
}
// type 1 (code 11)
for (size_t i = 0; i < bitmap1.size(); i++) {
block = bitmap1[i] & bitmap2[i];
...
}
// type 2 (code 01)
for (size_t i = 0; i < bitmap1.size(); i++) {
block = ~bitmap1[i] & bitmap2[i];
...
}
...