Why is there no optimization for checking for empty string via "" comparison?

Viewed 351

This google-benchmark code checked on Quick Bench, shows that string::empty() runs a lot faster than comparing with an empty-string literal. However, creating a name string of "" actually makes the compiler optimize away the check:

bool compareWithNamedConst(const std::string& target) {
    const std::string emptyString = "";
    return emptyString == target;
}

bool compareWithLiteral(const std::string& target) {
    return "" == target;
}

bool compareWithRvalue(const std::string& target) {
    return std::string{""} == target;
}

bool checkForEmpty(const std::string& target) {
    return target.empty();
}

The performance for each of the calls is shown here:

As you can see, comparing with "" is very slow compared to all the other options. I wonder why it is the case? It must be somehow related to SSO not being applied on const char*, as testing this:

bool compareWithLiteral(const std::string& target) {
    return "test with a longer string not optimized" == target;
}

bool compareWithRvalue(const std::string& target) {
    return std::string{"test with a longer string not optimized"} == target;
}

Results comparing with a literal actually being faster:

I find that checking for string emptiness, the easiest syntax to read is "" == myVariable as it clearly indicates that myVariable is a std::string with no unneccesary clutter. Why can't we have it optimized as we have all the other cases?

2 Answers

For emptyness check, if you really like the literal syntax, you may help your compiler by using namespace std::string_literals;, and replacing "" with ""s. In that case compareWithLiteral, compareWithRvalue and checkForEmpty would be essentially the same, since the operator== between const std::string& usually checks the sizes before the content, and the size of ""s is trivial.

bool compareWithLiteral(const std::string& target) {
    using namespace std::string_literals;
    return ""s == target;
}

For strings not falling into the SSO, you should try with std::string_view and its operator""sv, even if unfortunately is available only since C++17. Boost has boost::string_view. No user string literal is provided for compile time creation (essential because the test string length would be hardcoded in the binary), but you can easily define one:

#include <boost/utility/string_view.hpp>

constexpr auto operator""_sv(const char* str, std::size_t len) noexcept {
    return boost::basic_string_view<char>{str, len};
}

bool compareWithLiteral(const std::string& target) {
    return "test with a longer string not optimized"_sv == target;
}

This version runs much faster than your compareWithLiteral version, at least when target size is different.

I believe the main difference is that comparison that involves std::string objects first only checks their sizes. If they are not equal, the comparison immediately returns. (That's also might explain why memcmp may be then used if both strings are of equal lengths, as pointed out by @KamilCuk.)

On the contrary, with literals, these are treated as C-strings/char-arrays where no information about their lengths are provided. Therefore, strcmp function is used inside which has larger overhead, even in case that one of the string is empty (at least, the overhead of a function call since strcmp seems to be not inlined in your assembly).

As for the last question, compareWithRvalue, there the string is too large for SSO, therefore, dynamic memory allocation (and deallocation) is performed (and likely not optimized away here), which represents a relatively large overhead here.

Related