【问题标题】:Hand written move手写动作
【发布时间】:2021-04-07 00:30:32
【问题描述】:

我写了一个向量类来学习移动语义。 我使用移动构造函数来移动 T(注释行)。

我的问题是为什么不直接复制临时对象的所有字节并将临时对象的所有字节设置为零,就像在 C 中一样?

我知道会为 temp 对象调用析构函数,它可能需要一些已初始化的成员才能正确析构。这就是为什么我不能更改对象的内部表示的原因。

但如果我 100% 确定我的 ~T() 没有这样的要求,这是一个很好的优化吗?

 20     void push_back(T&& val)
 21     {
 22         check_cap();
 23         //new (m_data + m_size) T(std::move(val));
 24         for(int i = 0; i < sizeof(T); ++i)
 25         {
 26             reinterpret_cast<char*> (m_data + m_size)[i] = reinterpret_cast<char*> (&val)[i];
 27             reinterpret_cast<char*> (&val)[i] = 0;
 28         }
 29         m_size++;
 30     }

(如果与实际问题无关,请不要透露有关演员阵容及其安全性的任何信息)

(我知道这不是一个好方法,最好不要在实际项目中使用它。但我只感兴趣从效率的角度来看它有多好。)

【问题讨论】:

  • 您会在 stackoverflow.com 上找到很多问题,人们想知道为什么在他们 memcpy 他们的 C++ 对象之后,从这里到那里,一切都乱套了。这就是最终在这里发生的事情。这只会以眼泪收场。您不能期望简单地逐字节复制 C++ 对象,然后每个人都会过上幸福的生活。 C++ 不能以这种方式工作。
  • 您只能对 trivially-copyable 类型的内存进行内存复制。一旦一个类型有一个用户提供的移动构造函数,它就不再是简单的可复制的了。
  • 您是否尝试测量这两个版本,看看有什么区别?
  • 当然。这是一个简单的例子。一个典型的std::string 是一个指针和一个字符串的大小。您只是逐字节复制它。伟大的!现在你有两个std::strings 指向同一个内部缓冲区,它们的两个析构函数(最终)都会尝试delete[] 相同的精确指针。希拉里随之而来。现在您的代码中完全不相关的部分出现了神秘的崩溃和内存损坏,迫使您在 Stackoverflow 上写另一篇文章,想知道这是怎么回事?那么,最终证明这比正确的移动语义更“有效”吗?
  • 这个问题意味着对 C++ 中的运动缺乏了解。

标签: c++ c++11 move-semantics memcpy


【解决方案1】:

为什么不直接复制临时对象的所有字节并将临时对象的所有字节设置为零,就像在 C 中一样?

这是一个很好的优化吗?

我认为不会。这是您的手写版本与使用clang++ 的普通版本之间的quick-bench 比较:

当使用g++ 时,结果是a tie,所以也没有任何收获。

【讨论】:

  • 吹毛求疵 - 元素必须通过放置 new new(m_data+m_size) T(std::move(val)); 而不是 operator= 插入。但它不会改变基准。
  • 奇怪的是,在Normal 中,编译器发出 SSE 指令,而对于HandWritten,它没有。当我使用 memcpy/memset 而不是手动循环时也是如此。在我的上述实验 (godbolt.org/z/cTs8z4) 中,GCC 的行为是相同的,直到我明确提供了 -mavx 标志。
  • @Quimby 也许吧。我使用了new T[...];,所以元素已经存在,可以移动分配。
  • @TedLyngmo Clang 仅使用 -O2:godbolt.org/z/TxTo94 生成了相同的基于 SSE 的程序集。不知道为什么它没有在快速板凳内。
  • @TedLyngmo 啊,没注意到,抱歉。
【解决方案2】:

你的计划很糟糕。

使用the as-if rule,编译器通常可以确定 memcpy 和零 - 甚至只是跳过析构函数 - 是合法的。

如果这样的编译器遇到您手工制作的未定义行为,它要么会被未定义行为弄糊涂,要么因此无法进一步优化。


有时有充分的理由诉诸未定义的行为。但他们首先要证明您的解决方案能让事情变得更好,然后用尽符合标准的解决方案。最后,他们坦诚地讨论了真正的短期、中期和长期风险,以换取短期的保证收益。

你的案子没有这些。

将 move-destroy 优化到 memcpy 是编译器已经对简单代码流中易于理解的类型进行的操作。手动操作是 99/100 毫无意义,90/100 倍有害。

如果您的类型不简单且代码流易于理解,那么您的优化可能也难以证明是安全的。如果你简化你的类型和代码流,当你可以可靠地证明你的 memcpy 零是最优的时,你的编译器可能也可以。

坐下来使用 Godbolt 并使用优化的编译器输出。很有教育意义。

【讨论】:

    【解决方案3】:

    您的优化可能在少数情况下有效。当您的移动构造函数执行特定操作时,它可能会导致问题。

     class MyCustomClass : public IObserver
     {
         Registry &registry;
         // ...
     public:
         MyCustomClass(MyCustomClass &&rhs)
         : registry{rhs.registry}
         {
              registry.register(*this);
         }
         // ...
     };
    

    如果您按位复制此类,则移动的实例未在注册表中注册。

    通过归零,引用被破坏,原始实例的析构函数很可能在从注册表中注销时崩溃。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2010-11-12
      • 2011-03-28
      • 1970-01-01
      • 2018-10-22
      • 2015-11-30
      • 2017-01-16
      • 2021-08-14
      相关资源
      最近更新 更多