Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

On the implementation of std::string moves

I was investigating the performance of moving std::string. For the longest time, I've regarded string moves as almost free, thinking the compiler will inline everything and it will only involve a few cheap assignments.

In fact, my mental model for moving is literally

string& operator=(string&& rhs) noexcept
{
    swap(*this, rhs);
    return *this;
}

friend void swap(string& x, string& y) noexcept
{
    // for disposition only
    unsigned char buf[sizeof(string)];
    memcpy(buf, &x, sizeof(string));
    memcpy(&x, &y, sizeof(string));
    memcpy(&y, buf, sizeof(string));
}

To the best of my understanding, this is a legal implementation if the memcpy is changed to assigning individual fields.

It is to my great surprise to find gcc's implementation of moving involves creating a new string and might possibly throw due to the allocations despite being noexcept.

Is this even conforming? Equally important, should I not think moving is almost free?


Bewilderingly, std::vector<char> compiles down to what I'd expect.

clang's implementation is much different, although there is a suspicious std::string::reserve

like image 994
Passer By Avatar asked May 29 '18 14:05

Passer By


People also ask

Is std::string movable?

Yes, std::string (since C++11) is able to be moved i.e. it supports move semantics.

What does std :: move () do?

std::move is used to indicate that an object t may be "moved from", i.e. allowing the efficient transfer of resources from t to another object. In particular, std::move produces an xvalue expression that identifies its argument t . It is exactly equivalent to a static_cast to an rvalue reference type.

How are strings implemented in C++?

Strings in C++ can be defined in two ways: using the string class or character arrays. It is more convenient to use string objects over character arrays. The string class provides many functions for the string objects. We can take string inputs from the user using the cin keyword or getline() function.

Does STD copy move?

std::move is actually just a request to move and if the type of the object has not a move constructor/assign-operator defined or generated the move operation will fall back to a copy.


1 Answers

I've only analyzed GCC's version. Here's what's going on: the code handles different kind of allocators. If the allocator has the trait of _S_propagate_on_move_assign or _S_always_equal, then the move is almost free, as you expect. This is the if in move operator=:

if (!__str._M_is_local()
    && (_Alloc_traits::_S_propagate_on_move_assign()
      || _Alloc_traits::_S_always_equal()))
          // cheap move
else assign(__str);

If the condition is true (_M_is_local() means small string, description here), then the move is cheap.

If it is false, then it calls normal assign (not the moving one). This is the case when either:

  • the string is small, so the assign will do a simple memcpy (cheap)
  • or the allocator doesn't have the trait always-equal nor propagate-on-move-assign, so the assign will allocate (not cheap)

What does this mean?

It means, that if you use the default allocator (or any allocator with traits mentioned earlier), then the move is still almost free.

On the other hand, the generated code is unnecessarily huge, and can be improved I think. It should have a separate code for handling usual allocators, or have a better assign code (the problem is that assign doesn't check for _M_is_local(), but it does a capacity check, so the compiler cannot decide whether an allocation is needed or not, so it puts the allocation codepath into the executable unnecessarily - you can check out the exact details in the source code).

like image 98
geza Avatar answered Oct 17 '22 07:10

geza