为啥使用单个赋值运算符处理复制和移动赋值效率不高?

Posted

技术标签:

【中文标题】为啥使用单个赋值运算符处理复制和移动赋值效率不高?【英文标题】:Why is it not efficient to use a single assignment operator handling both copy and move assignment?为什么使用单个赋值运算符处理复制和移动赋值效率不高? 【发布时间】:2014-01-27 10:08:20 【问题描述】:

这是C++ Primer 5th Edition中的一个练习:

练习 13.53: 作为低级效率问题,HasPtr 赋值运算符并不理想。解释为什么。实施一个 HasPtr 的复制赋值和移动赋值运算符并进行比较 在新的移动赋值运算符中执行的操作与 复制和交换版本。(P.544)

文件hasptr.h

//! a class holding a std::string*
class HasPtr

    friend void swap(HasPtr&, HasPtr&);
    friend bool operator <(const HasPtr& lhs, const HasPtr& rhs);
public:
    //! default constructor.
    HasPtr(const std::string &s = std::string()):
        ps(new std::string(s)), i(0)
     

    //! copy constructor.
    HasPtr(const HasPtr& hp) :
        ps(new std::string(*hp.ps)), i(hp.i)
     

    //! move constructor.
    HasPtr(HasPtr&& hp) noexcept :
        ps(hp.ps), i(hp.i)
     hp.ps = nullptr; 

    //! assignment operator
    HasPtr&
    operator = (HasPtr rhs);

    //! destructor.
    ~HasPtr()
    
        delete ps;
    

private:
    std::string *ps;
    int    i;
;

部分文件hasptr.cpp

//! specific swap.
inline void
swap(HasPtr &lhs, HasPtr &rhs)

    using std::swap;
    swap(lhs.ps, rhs.ps); // swap the pointers, not the string data
    swap(lhs.i, rhs.i);   // swap the int members

    std::cout <<"swapping!\n";


//! operator = using specific swap
HasPtr&
HasPtr::operator = (HasPtr rhs)

    swap(*this,rhs);
    return *this;
 

我的问题是为什么这样做效率不高?

【问题讨论】:

为了回答这个问题,你首先要实现复制和移动。然后使用它们。然后比较发生了什么操作 - 计算移动和复制分配调用了多少移动和复制构造函数,以及上面的那个。 @Yakk '你首先必须实现复制和移动'--你的意思是复制和移动分配? 因为它复制了它? 另一个不同的问题是你为什么要维护一个指向std::string指针,答案是在99%的情况下你不想这样做.但回到最初的问题,给 lvalue 分配一个长度相同或更小的字符串会有什么影响? @Alan.W:没有理由(在大多数情况下)动态创建std::string,它的代价是额外的内存分配和必须管理生命周期的痛苦。鉴于没有优点和一些缺点,我会避免这样做(我在生产代码中从未遇到过,在代码审查中我不会接受) 【参考方案1】:

第 1 步

设置执行移动赋值运算符的性能测试。

设置另一个执行复制赋值运算符的性能测试。

第 2 步

按照问题陈述中的说明,双向设置赋值运算符。

第 3 步

重复第 1 步和第 2 步,直到您确信自己正确地完成了它们。

第 3 步应该帮助您了解正在发生的事情,很可能会告诉您性能在哪里发生变化在哪里没有变化。

猜测不是步骤 1-3 的选项。你实际上必须这样做。否则你将(正确地)不相信你的猜测是正确的。

第 4 步

现在你可以开始猜测了。有人会称之为“形成假设”。说“猜测”的花哨方式。但至少现在它是受过教育的猜测。

我在回答这个问题时完成了这个练习,并注意到一个测试没有显着的性能差异,而另一个测试有 6 倍的性能差异。这进一步让我想到了一个假设。完成这项工作后,如果您不确定自己的假设,请使用您的代码、结果和后续问题更新您的问题。

澄清

有两个特殊的成员赋值运算符,它们通常具有签名:

HasPtr& operator=(const HasPtr& rhs);  // copy assignment operator
HasPtr& operator=(HasPtr&& rhs);       // move assignment operator

可以使用单个赋值运算符实现移动赋值和复制赋值,这就是所谓的复制/交换习语:

HasPtr& operator=(HasPtr rhs);

这个单一的赋值运算符不能被第一个集合重载。

使用复制/交换习语实现两个赋值运算符(复制和移动)还是只实现一个更好?这就是练习 13.53 所要求的。要回答,您必须尝试两种方式,并测量复制分配和移动分配。聪明,善意的人通过猜测而不是测试/测量来弄错。你选择了一个很好的练习来学习。

【讨论】:

您好,谢谢您的建议。只是想知道您在句子中指的是什么:注意到一个测试没有显着的性能差异,而另一个的性能差异是6倍。 这里要检查两个赋值运算符:复制赋值运算符和移动赋值运算符。按值获取 rhs 的赋值运算符可以用一个签名实现两者。确定这是否是一个好主意是本练习的重点。您必须同时测试复制作业和移动作业才能完全完成此练习。 感谢您的澄清。还有一个问题,作为一个从未使用过任何性能测量工具的新手,我应该使用任何工具来测量性能吗?哪个工具适合此代码?有什么推荐吗? 我喜欢使用 C++11 &lt;chrono&gt; 工具。在本练习中,我将测试包装在 auto t0 = high_resolution_clock::now();auto t1 = high_resolution_clock::now(); 中,然后以毫秒为单位打印出经过的时间:std::cout &lt;&lt; duration_cast&lt;milliseconds&gt;(t1-t0).count() &lt;&lt; "ms\n";。以上假设using namespace std::chrono;。没有它,我列出的几个标识符将找不到。如果您没有auto,请使用high_resolution_clock::time_point 代替。 我还在t0t1 之间放置了大量的复制/移动分配,所以我得到了一个非零的毫秒数。放置太少会导致更快的测试运行,但结果的可变性更大。放置太多会导致烦人的长时间测试。正确进行性能测试是一种艺术形式。【参考方案2】:

正如问题所暗示的,这是“低级效率问题”。当您使用HasPtr&amp; operator=(HasPtr rhs) 并编写类似hp = std::move(hp2); 的内容时,ps 成员被复制两次(指针本身而不是它指向的对象):一次从hp2rhs,因为调用 move构造函数,一次从rhs*this 作为调用swap 的结果。但是当你使用HasPtr&amp; operator=(HasPtr&amp;&amp; rhs) 时,ps 只会从rhs 复制一次到*this

【讨论】:

以上是关于为啥使用单个赋值运算符处理复制和移动赋值效率不高?的主要内容,如果未能解决你的问题,请参考以下文章

为啥短原语有赋值运算符(&=、+=)但没有非赋值运算符(&、+)? [复制]

为啥我能够为 QObject 子类创建复制构造函数并重载赋值运算符?

为啥我们在重载赋值中使用 return *this? [复制]

自动生成默认/复制/移动 ctor 和复制/移动赋值运算符的条件?

为啥移动赋值运算符应该返回对 *this 的引用 [重复]

如何利用模板复制&移动构造函数和赋值运算符?