保证副本省略的替代方案

时间:2018-02-02 13:30:44

标签: c++ icc c++17

C ++ 17引入了guaranteed copy elision,但目前尚未为some compilers实现该功能(特别是英特尔C ++ 17和18)。

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

我的程序看起来像这样:

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,gcc 7.2和-std = c ++ 17接受代码。但icc 17,即使使用-std = c ++ 17也不会忽视复杂情况。

有没有办法确保在英特尔C ++上进行复制省略优化?据我所知,优化大多数时间都在进行,但是一些复杂的情况在没有明显原因的情况下失败。副本缺失失败的原因是什么?

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

附带问题:在C ++ 14时代,在强制要求之前复制省略优化有多好?我似乎记得clang同样没有优化它可能拥有的一切。

1 个答案:

答案 0 :(得分:1)

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();
}