6

Friendly reminder to mark your move constructors noexcept – Andy G's Blog

 3 years ago
source link: https://gieseanw.wordpress.com/2020/08/28/friendly-reminder-to-mark-your-move-constructors-noexcept/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Friendly reminder to mark your move constructors noexcept

^^^ The culprit to your performance losses ^^^

Since C++11 we have had the noexcept keyword, which is a promise that the function will not throw an exception (and if it does, go straight to std::terminate, do not pass go). noexcept is nice for two reasons:

  1. The compiler can optimize a little better because it doesn’t need to emit any code for unwinding a call stack in case of an exception, and
  2. It leads to incredible performance differences at runtime for std::vector (and other containers, too)

We’re all familiar with std::vector, our favorite wrapper around a contiguous resizable array. What you may not be familiar with is the fact that std::vector::push_back (and emplace_back) make "strong exception guarantees"

A strong exception guarantee is a guarantee that if emplace_back should fail, the vector is otherwise unchanged. For example:

struct MyClass
{
MyClass(int i)
{
if (i == 3)
throw std::invalid_argument("How dare you");
}
};
int main()
{
std::vector<MyClass> vec;
vec.emplace_back(1);
vec.emplace_back(2);
try{
vec.emplace_back(3); // throws
}catch(...){}
std::cout << vec.size() << '\n'; // 2
}

If emplace_back causes the vector to reallocate, it can’t exactly std::move() your elements to the new storage if the operation might throw, so it will copy them instead.

For example:

struct MyClass
{
MyClass(int i)
{
if (i == 3)
throw std::invalid_argument("How dare you");
}
MyClass(const MyClass&)
{
std::cout << "Copied\n";
}
MyClass(MyClass&&)
{
std::cout << "Moved\n";
}
};
int main()
{
std::vector<MyClass> vec;
vec.emplace_back(1);
vec.emplace_back(2); // outputs "Copied" after reallocating to store 2 elements
}

But when we mark our move constructor as noexcept, suddenly we’re only performing moves on reallocation.

For example:

struct MyClass
{
MyClass(int i)
{
if (i == 3)
throw std::invalid_argument("How dare you");
}
MyClass(const MyClass&)
{
std::cout << "Copied\n";
}
MyClass(MyClass&&) noexcept
{
std::cout << "Moved\n";
}
};
int main()
{
std::vector<MyClass> vec;
vec.emplace_back(1);
vec.emplace_back(2); // outputs "Moved" after reallocating to store 2 elements
}

If no copy operation is available, it will begrudgingly use your throwing move constructor and forsake all exception guarantees.

For example:

struct MyClass
{
MyClass(int i)
{
if (i == 3)
throw std::invalid_argument("How dare you");
}
MyClass(const MyClass&) = delete;
MyClass(MyClass&&) noexcept(false)
{
std::cout << "Moved\n";
}
};
int main()
{
std::vector<MyClass> vec;
vec.emplace_back(1);
vec.emplace_back(2); // outputs "Moved" after reallocating to store 2 elements
}

Except MSVC didn’t start behaving this way until Visual Studio 2017!

The issue is historical (as most are) — when MSVC first got support for C++11, it didn’t support the noexcept keyword at all, so programmers didn’t have a ton of control over this. (Well, there were dynamic exception specifications but nobody really used them).

Not wanting to sacrifice performance, Microsoft opted to bend their standards compliance for a while until they could have proper noexcept support. I personally think Microsoft did a poor job of communicating this change; I cannot find a single article about it.

(However, I can forgive them due to the excellent performance profiler that comes with VS these days.)

I only found this issue when my team saw a big performance hit after upgrading our toolset to vs141, when I tracked it down to the surprisingly well-named function std::vector::_Umove_if_noexcept.

So, be your project’s hero and add noexcept to your move constructors wherever feasible. Better yet, let the compiler define your move constructor for you if possible.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK