在没有动态分配的情况下在构造对象时避免数据的多个副本
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
除此之外,在某些情况下允许编译器跳过一些副作用。例如,在下面的示例中,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(参见 ). 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
)——它不适用于聚合,但我敢打赌你的情况没问题。
我们有用 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
除此之外,在某些情况下允许编译器跳过一些副作用。例如,在下面的示例中,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;
}
这种方法将使您减少对编译器优化的依赖。如果您的 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
)——它不适用于聚合,但我敢打赌你的情况没问题。