8

I recently found something akin to the following lines:

#include <string>

// test if the extension is either .bar or .foo
bool test_extension(const std::string& ext) {
    return ext == ".bar" || ".foo";
    // it obviously should be
    // return ext == ".bar" || ext == ".foo";
}

The function obviously does not do what the comment suggests. But that's not the point here. Please note that this is not a duplicate of Can you use 2 or more OR conditions in an if statement? since I'm fully aware of how you would write the function properly!


I started to wonder how a compiler might treat this snippet. My first intuition would have been that this would be compiled to return true; basically. Plugging the example into godbolt, showed that neither GCC 9.2 nor clang 9 make this optimization with optimization -O2.

However, changing the code to1

#include <string>

using namespace std::string_literals;

bool test_extension(const std::string& ext) {
    return ext == ".bar"s || ".foo";
}

seems to do the trick since the assembly is now in essence:

mov     eax, 1
ret

So my core question is: Is there something I missed that does not allow a compiler to make the same optimization on the first snippet?


1With ".foo"s this would not even compile, since the compiler does not want to convert a std::string to bool ;-)


Edit

The following piece of code also gets "properly" optimized to return true;:

#include <string>

bool test_extension(const std::string& ext) {
    return ".foo" || ext == ".bar";
}
AlexV
  • 578
  • 8
  • 19
  • 3
    Hm, does `string::compare(const char*)` have some side effects that the compiler won't eliminate (that `operator==(string, string)` doesn't have)? Seems unlikely, but the compiler did already determine that the result is always true (also has `mov eax, 1` `ret`) even for the first snippet. – Max Langhof Dec 03 '19 at 13:57
  • 2
    Perhaps because the `operator==(string const&, string const&)` is `noexcept` while the `operator==(string const&, char const*)` isn't? I've no time to dig that further now. – AProgrammer Dec 03 '19 at 14:19
  • @MaxLanghof When changing the order to `foo || ext == ".bar"`, the call is optimized away (see edit). Does that contradict your theory? – AlexV Dec 03 '19 at 14:51
  • @AlexV No, because `||` short-circuits. The edit is equivalent to `true || ...` which will never evaluate the right hand side operand. Also note that you can replace `".foo"` with `true` in all your code and get the exact same result (which you seem to be aware of, but I'd say it's weird to frame the question with the string version when there's no apparent reason - it only adds potential for confusion). – Max Langhof Dec 03 '19 at 14:52
  • @MaxLanghof IIRC someone mentioned in a (now deleted) comment, that short circuiting is a runtime thing. That lead me to rephrase parts of the question. Would you say that they were wrong in that regard? – AlexV Dec 03 '19 at 14:57
  • 2
    @AlexV I'm not sure what that's supposed to mean. Short-circuiting for the expression `a || b` means "evaluate expression `b` only if expression `a` is `false`". It's orthogonal to run-time or compile-time. `true || foo()` can be optimized to `true`, even if `foo()` has side effects, because (no matter whether optimized or not) the right hand side is never evaluated. But `foo() || true` cannot be optimized to `true` unless the compiler can prove that calling `foo()` has no observable side-effects. – Max Langhof Dec 03 '19 at 15:05
  • @AProgrammer Hm, I don't think it's exceptions that are the issue. Even at `-O3 -fno-exceptions` clang won't optimize it out. And if I'm reading the LLVM IR correctly, the function is [`nounwind`](https://llvm.org/docs/LangRef.html#function-attributes) in the first place: https://godbolt.org/z/229cWH – Max Langhof Dec 03 '19 at 15:15
  • Related: "*[Should I compare a std::string to “string” or “string”s?](https://stackoverflow.com/questions/56427627/should-i-compare-a-stdstring-to-string-or-strings)*" – Deduplicator Dec 03 '19 at 15:47
  • 1
    When I take your provided Compiler Explorer link and check the "Compile to binary and disassemble the output" option, it is suddenly compiles to `xor eax,eax` even though without that option it calls the string compare function. I have no idea what to make of that. – Daniel H Dec 04 '19 at 11:35
  • @DanielH Interesting find! – AlexV Dec 04 '19 at 11:42
  • @DanielH that is because the disassembled code shown is for a `main` function, and not for the `test_extension` function. Since the latter was never used, the compiler simply never added it to the final executable. How it generated an executable with a `main` function, however, is not clear to me, since there's no definition for one in the example. Maybe the compiler explorer adds one when it is not present. – Not a real meerkat Jan 09 '20 at 02:28

1 Answers1

3

This will boggle your head even more: What happens if we create a custom char type MyCharT and use it to make our own custom std::basic_string?

#include <string>

struct MyCharT {
    char c;
    bool operator==(const MyCharT& rhs) const {
        return c == rhs.c;
    }
    bool operator<(const MyCharT& rhs) const {
        return c < rhs.c;
    }
};
typedef std::basic_string<MyCharT> my_string;

bool test_extension_custom(const my_string& ext) {
    const MyCharT c[] = {'.','b','a','r', '\0'};
    return ext == c || ".foo";
}

// Here's a similar implementation using regular
// std::string, for comparison
bool test_extension(const std::string& ext) {
    const char c[] = ".bar";
    return ext == c || ".foo";
}

Certainly, a custom type cannot be optimized more easily than a plain char, right?

Here's the resulting assembly:

test_extension_custom(std::__cxx11::basic_string<MyCharT, std::char_traits<MyCharT>, std::allocator<MyCharT> > const&):
        mov     eax, 1
        ret
test_extension(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&):
        sub     rsp, 24
        lea     rsi, [rsp+11]
        mov     DWORD PTR [rsp+11], 1918984750
        mov     BYTE PTR [rsp+15], 0
        call    std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::compare(char const*) const
        mov     eax, 1
        add     rsp, 24
        ret

See it live!


Mindblown!

So, what's the difference between my "custom" string type and std::string?

Small String Optimization

At least on GCC, Small String Optimization is actually compiled into the binary for libstdc++. This means that, during the compilation of your function, the compiler has no access to this implementation, thus, it cannot know if there are any side effects. Because of this, it cannot optimize the call to compare(char const*) away. Our "custom" class does not have this problem because SSO is implemented only for plain std::string.

BTW, if you compile with -std=c++2a, the compiler does optimize it away. I'm unfortunately not savvy enough on C++ 20 yet to know what changes made this possible.

Not a real meerkat
  • 5,604
  • 1
  • 24
  • 55