【问题标题】:low efficient std::move()?低效率的 std::move()?
【发布时间】:2020-11-23 09:20:54
【问题描述】:

请看下面的sn-p代码,看来std::move()在这种情况下效率很低。

class A {};

struct B {
    double pi{ 3.14 };
    int    i{ 100 };
    A* pa{ nullptr };
};

int main() {
    B b;
    std::vector<B> vec;
    vec.emplace_back(b);                // 1) without move
    vec.emplace_back(std::move(b));     // 2) with move
    return 0;
}

我在 Visual Studio 2019 [C++ 14,Release] 中得到了以下反汇编:

    vec.emplace_back(b);                // 1) without move
00E511D1  push        eax  
00E511D2  push        0  
00E511D4  lea         ecx,[vec]  
00E511D7  call        std::vector<B,std::allocator<B> >::_Emplace_reallocate<B> (0E512C0h)  
    vec.emplace_back(std::move(b));     // 2) with move
00E511DC  mov         eax,dword ptr [ebp-18h]  
00E511DF  cmp         eax,dword ptr [ebp-14h]  
00E511E2  je          main+91h (0E511F1h)  
00E511E4  movups      xmm0,xmmword ptr [b]  
00E511E8  movups      xmmword ptr [eax],xmm0  
00E511EB  add         dword ptr [ebp-18h],10h  
00E511EF  jmp         main+9Eh (0E511FEh)  
00E511F1  lea         ecx,[b]  
00E511F4  push        ecx  
00E511F5  push        eax  
00E511F6  lea         ecx,[vec]  
00E511F9  call        std::vector<B,std::allocator<B> >::_Emplace_reallocate<B> (0E512C0h)  

很容易看出,移动版本需要更多不必要的工作。根据here的描述,编译器将为结构B生成一个普通的移动构造函数,这个普通的移动构造函数将采用复制语义

那么我的问题是:

  1. std::move() 在这种情况下是完全多余的。
  2. 此外,如果 std::move() 的参数具有平凡的移动构造函数,则 std::move() 是多余的。
  3. 如果普通移动构造函数执行与普通复制构造函数相同的操作,为什么编译器会生成不同的反汇编?实际上,这对我来说是最令人困惑的。

【问题讨论】:

  • 看来你的反汇编也有调整矢量大小的代码。您可以尝试提前预订。
  • 是的,你是对的。
  • 优化编译器会将 main() 的主体优化为“返回 0”。最好使用 godbolt.org 进行此类研究。
  • 很高兴知道。我尝试使用 godbolt.org,现在更加困惑。对于非移动函数,称为 void std::vector >::emplace_back(B &);对于移动版本,称为 void std::vector >::emplace_back(B &&)。因此,std::move() 似乎确实有效,因为调用了不同版本的 emplace_back()。
  • B&amp;B 非常不同,不要期望运行相同的代码。此外,如果您的指针做了更有趣的事情,您可能还想编写自己的 move 构造函数/赋值等。

标签: c++ move-semantics


【解决方案1】:
  1. std::move() 在这种情况下是完全多余的。

技术上不是问题,但是是的,这是正确的。

  1. 此外,如果 std::move() 的参数具有平凡的移动构造函数,则 std::move() 是多余的。

同上。

  1. 如果普通移动构造函数执行与普通复制构造函数相同的操作,为什么编译器会生成不同的反汇编?

可能是因为您在一个中调用函数std::move 而不是另一个。

但它不必产生不同的程序集,因为可观察的行为是相同的。我的编译器生成相同的程序集。

【讨论】:

  • 我尝试了@bobah 建议的goldbolt.org,但反汇编有所不同。对于非移动函数,称为 void std::vector >::emplace_back(B &);对于移动版本,称为 void std::vector >::emplace_back(B &&)。因此,std::move() 似乎确实有效,因为调用了不同版本的 emplace_back()。
  • @fetag 如果你想分享这方面的信息,你可以分享一个godbolt链接。目前我不确切知道您使用的编译器版本或如何重现您看到的内容。
  • @没用的非常感谢!就是这样:godbolt.org/z/zczGYG
  • @fetag 好的,没有优化。甚至对 std::move 的调用也没有内联。如果您完全打开任何优化,图片就会大不相同,而且不太清晰。
【解决方案2】:

好吧,我想对这个问题做一个总结。

  1. 如果移动构造函数是微不足道的(由编译器生成),则 std::move() 只需要按位复制。这意味着 vec.emplace_back(std::move(b)) 等价于 vec.emplace_back(b)。

  2. 以下是另一个更好的示例来显示差异。我们尝试用移动语义构造两个成员 _x 和 _y。由于 X 具有用户声明的移动 ctor 而 Y 没有,因此调用 X 的移动 ctor,并调用 Y 的复制 ctor。

class X {
public:
    X() { cout << "X()" << endl; }
    X(X const&) { cout << "X(X const&)" << endl; }
    X(X&&) noexcept { cout << "X(X&&)" << endl; }
};

class Y {
public:
    Y() { cout << "Y()" << endl; }
    Y(Y const&) { cout << "Y(Y const&)" << endl; }
};

struct Box {
    Box(X&& x, Y&& y) : _x(std::move(x)), _y(std::move(y)) {}
    X _x;
    Y _y;
};

int main() {
    X x;
    Y y;
    Box bb(std::move(x), std::move(y));
    return 0;
}

/*
output:
X()
Y()
X(X&&)
Y(Y const&)
*/

【讨论】: