首页 > 解决方案 > 在没有动态分配的情况下在构造时组合对象时避免数据的多个副本

问题描述

我们有由类表示的分层消息。它们用于通过序列化/反序列化在线程和组件之间发送消息。在我们的用例中,我们使用std::variant<InnerA, InnherB, ...>,但为了简化,我们的代码类似于:

class Inner {
  public:
    Inner(uint8_t* array, uint16_t arrayLength) {
        m_payloadLength = arrayLength; // Let's assume arrayLength is always < 256
        memcpy(m_payload.data(), array, arrayLength));
    }
    std::array<uint8_t, 256> m_payload;
    uint16_t m_payloadLength;
}

class Outer {
  public:
    Outer(const Inner& inner): m_inner(inner){};
    Inner m_inner;
}

class OuterOuter {
  public:
    OuterOuter(const Outer& outer): m_outer(outer){};
    Outer m_outer;
}

因此,要创建一个OuterOuter我们需要做的对象

int main(int argc, char** argv){
   uint8_t buffer[4]  = {1,2,3,4};
   Inner inner(buffer, 4);
   Outer outer(inner);
   OuterOuter outerOuter(outer);
   addToThreadQueue(outerOuter);
}

现在的问题是,我们使用嵌入式设备,因此我们不能将动态内存与 malloc 和 new 一起使用。截至目前,有效载荷内容将被复制三次吗?一次是创建inner,一次是在Outer中调用Inner的拷贝构造函数,一次是在OuterOuter中调用Outer的拷贝构造函数?如果是这样,有没有办法在不使用动态内存的情况下避免所有这些复制?如果有办法将我的意图传递给编译器,那么可能会进行优化,如果它还没有优化它的话。

理想情况下,我们会避免OuterOuter类采用所有子类构造参数,因为我们的树非常深并且我们使用 std::variant。在此示例中,它将是OuterOuter(uint8_t* array, uint16_t arrayLength)Outer(uint8_t* array, uint16_t arrayLength)然后Outer将构建Inner

标签: c++constructorembeddedc++17

解决方案


一般来说,现代编译器在优化类层次结构方面做得很好,除了填充连续的内存布局之外,它们的构造没有副作用。

例如,gcc 将您的示例编译为基本上单个类:

main:
  sub rsp, 280
  mov eax, 4
  mov rdi, rsp
  mov WORD PTR [rsp+256], ax
  mov DWORD PTR [rsp], 67305985
  call addToThreadQueue(OuterOuter const&)
  xor eax, eax
  add rsp, 280
  ret

见神螺栓

甚至除此之外,编译器还可以在某些情况下跳过一些副作用。例如,在下面的示例中,gcc 通过称为“heap elision”的过程完全摆脱了堆分配。

#include <memory>

extern int foo(int);
extern void bar(int);

struct MyStruct {
    int data;

    MyStruct() {
        auto val = std::make_unique<int>(12); 
        data = foo(*val);
    }
};

int main(int argc, char** argv){
   MyStruct x;
   bar(x.data);
}

变成:

main:
  sub rsp, 8
  mov edi, 12
  call foo(int)
  mov edi, eax
  call bar(int)
  xor eax, eax
  add rsp, 8
  ret

见神螺栓

显然,您需要仔细检查您自己的代码库,但通常的做法仍然存在:“首先编写易于阅读和维护的代码,并且只有当编译器对它的处理不好时,您才应该费心费力地跳过箍来优化它。 "


推荐阅读