Vergleich mit String-Literal, das zur Kompilierungszeit nicht aufgelöst wurde

8

Ich habe kürzlich etwas gefunden, das den folgenden Zeilen ähnelt:

#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";
}

Die Funktion macht offensichtlich nicht das, was der Kommentar vorschlägt. Aber darum geht es hier nicht. Bitte beachten Sie, dass dies kein Duplikat von Können Sie 2 oder mehr ODER-Bedingungen in einer if-Anweisung verwenden? da ich mir voll bewusst bin, wie du die Funktion richtig schreiben würdest!


Ich begann mich zu fragen, wie ein Compiler dieses Snippet behandeln könnte. Meine erste Intuition wäre gewesen, dass dies im return true;Grunde genommen kompiliert würde . Das Einstecken des Beispiels in Godbolt zeigte, dass weder GCC 9.2 noch Clang 9 diese Optimierung mit Optimierung durchführen -O2.

Ändern des Codes in 1

#include <string>

using namespace std::string_literals;

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

scheint den Trick zu tun, da die Versammlung jetzt im Wesentlichen ist:

mov     eax, 1
ret

Meine Kernfrage lautet also: Gibt es etwas, das ich verpasst habe und das es einem Compiler nicht ermöglicht, beim ersten Snippet dieselbe Optimierung vorzunehmen?


1 Mit ".foo"sdieser würde nicht einmal kompilieren, da der Compiler nicht konvertieren will eine std::stringzu bool;-)


Bearbeiten

Der folgende Code wird ebenfalls "richtig" optimiert für return true;:

#include <string>

bool test_extension(const std::string& ext) {
    return ".foo" || ext == ".bar";
}
AlexV
quelle
3
Hm, hat string::compare(const char*)es einige Nebenwirkungen, die der Compiler nicht beseitigen wird (die operator==(string, string)es nicht gibt)? Scheint unwahrscheinlich, aber der Compiler hat bereits festgestellt, dass das Ergebnis mov eax, 1 retauch für das erste Snippet immer wahr ist (auch hat ).
Max Langhof
2
Vielleicht, weil das operator==(string const&, string const&)ist, noexceptwährend das operator==(string const&, char const*)nicht ist? Ich habe jetzt keine Zeit, das weiter zu graben.
AProgrammer
@MaxLanghof Beim Ändern der Reihenfolge in foo || ext == ".bar"wird der Anruf weg optimiert (siehe Bearbeiten). Widerspricht das Ihrer Theorie?
AlexV
2
@AlexV Ich bin mir nicht sicher, was das bedeuten soll. Kurzschluss für den Ausdruck a || bbedeutet "Ausdruck bnur bewerten, wenn Ausdruck aist false". Es ist orthogonal zur Laufzeit oder zur Kompilierungszeit. true || foo()kann optimiert werden true, auch wenn foo()Nebenwirkungen auftreten, da (egal ob optimiert oder nicht) die rechte Seite niemals bewertet wird. Aber foo() || truekann nicht optimiert werden , truees sei denn der Compiler nachweisen kann , dass Berufung foo()keine feststellbaren Nebenwirkungen hat.
Max Langhof
1
Wenn ich Ihren bereitgestellten Compiler Explorer-Link nehme und die Option "In Binär kompilieren und Ausgabe zerlegen" aktiviere, wird er plötzlich kompiliert, xor eax,eaxobwohl er ohne diese Option die Funktion zum Vergleichen von Zeichenfolgen aufruft. Ich habe keine Ahnung, was ich davon halten soll.
Daniel H

Antworten:

3

Dies wird Ihren Kopf noch mehr verwirren: Was passiert, wenn wir einen benutzerdefinierten Zeichentyp erstellen MyCharTund ihn verwenden, um unseren eigenen benutzerdefinierten Typ zu erstellen 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";
}

Sicherlich kann ein benutzerdefinierter Typ nicht einfacher optimiert werden als ein einfacher char, oder?

Hier ist die resultierende Baugruppe:

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

Sehen Sie es live!


Verblüfft!

Was ist der Unterschied zwischen meinem "benutzerdefinierten" Zeichenfolgentyp und std::string?

Optimierung kleiner Zeichenfolgen

Zumindest unter GCC wird Small String Optimization tatsächlich in die Binärdatei für libstdc ++ kompiliert. Dies bedeutet, dass der Compiler während der Kompilierung Ihrer Funktion keinen Zugriff auf diese Implementierung hat und daher nicht wissen kann, ob Nebenwirkungen auftreten. Aus diesem Grund kann der Anruf nicht compare(char const*)entfernt werden. Unsere "benutzerdefinierte" Klasse hat dieses Problem nicht, da SSO nur für Plain implementiert ist std::string.

Übrigens, wenn Sie mit kompilieren -std=c++2a, optimiert der Compiler es weg . Ich bin leider noch nicht geschickt genug in C ++ 20, um zu wissen, welche Änderungen dies möglich gemacht haben.

Cássio Renan
quelle