为什么有些人使用交换移动作业? [英] Why do some people use swap for move assignments?

查看:140
本文介绍了为什么有些人使用交换移动作业?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

例如,stdlibc ++具有以下属性:

  unique_lock& operator =(unique_lock&& __u)
{
if(_M_owns)
unlock();
unique_lock(std :: move(__ u))。swap(* this);
__u._M_device = 0;
__u._M_owns = false;
return * this;
}

为什么不直接将两个__u成员分配给* this?不交换暗示__u被分配了*这个成员,只有稍后分配0和假...在这种情况下,交换正在做不必要的工作。我缺少什么?
(unique_lock :: swap只是每个成员都有一个std :: swap)

解决方案

这是我的错。 (half-kidding,half-not)。



当我第一次展示移动赋值运算符的示例实现时,我只是使用交换。然后一些聪明的家伙(我不记得谁)指出,在分配之前破坏lhs的副作用可能是重要的(如在你的例子中的unlock())。所以我停止使用交换移动分配。但是使用交换的历史仍然存在,并继续存在。



在这个例子中没有理由使用交换。它的效率低于你的建议。确实,在 libc ++ 中,我完全按照您的建议:

  unique_lock& operator =(unique_lock&& __u)
{
if(__owns_)
__m _-> unlock
__m_ = __u .__ m_;
__owns_ = __u .__ owns_;
__u .__ m_ = nullptr;
__u .__ owns_ = false;
return * this;
}

一般来说,移动赋值操作符应该:


  1. 销毁可见资源(但可能还会保存实施详细资源)。

  2. 移动分配所有基数和成员

  3. 如果基数和成员的移动分配没有使rhs无资源,那么就这么做。

像这样:

  unique_lock& operator =(unique_lock&& __u)
{
// 1.销毁可见资源
if(__owns_)
__m _-> unlock
// 2.移动分配所有基数和成员。
__m_ = __u .__ m_;
__owns_ = __u .__ owns_;
// 3.如果基址和成员的移动赋值没有,
//使rhs无资源,那么就这么做。
__u .__ m_ = nullptr;
__u .__ owns_ = false;
return * this;
}

更新

在注释中有一个关于如何处理移动构造函数的后续问题。我开始回答那里(在评论),但格式和长度限制使得很难创建一个明确的反应。因此,我把我的回答在这里。



问题是:什么是创建移动构造函数的最佳模式?代理到默认构造函数然后交换?这具有减少代码重复的优点。



我的回答是:我认为最重要的是,程序员应该没有思考下面的模式。可能有一些类实现移动构造函数作为默认+交换是完全正确的答案。这个类可能很大又复杂。 A(A&&)= default; 可能做错了事。我认为重要的是考虑每个类的所有选择。



让我们来详细看看OP的例子: std :: unique_lock(unique_lock&&)



观察:



这个类相当简单。它有两个数据成员:


  mutex_type * __m_; 
bool __owns_;


B。这个类在一个通用库中,被未知数量的客户端使用。在这种情况下,性能问题是高度优先的。我们不知道我们的客户端是否将在性能关键代码中使用这个类。所以我们必须假设他们是。



C。这个类的移动构造函数将由少量的加载和存储组成,不管是什么。因此,一个好的方式来看看性能是计数加载和存储。例如,如果你对4个商店做某事,而其他人只有2个商店做同样的事情,你的两个实现都非常快。但他们的速度是你的速度的两倍!



首先让我们在默认构造函数和成员交换函数中计算加载和存储:

  // 2 stores 
unique_lock()
:__m_(nullptr),
__owns_(false)
{
}

// 4 store,4 loads
void swap(unique_lock& __u)
{
std :: swap(__ m_,__u .__ m_);
std :: swap(__ owns_,__u .__ owns_);
}

现在可以通过两种方式实现move构造函数:

  // 4 store,2 loads 
unique_lock(unique_lock& __u)
:__m _(__ u .__ m_),
__owns _(__ u .__ owns_)
{
__u .__ m_ = nullptr;
__u .__ owns_ = false;
}

// 6 store,4 loads
unique_lock(unique_lock& __u)
:unique_lock()
{
swap (__u);
}

第一种方法比第二种方法复杂得多。源代码较大,有些重复的代码,我们可能已经写在别处(例如在移动赋值运算符)。这意味着有更多的错误机会。



第二种方法是更简单和重用我们已经写的代码。因此,错误的机会较少。



第一种方式更快。如果装载和商店的成本大致相同,可能会快66%!



这是一个经典的工程权衡。没有免费午餐。而且工程师从来没有免除必须作出权衡决策的负担。



对于 libc ++ ,我选择了更快的解决方案。我的理由是,对于这个类,我更好地得到它的权利,无论什么;这个类很简单,我的正确机会很高;我的客户将重视性能。我可能在另一个不同的上下文中得到另一个结论。


For example, stdlibc++ has the following:

unique_lock& operator=(unique_lock&& __u)
{
    if(_M_owns)
        unlock();
    unique_lock(std::move(__u)).swap(*this);
    __u._M_device = 0;
    __u._M_owns = false;
    return *this;
}

Why not just assign the two __u members to *this directly? Doesn't the swap imply that __u is assigned the *this members, only to later have then assigned 0 and false... in which case the swap is doing unnecessary work. What am I missing? (the unique_lock::swap just does an std::swap on each member)

解决方案

It's my fault. (half-kidding, half-not).

When I first showed example implementations of move assignment operators, I just used swap. Then some smart guy (I can't remember who) pointed out to me that the side effects of destructing the lhs prior to the assignment might be important (such as the unlock() in your example). So I stopped using swap for move assignment. But the history of using swap is still there and lingers on.

There's no reason to use swap in this example. It is less efficient than what you suggest. Indeed, in libc++, I do exactly what you suggest:

unique_lock& operator=(unique_lock&& __u)
    {
        if (__owns_)
            __m_->unlock();
        __m_ = __u.__m_;
        __owns_ = __u.__owns_;
        __u.__m_ = nullptr;
        __u.__owns_ = false;
        return *this;
    }

In general a move assignment operator should:

  1. Destroy visible resources (though maybe save implementation detail resources).
  2. Move assign all bases and members.
  3. If the move assignment of bases and members didn't make the rhs resource-less, then make it so.

Like so:

unique_lock& operator=(unique_lock&& __u)
    {
        // 1. Destroy visible resources
        if (__owns_)
            __m_->unlock();
        // 2. Move assign all bases and members.
        __m_ = __u.__m_;
        __owns_ = __u.__owns_;
        // 3. If the move assignment of bases and members didn't,
        //           make the rhs resource-less, then make it so.
        __u.__m_ = nullptr;
        __u.__owns_ = false;
        return *this;
    }

Update

In comments there's a followup question about how to handle move constructors. I started to answer there (in comments), but formatting and length constraints make it difficult to create a clear response. Thus I'm putting my response here.

The question is: What's the best pattern for creating a move constructor? Delegate to the default constructor and then swap? This has the advantage of reducing code duplication.

My response is: I think the most important take-away is that programmers should be leery of following patterns without thought. There may be some classes where implementing a move constructor as default+swap is exactly the right answer. The class may be big and complicated. The A(A&&) = default; may do the wrong thing. I think it is important to consider all of your choices for each class.

Let's take a look at the OP's example in detail: std::unique_lock(unique_lock&&).

Observations:

A. This class is fairly simple. It has two data members:

mutex_type* __m_;
bool __owns_;

B. This class is in a general purpose library, to be used by an unknown number of clients. In such a situation, performance concerns are a high priority. We don't know if our clients are going to be using this class in performance critical code or not. So we have to assume they are.

C. The move constructor for this class is going to consist of a small number of loads and stores, no matter what. So a good way to look at the performance is to count loads and stores. For example if you do something with 4 stores, and somebody else does the same thing with only 2 stores, both of your implementations are very fast. But their's is twice as fast as yours! That difference could be critical in some client's tight loop.

First lets count loads and stores in the default constructor, and in the member swap function:

// 2 stores
unique_lock()
    : __m_(nullptr),
      __owns_(false)
{
}

// 4 stores, 4 loads
void swap(unique_lock& __u)
{
    std::swap(__m_, __u.__m_);
    std::swap(__owns_, __u.__owns_);
}

Now lets implement the move constructor two ways:

// 4 stores, 2 loads
unique_lock(unique_lock&& __u)
    : __m_(__u.__m_),
      __owns_(__u.__owns_)
{
    __u.__m_ = nullptr;
    __u.__owns_ = false;
}

// 6 stores, 4 loads
unique_lock(unique_lock&& __u)
    : unique_lock()
{
    swap(__u);
}

The first way looks much more complicated than the second. And the source code is larger, and somewhat duplicating code we might have already written elsewhere (say in the move assignment operator). That means there's more chances for bugs.

The second way is simpler and reuses code we've already written. Thus less chance of bugs.

The first way is faster. If the cost of loads and stores is approximately the same, perhaps 66% faster!

This is a classic engineering tradeoff. There is no free lunch. And engineers are never relieved of the burden of having to make decisions about tradeoffs. The minute one does, planes start falling out of the air and nuclear plants start melting down.

For libc++, I chose the faster solution. My rationale is that for this class, I better get it right no matter what; the class is simple enough that my chances of getting it right are high; and my clients are going to value performance. I might well come to another conclusion for a different class in a different context.

这篇关于为什么有些人使用交换移动作业?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
相关文章
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆