保证复制省略的替代方案

Alternative to guaranteed copy elision

本文关键字:方案 复制省      更新时间:2023-10-16

>C++17 引入了有保证的副本 elision,但该功能目前尚未在某些编译器(特别是 Intel C++ 17 和 18)中实现。

我目前有一个使用 Intel C++ 17 编译的 SIMD 程序,其中没有优化复制/移动省略,导致分配的 SIMD 序列化首先是无用的(进一步导致不可忽视的性能损失)。

我的程序如下所示:

class X { ... };
struct S {
// default ctor, default copy/move ctor/assignement
// and recursivly so for all data members
// no virtual member functions
...
constexpr
S(const X& x) : ... {}
constexpr auto
update(const X& x) {
...
}
};
constexpr auto
make_S(const X& x) { // note : except for being templated, make_S is not simplified from my real implementation. 
// I only want to use it for overloading, allowing the caller to not specify the actual type of S
return S(x); // Yes, that simple :'(
}
constexpr auto
init_S(S& s, const X& x) { 
s.update(x);
}
int main() {
X x;
S s(x); // copy elision success
// vs
auto s = make_S(x); // icc does not elide the move
// vs
S s;
init_S(s,x); // no copy
return 0;
}

据我了解,C++17 需要复制省略,事实上,对于删除的移动/复制 ctors,带有 -std=c++17 的 gcc 7.2 接受代码。但是 icc 17,即使使用 -std=c++17 也不能避免在复杂情况下采取行动。

有没有办法确保英特尔C++上的复制消除优化?据我所知,优化大部分时间都是完成的,但一些复杂的情况在没有明显原因的情况下失败。复制失败的原因可能是什么?

注意:所有函数都是inline的,并由编译器内联。

附带问题:在 C++14 时代,在成为强制性之前,复制消除优化有多好?我似乎记得,叮当同样没有优化它可能拥有的一切。

optional的方式

如果您C++17确保不进行复制并在函数内创建对象的最佳选项是std::optional(或boost:optional)。多亏@Barry,你让我完美的小班过时了:)

struct X
{
X(int, int);
X(const X&) = delete;
X(X&&) = delete;
};
auto foo_no_copy(std::optional<X>& xo)
{
X& x = xo.emplace(11, 24);
// use x
}
auto test()
{
std::optional<X> xo;
foo_no_copy(xo);
// use *xo
}

只是一个参考

有时最简单的解决方案是正确的解决方案。

根据您的方案,其他解决方案可能矫枉过正。如果你可以在函数外部构造你的对象而没有任何性能损失,那么你所要做的就是传递一个引用:

auto foo(X& x) -> void;
auto test()
{
X x{};
foo(x);
// use x
}

自定义Copy_elision包装器

以下是我的原始答案,以防您无法访问optional类:

如果你真的必须确保复制选择机制,你可以在高级别上模拟编译器在低级别上会做的事情:在调用者上分配内存,并将指向它的指针传递给被调用的函数。然后,被调用的函数将使用该内存来构造一个放置 new 的对象。

我为此创建了一个类型安全的 RAII 包装器类:

template <class X>
struct Copy_elision
{
std::aligned_storage_t<sizeof(X)> storage_{};
X* x_ = nullptr;
public:
Copy_elision() = default;
Copy_elision(const Copy_elision&) = delete;
Copy_elision(Copy_elision&) = delete;
auto operator=(const Copy_elision&) -> Copy_elision& = delete;
auto operator=(Copy_elision&&) -> Copy_elision& = delete;
~Copy_elision()
{
if (x_)
x_->~X();
}
template <class... Args>
auto construct(Args&&... args) -> X&
{
x_ = new (&storage_) X{std::forward<Args>(args)...};
return *x_;
}
auto get() const -> X* { return x_; }
};

以下是使用它的方法:

auto foo_no_copy(Copy_elision<X>& xce) -> X&
{
X& x = xce.construct(11, 24);
// work with x
return x;
}
auto test()
{
Copy_elision<X> x_storage;
X& x = foo_no_copy(x_storage);
// work with x
}

手动方式(未重新开始)

如果你很好奇,这里是没有包装类的Copy_elision版本。我强烈建议不要使用此版本,因为它非常脆弱:

  • 类型 安全损失
  • no RAII:异常将导致不调用析构函数。
#include <memory>
#include <type_traits>
struct X
{
X(int, int);
X(const X&) = delete; // can't copy
X(X&&) = delete;      // can't move
};
auto foo_no_copy(void* xp/* , other params*/) -> X&
{
X& x = *(new (xp) X{12, 24});
// work with x
return x;
}
auto test()
{
std::aligned_storage_t<sizeof(X)> x_storage;
X& x = foo_no_copy(&x_storage);
// work with x
x.~X();
}