C++ 在c+中最快地实现简单、虚拟、观察者式的模式+;?

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 {

我正在努力实现一个vtables的替代方案,使用枚举和大量的宏魔法,这真的开始扰乱我的大脑。我开始认为我没有走上正确的道路,因为代码越来越难看,无论如何都不适合生产

如何以最少的重定向/操作实现以下代码的模式

必须在标准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];
        ...
}

...