在尝试“快速”pimpl 时,memcpy 没有优化
memcpy not optimised out during attempt at ‘fast’ pimpl
我需要使用一个非常大和复杂的 header-only class(想想 boost::multiprecision::cpp_bin_float<76>,下面称为 BHP
),我想隐藏在后面pimpl-like 实现,纯粹是为了减少一个较大项目的编译时间(将 Boost class 替换为 std::complex<double>
减少了大约 50% 的编译时间)。
但是,我想避免动态内存分配。因此,这样的事情看起来很自然(暂时忽略可以使用 aligned_storage
或 alignas
避免的对齐问题):
struct Hidden {
char data[sz];
Hidden& punned(Hidden const& other);
};
然后可以在单个翻译单元中定义 Hidden::punned
以将 data
转换为 BHP*
,对其进行操作并且不会污染具有 170k LOC of [=53] 的所有其他翻译单元=] 文件。一个可能的实现可能是
Hidden& Hidden::punned(Hidden const& other) {
*(BHP*)(data) += *(BHP*)(other.data);
return *this;
}
这当然是未定义的行为,因为我们通过 char
类型的指针访问 BHP
类型的 object,因此违反了严格的别名规则。正确的做法是:
Hidden& Hidden::proper(Hidden const& other) {
BHP tmp; std::memcpy(&tmp, data, sz);
BHP tmp2; std::memcpy(&tmp2, other.data, sz);
tmp += tmp2;
std::memcpy(data, &tmp, sz);
return *this;
}
现在这些 memcpy
调用可能会被优化掉,这看起来“很明显”。不幸的是,情况并非如此,它们仍然存在并使 proper()
比 punned()
大得多。
我想知道正确的方法是 a) 将数据直接存储在 Hidden
object 和 b) 避免不必要的副本到 re-interpret 它和 c) 避免违反严格对齐规则和 d) 不携带指向存储区的额外指针。
有一个godbolt link here;请注意,我测试的所有编译器(GCC 4.9 - trunk、Clang 3.9、4.0 和 5.0 以及 Intel 18)都没有“优化”memcpy。某些版本的 GCC(例如 5.3)也直接抱怨违反了严格的别名规则,尽管并非所有版本都这样做。我还插入了一个 Direct
class,它知道 BHP
,因此可以直接调用它,但我想避免这种情况。
最小工作示例:
#include <cstring>
constexpr std::size_t sz = 64;
struct Base {
char foo[sz];
Base& operator+=(Base const& other) { foo[0] += other.foo[0]; return *this; }
};
typedef Base BHP;
// or:
//#include <boost/multiprecision/cpp_bin_float.hpp>
//typedef boost::multiprecision::number<boost::multiprecision::cpp_bin_float<76> > BHP;
struct Hidden {
char data[sz];
Hidden& proper(Hidden const& other);
Hidden& punned(Hidden const& other);
};
Hidden& Hidden::proper(Hidden const& other) {
BHP tmp; std::memcpy(&tmp, data, sz);
BHP tmp2; std::memcpy(&tmp2, other.data, sz);
tmp += tmp2;
std::memcpy(data, &tmp, sz);
return *this;
}
Hidden& Hidden::punned(Hidden const& other) {
*(BHP*)(data) += *(BHP*)(other.data);
return *this;
}
struct Direct {
BHP member;
Direct& direct(Direct const& other);
};
Direct& Direct::direct(Direct const& other) {
member += other.member;
return *this;
}
struct Pointer {
char storage[sz];
BHP* data;
Pointer& also_ok(Pointer const& other);
};
Pointer& Pointer::also_ok(Pointer const& other) {
*data += *other.data;
return *this;
}
This, of course, is undefined behaviour, because we access an object of type BHP
through a pointer of type char.
事实并非如此。通过char*
is fine访问,前提是那里确实有一个BHP
对象。也就是说,只要双方都有:
new (data) BHP(...);
那么这就完全可以了:
*(BHP*)(data) += *(BHP*)(other.data);
只需确保您的字符数组也是 alignas(BHP)
。
请注意,gcc 有时不喜欢您重新解释 char[]
,因此您可以选择使用 std::aligned_storage_t
.
之类的东西
我需要使用一个非常大和复杂的 header-only class(想想 boost::multiprecision::cpp_bin_float<76>,下面称为 BHP
),我想隐藏在后面pimpl-like 实现,纯粹是为了减少一个较大项目的编译时间(将 Boost class 替换为 std::complex<double>
减少了大约 50% 的编译时间)。
但是,我想避免动态内存分配。因此,这样的事情看起来很自然(暂时忽略可以使用 aligned_storage
或 alignas
避免的对齐问题):
struct Hidden {
char data[sz];
Hidden& punned(Hidden const& other);
};
然后可以在单个翻译单元中定义 Hidden::punned
以将 data
转换为 BHP*
,对其进行操作并且不会污染具有 170k LOC of [=53] 的所有其他翻译单元=] 文件。一个可能的实现可能是
Hidden& Hidden::punned(Hidden const& other) {
*(BHP*)(data) += *(BHP*)(other.data);
return *this;
}
这当然是未定义的行为,因为我们通过 char
类型的指针访问 BHP
类型的 object,因此违反了严格的别名规则。正确的做法是:
Hidden& Hidden::proper(Hidden const& other) {
BHP tmp; std::memcpy(&tmp, data, sz);
BHP tmp2; std::memcpy(&tmp2, other.data, sz);
tmp += tmp2;
std::memcpy(data, &tmp, sz);
return *this;
}
现在这些 memcpy
调用可能会被优化掉,这看起来“很明显”。不幸的是,情况并非如此,它们仍然存在并使 proper()
比 punned()
大得多。
我想知道正确的方法是 a) 将数据直接存储在 Hidden
object 和 b) 避免不必要的副本到 re-interpret 它和 c) 避免违反严格对齐规则和 d) 不携带指向存储区的额外指针。
有一个godbolt link here;请注意,我测试的所有编译器(GCC 4.9 - trunk、Clang 3.9、4.0 和 5.0 以及 Intel 18)都没有“优化”memcpy。某些版本的 GCC(例如 5.3)也直接抱怨违反了严格的别名规则,尽管并非所有版本都这样做。我还插入了一个 Direct
class,它知道 BHP
,因此可以直接调用它,但我想避免这种情况。
最小工作示例:
#include <cstring>
constexpr std::size_t sz = 64;
struct Base {
char foo[sz];
Base& operator+=(Base const& other) { foo[0] += other.foo[0]; return *this; }
};
typedef Base BHP;
// or:
//#include <boost/multiprecision/cpp_bin_float.hpp>
//typedef boost::multiprecision::number<boost::multiprecision::cpp_bin_float<76> > BHP;
struct Hidden {
char data[sz];
Hidden& proper(Hidden const& other);
Hidden& punned(Hidden const& other);
};
Hidden& Hidden::proper(Hidden const& other) {
BHP tmp; std::memcpy(&tmp, data, sz);
BHP tmp2; std::memcpy(&tmp2, other.data, sz);
tmp += tmp2;
std::memcpy(data, &tmp, sz);
return *this;
}
Hidden& Hidden::punned(Hidden const& other) {
*(BHP*)(data) += *(BHP*)(other.data);
return *this;
}
struct Direct {
BHP member;
Direct& direct(Direct const& other);
};
Direct& Direct::direct(Direct const& other) {
member += other.member;
return *this;
}
struct Pointer {
char storage[sz];
BHP* data;
Pointer& also_ok(Pointer const& other);
};
Pointer& Pointer::also_ok(Pointer const& other) {
*data += *other.data;
return *this;
}
This, of course, is undefined behaviour, because we access an object of type
BHP
through a pointer of type char.
事实并非如此。通过char*
is fine访问,前提是那里确实有一个BHP
对象。也就是说,只要双方都有:
new (data) BHP(...);
那么这就完全可以了:
*(BHP*)(data) += *(BHP*)(other.data);
只需确保您的字符数组也是 alignas(BHP)
。
请注意,gcc 有时不喜欢您重新解释 char[]
,因此您可以选择使用 std::aligned_storage_t
.