在没有动态分配的情况下在构造对象时避免数据的多个副本

Avoid multiple copy of data when composing objects at construction without dynamic allocation

我们有用 classes 表示的分层消息。 serializing/deserializing 用于在线程和组件之间发送消息。在我们的用例中,我们使用 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 class 接受所有子classes 构造参数,因为我们的树很深并且我们使用 std::variant。在此示例中,它将是 OuterOuter(uint8_t* array, uint16_t arrayLength)Outer(uint8_t* array, uint16_t arrayLength),然后 Outer 将构建 Inner.

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

例如,gcc 将您的样本编译成一个 class:

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

see on godbolt

除此之外,在某些情况下允许编译器跳过一些副作用。例如,在下面的示例中,gcc 通过称为“堆省略”的过程完全摆脱了堆分配。

#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

see on godbolt

显然,你需要仔细检查你自己的代码库,但通常的克制仍然是:“首先编写易于阅读和维护的代码,只有当编译器做得不好时,你才应该费心跳过箍优化它。"

您可以使用 inplacer(参见 ). Your code will look like this:

#include <type_traits>
#include <array>
#include <cstdint>
#include <cstring>


using namespace std;


template<class F>
struct inplacer
{
    F f_;
    operator std::invoke_result_t<F&>() { return f_(); }
};

template<class F> inplacer(F) -> inplacer<F>;


struct Inner
{
    Inner(uint8_t* data, size_t len)
        : len_(len) // Let's assume arrayLength is always < 256
    {
        memcpy(payload_.data(), data, len*sizeof(*data));
    }

    std::array<uint8_t, 256>    payload_;
    size_t                      len_;
};

struct Outer
{
    template<class T>
    Outer(T&& inner): m_inner(std::forward<T>(inner)) {}

    Inner m_inner;
};

struct OuterOuter
{
    template<class T>
    OuterOuter(T&& outer): m_outer(std::forward<T>(outer)) {}

    Outer m_outer;
};


void addToThreadQueue(OuterOuter const&);

int main()
{
    uint8_t buffer[4]  = {1,2,3,4};
    OuterOuter outerOuter{ inplacer{[&]{ return Inner{buffer, size(buffer)}; }} };
    addToThreadQueue(outerOuter);
    return 0;
}

这种方法将使您减少对编译器优化的依赖。如果您的 ctors 有副作用(或者编译器无法在此翻译单元中分析),它也会起作用。

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

Edit: here is 类似的解决方案(但没有 inplacer)——它不适用于聚合,但我敢打赌你的情况没问题。