【问题标题】:memcpy not optimised out during attempt at ‘fast’ pimplmemcpy 在尝试“快速”pimpl 时未优化
【发布时间】:2017-12-14 14:34:01
【问题描述】:

我需要使用一个非常大且复杂的纯标题类(想想 boost::multiprecision::cpp_bin_float,下面称为 BHP),我想将它隐藏在类似 pimpl 的实现后面,纯粹是为了减少大型项目中的编译时间(将 Boost 类替换为 std::complex<double> 可减少大约 50% 的编译时间)。

但是,我想避免动态内存分配。因此,这样的事情看起来很自然(暂时忽略对齐问题,使用aligned_storagealignas 可以避免):

struct Hidden {
  char data[sz];

  Hidden& punned(Hidden const& other);
};

Hidden::punned 然后可以在单个翻译单元中定义以将data 转换为BHP*,对其进行操作,并且不会用 170k LOC 的头文件污染所有其他翻译单元。一个可能的实现可能是

Hidden& Hidden::punned(Hidden const& other) {
  *(BHP*)(data) += *(BHP*)(other.data);
  return *this;
}

这当然是未定义的行为,因为我们通过char 类型的指针访问BHP 类型的对象,因此违反了严格的别名规则。正确的做法是:

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 对象中和 b) 避免不必要的副本以重新解释它和 c) 避免违反严格对齐规则和 d) 的正确方法是什么不要携带指向存储区域的额外指针。

有一个godbolt link here;请注意,我测试的所有编译器(GCC 4.9 - trunk、Clang 3.9、4.0 和 5.0 以及 Intel 18)都没有“优化”memcpy。某些版本的 GCC(例如 5.3)也直接抱怨违反了严格的别名规则,尽管并非所有版本都这样做。我还插入了一个知道BHPDirect 类,因此可以直接调用它,但我想避免这种情况。

最小的工作示例:

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

【问题讨论】:

  • “这当然是未定义的行为,因为我们通过 char 类型的指针访问 BHP 类型的对象,因此违反了严格的别名规则。” 不,转换指向char* 的其他一些指针类型并通过后者重新解释免于别名。 必须才能实现memcpy()(无需编译器技巧)和使用它的“位转换”过程。然而,反之则不然。
  • @underscore_d 我不明白你所说的“通过后者重新解释”是什么意思?
  • @Joe 取消对演员 char* 的引用并使用它来读取对象的对象表示,即将对象重新解释为字节。这是允许的。将只是一组字节的东西转换为其他类型的东西,它不是被构造成的,不是。换句话说,只有在确实存在该类型的对象(或兼容的对象)时,才能取消引用从更改类型的强制转换中获得的指针;我们不能只创建一个chars 数组,用数据填充它并告诉编译器'这里:有一个SomeClass'。但是我们可以'给我看看构成这个SomeClasschar。'
  • @underscore_d 并且不允许通过字节表示转换为不是原始类型的其他类型,对吗?
  • 是否可以为您的项目使用显式模板初始化

标签: c++ memcpy strict-aliasing


【解决方案1】:

这当然是未定义的行为,因为我们通过 char 类型的指针访问 BHP 类型的对象。

实际上并非如此。通过char* is fine 访问假设那里实际上有一个BHP 对象。也就是说,只要双方都有:

new (data) BHP(...);

那么这完全没问题:

*(BHP*)(data) += *(BHP*)(other.data);

只需确保您的 char 数组也是 alignas(BHP)

请注意,gcc 有时不喜欢您重新解释 char[],因此您可以选择使用类似 std::aligned_storage_t 的内容。

【讨论】:

  • 好的,谢谢!我对 GCC 警告感到困惑(它在 GCC 5.4 的 Godbold 上触发,我也得到了 7.2 在某一时刻发出它,但我不确定何时或如何)。但是,是的,我总是在那里保留一个BHP,按照你说的构建。
猜你喜欢
  • 2019-04-28
  • 2012-06-25
  • 2012-03-13
  • 1970-01-01
  • 2013-04-04
  • 2011-11-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多