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



我们有用类表示的分层消息。它们用于通过序列化/反序列化在线程和组件之间发送消息。在我们的用例中,我们使用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

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

例如,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通过一个名为"堆省略";。

#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

参见导螺杆

显然,您需要在自己的代码库中进行双重检查,但通常的副歌仍然是:"首先编写易于阅读和维护的代码,只有当编译器做得不好时,你才应该费力地跳过重重关卡来优化它;

您可以使用inplacer(请参阅本文(。您的代码将如下所示:

#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;
}

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

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

编辑:这里有类似的解决方案(但没有inplacer(——它不适用于聚合,但我敢打赌,在您的情况下,这不是问题。

最新更新