Comparando com o literal da string não resolvido no tempo de compilação

8

Recentemente, encontrei algo semelhante às seguintes linhas:

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

Obviamente, a função não faz o que o comentário sugere. Mas esse não é o ponto aqui. Observe que esta não é uma duplicata de Você pode usar 2 ou mais condições OR em uma instrução if? desde que eu estou ciente de como você escreveria a função corretamente!


Comecei a me perguntar como um compilador poderia tratar esse trecho. Minha primeira intuição seria que isso seria compilado return true;basicamente. Ao incluir o exemplo no godbolt , mostrou que nem o GCC 9.2 nem o clang 9 fazem essa otimização com otimização -O2.

No entanto, alterar o código para 1

#include <string>

using namespace std::string_literals;

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

parece fazer o truque, já que a montagem agora é essencialmente:

mov     eax, 1
ret

Então, minha pergunta principal é: existe algo que eu perdi que não permite que um compilador faça a mesma otimização no primeiro trecho?


1 Com ".foo"sisso nem compilaria, pois o compilador não deseja converter um std::stringpara bool;-)


Editar

O seguinte trecho de código também é otimizado "corretamente" para return true;:

#include <string>

bool test_extension(const std::string& ext) {
    return ".foo" || ext == ".bar";
}
AlexV
fonte
3
Hum, string::compare(const char*)tem alguns efeitos colaterais que o compilador não eliminará (que operator==(string, string)não possui)? Parece improvável, mas o compilador já determinou que o resultado é sempre verdadeiro (também tem mov eax, 1 ret) mesmo para o primeiro trecho.
precisa
2
Talvez porque operator==(string const&, string const&)seja noexceptenquanto operator==(string const&, char const*)não é? Não tenho tempo para aprofundar isso agora.
APROGRAMMER #
@MaxLanghof Ao alterar a ordem para foo || ext == ".bar", a chamada é otimizada (consulte editar). Isso contradiz sua teoria?
AlexV # 03/19
2
@ AlexV Não sei ao certo o que isso significa. Curto-circuito para a expressão a || bsignifica "avaliar a expressão bapenas se a expressão afor false". É ortogonal ao tempo de execução ou tempo de compilação. true || foo()pode ser otimizado para true, mesmo que foo()tenha efeitos colaterais, porque (não importa se otimizado ou não) o lado direito nunca é avaliado. Mas foo() || truenão pode ser otimizado, a truemenos que o compilador possa provar que a chamada foo()não tem efeitos colaterais observáveis.
Max Langhof
1
Quando eu pego o link fornecido pelo Compiler Explorer e marco a opção "Compilar para binário e desmontar a saída", ele é compilado repentinamente xor eax,eax, embora sem essa opção chame a função de comparação de cadeias. Não tenho ideia do que fazer com isso.
Daniel H

Respostas:

3

Isso vai surpreender ainda mais a sua cabeça: o que acontece se criarmos um tipo de caractere personalizado MyCharTe o usarmos para criar nosso próprio costume 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";
}

Certamente, um tipo personalizado não pode ser otimizado mais facilmente do que um simples char , certo?

Aqui está a montagem resultante:

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

Veja ao vivo!


Mindblown!

Então, qual é a diferença entre o meu tipo de string "personalizado" e std::string?

Otimização de pequenas cadeias

Pelo menos no GCC, a Small String Optimization é realmente compilada no binário do libstdc ++. Isso significa que, durante a compilação de sua função, o compilador não tem acesso a essa implementação, portanto, não pode saber se há efeitos colaterais. Por esse motivo, não é possível otimizar a chamada para compare(char const*)ausência. Nossa classe "personalizada" não tem esse problema porque o SSO é implementado apenas para simples std::string.

BTW, se você compilar -std=c++2a, o compilador o otimiza . Infelizmente, ainda não sou experiente o suficiente em C ++ 20 para saber quais mudanças tornaram isso possível.

Cássio Renan
fonte