Como exatamente std :: string_view é mais rápido que const std :: string &?

221

std::string_viewchegou ao C ++ 17 e é amplamente recomendado usá-lo em vez de const std::string&.

Uma das razões é o desempenho.

Alguém pode explicar como exatamente std::string_view será / será mais rápido do que const std::string&quando usado como um tipo de parâmetro? (vamos supor que não sejam feitas cópias no chamado)

Patryk
fonte
7
std::string_viewé apenas uma abstração do par (char * begin, char * end). Você o usa ao fazer uma std::stringcópia desnecessária.
QuestionC
Na minha opinião, a questão não é exatamente qual é mais rápido, mas quando usá-los. Se eu precisar de alguma manipulação na string e ela não for permanente e / ou manter o valor original, string_view será perfeito, pois não preciso fazer uma cópia da string. Mas se eu só precisar verificar algo na string usando string :: find, por exemplo, a referência é melhor.
TheArquitect
@QuestionC você usa quando não deseja que sua API restrinja std::string(string_view pode aceitar matrizes brutas, vetores, std::basic_string<>com alocadores não padrão etc. etc. etc. etc. Ah, e outras string_views obviamente)
consulte

Respostas:

213

std::string_view é mais rápido em alguns casos.

Primeiro, std::string const&exige que os dados estejam em uma std::stringmatriz C bruta, e não em uma bruta C, char const*retornada por uma API C, std::vector<char>produzida por algum mecanismo de desserialização etc. A conversão de formato evitada evita a cópia de bytes e (se a cadeia for maior que a SBO¹ para a std::stringimplementação específica ) evita uma alocação de memória.

void foo( std::string_view bob ) {
  std::cout << bob << "\n";
}
int main(int argc, char const*const* argv) {
  foo( "This is a string long enough to avoid the std::string SBO" );
  if (argc > 1)
    foo( argv[1] );
}

Nenhuma alocação é feita no string_viewcaso, mas haveria se fosse utilizado fooum em std::string const&vez de um string_view.

A segunda razão realmente grande é que ela permite trabalhar com substrings sem uma cópia. Suponha que você esteja analisando uma string json de 2 gigabytes (!) ². Se você analisá-lo std::string, cada nó de análise em que eles armazenam o nome ou o valor de um nó copia os dados originais da sequência de 2 gb para um nó local.

Em vez disso, se você analisá-lo como std::string_views, os nós se referem aos dados originais. Isso pode economizar milhões de alocações e reduzir pela metade os requisitos de memória durante a análise.

A aceleração que você pode obter é simplesmente ridícula.

Este é um caso extremo, mas outros casos "obtenha uma substring e trabalhe com ele" também podem gerar acelerações decentes com string_view .

Uma parte importante da decisão é o que você perde usando std::string_view . Não é muito, mas é alguma coisa.

Você perde rescisão nula implícita, e é isso. Portanto, se a mesma cadeia de caracteres for passada para 3 funções, todas as quais requerem um terminador nulo, a conversão para std::stringuma vez pode ser sensata. Portanto, se seu código precisar de um terminador nulo e você não espera que as sequências sejam alimentadas por buffers de origem C ou similares, talvez faça um std::string const&. Caso contrário, faça umastd::string_view .

Se std::string_viewhouvesse uma bandeira que declarasse que era nula encerrada (ou algo mais sofisticado), removeria até o último motivo para usar umstd::string const& .

Há um caso em que tirar um std::stringsem const&é ideal sobre um std::string_view. Se você precisar possuir uma cópia da sequência indefinidamente após a chamada, aceitar valores será eficiente. Você estará no caso do SBO (e não haverá alocações, apenas algumas cópias de caracteres para duplicá-lo) ou poderá mover o buffer alocado pelo heap para um local std::string. Com duas sobrecargas std::string&&e std::string_viewpode ser mais rápido, mas apenas marginalmente, e causaria um inchaço modesto no código (o que poderia custar todos os ganhos de velocidade).


¹ Otimização de buffer pequeno

² Caso de uso real.

Yakk - Adam Nevraumont
fonte
8
Você também perde a propriedade. O que é de interesse apenas se a string for retornada e pode ter que ser qualquer coisa além de uma sub-string de um buffer que garanta a sobrevivência por tempo suficiente. Na verdade, a perda de propriedade é uma arma de dois gumes.
Deduplicator
SBO parece estranho. Eu sempre ouvi SSO (pequena otimização string)
phuclv
@phu Claro; mas strings não são a única coisa em que você usa o truque.
Yakk - Adam Nevraumont 04/10/19
@phuclv SSO é apenas um caso específico de SBO, que significa otimização de buffer pequeno . Termos alternativos são pequenos dados opt. , objeto pequeno opt. ou tamanho pequeno, opt. .
Daniel Langr 24/04
59

Uma maneira de o string_view melhorar o desempenho é permitir a remoção de prefixos e sufixos facilmente. Sob o capô, string_view pode apenas adicionar o tamanho do prefixo a um ponteiro em algum buffer de string ou subtrair o tamanho do sufixo do contador de bytes, isso geralmente é rápido. Por outro lado, std :: string precisa copiar seus bytes quando você faz algo como substr (dessa forma, você obtém uma nova string que possui seu buffer, mas em muitos casos você deseja apenas obter parte da string original sem copiar). Exemplo:

std::string str{"foobar"};
auto bar = str.substr(3);
assert(bar == "bar");

Com std :: string_view:

std::string str{"foobar"};
std::string_view bar{str.c_str(), str.size()};
bar.remove_prefix(3);
assert(bar == "bar");

Atualizar:

Eu escrevi uma referência muito simples para adicionar alguns números reais. Eu usei uma incrível biblioteca de benchmark do Google . As funções comparadas são:

string remove_prefix(const string &str) {
  return str.substr(3);
}
string_view remove_prefix(string_view str) {
  str.remove_prefix(3);
  return str;
}
static void BM_remove_prefix_string(benchmark::State& state) {                
  std::string example{"asfaghdfgsghasfasg3423rfgasdg"};
  while (state.KeepRunning()) {
    auto res = remove_prefix(example);
    // auto res = remove_prefix(string_view(example)); for string_view
    if (res != "aghdfgsghasfasg3423rfgasdg") {
      throw std::runtime_error("bad op");
    }
  }
}
// BM_remove_prefix_string_view is similar, I skipped it to keep the post short

Resultados

(x86_64 linux, gcc 6.2, " -O3 -DNDEBUG"):

Benchmark                             Time           CPU Iterations
-------------------------------------------------------------------
BM_remove_prefix_string              90 ns         90 ns    7740626
BM_remove_prefix_string_view          6 ns          6 ns  120468514
Pavel Davydov
fonte
2
É ótimo que você tenha fornecido uma referência real. Isso realmente mostra o que pode ser obtido em casos de uso relevantes.
22616 Daniel Cassil Kozar
1
@DanielKamilKozar Obrigado pelo feedback. Eu também acho que os benchmarks são valiosos, às vezes eles mudam tudo.
Pavel Davydov
47

Existem 2 razões principais:

  • string_view é uma fatia em um buffer existente, não requer alocação de memória
  • string_view é passado por valor, não por referência

As vantagens de ter uma fatia são múltiplas:

  • você pode usá-lo com char const*ouchar[] sem alocar um novo buffer
  • você pode tirar várias fatias e sub-fatias para um buffer existente sem alocar
  • substring é O (1), não O (N)
  • ...

Desempenho melhor e mais consistente por todo o lado.


A passagem por valor também tem vantagens sobre a passagem por referência, porque é um alias.

Especificamente, quando você tem um std::string const& parâmetro, não há garantia de que a cadeia de referência não seja modificada. Como resultado, o compilador deve buscar novamente o conteúdo da sequência após cada chamada em um método opaco (ponteiro para dados, comprimento, ...).

Por outro lado, ao passar um string_viewvalor por, o compilador pode determinar estaticamente que nenhum outro código pode modificar o comprimento e os ponteiros de dados agora na pilha (ou nos registradores). Como resultado, ele pode "armazená-las" em cache em chamadas de função.

Matthieu M.
fonte
36

Uma coisa que ele pode fazer é evitar a construção de um std::stringobjeto no caso de uma conversão implícita a partir de uma sequência terminada nula:

void foo(const std::string& s);

...

foo("hello, world!"); // std::string object created, possible dynamic allocation.
char msg[] = "good morning!";
foo(msg); // std::string object created, possible dynamic allocation.
juanchopanza
fonte
12
Pode valer a pena dizer que const std::string str{"goodbye!"}; foo(str);, provavelmente, não vai ser qualquer mais rápido com string_view do que com corda &
Martin Bonner suporta Monica
1
Não string_viewserá lento, pois ele tem que copiar dois ponteiros em vez de um ponteiro const string&?
balki
9

std::string_viewé basicamente apenas um invólucro em torno de um const char*. E passar const char*significa que haverá um ponteiro a menos no sistema em comparação com passar const string*(ou const string&), porque string*implica algo como:

string* -> char* -> char[]
           |   string    |

Claramente, com a finalidade de transmitir argumentos const, o primeiro ponteiro é supérfluo.

ps Uma diferença substancial entre std::string_viewe const char*, no entanto, é que as string_views não precisam ter terminação nula (elas têm tamanho interno), e isso permite a emenda aleatória no local de seqüências mais longas.

n.caillou
fonte
4
O que há com os votos negativos? std::string_views são apenas const char*s chiques , ponto final. O GCC os implementa assim:class basic_string_view {const _CharT* _M_str; size_t _M_len;}
n.caillou
4
apenas chegar a 65 mil representante (a partir de sua atual 65) e esta seria a resposta aceita (acena para a multidão de carga e de culto) :)
mlvljr
7
@mlvljr Ninguém passa std::string const*. E esse diagrama é ininteligível. @ n.caillou: Seu próprio comentário já é mais preciso que a resposta. Isso faz string_viewmais do que "chique char const*" - é realmente bastante óbvio.
sehe
@sehe I pode ser que ninguém, não esquenta (ou seja, passar um ponteiro (ou de referência) para uma string const, por que não?) :)
mlvljr
2
@sehe Você entende isso de uma perspectiva de otimização ou execução std::string const*e std::string const&é o mesmo, não é?
N