¿Cómo es exactamente std :: string_view más rápido que const std :: string &?

221

std::string_viewha llegado a C ++ 17 y se recomienda ampliamente usarlo en lugar de const std::string&.

Una de las razones es el rendimiento.

¿Alguien puede explicar qué tan exactamente std::string_view es / será más rápido que const std::string&cuando se usa como un tipo de parámetro? (supongamos que no se hacen copias en la persona que llama)

Patryk
fuente
77
std::string_viewes solo una abstracción del par (char * begin, char * end). Lo usa al hacer una std::stringcopia innecesaria.
Pregunta
En mi opinión, la pregunta no es exactamente cuál es más rápido, sino cuándo usarlos. Si necesito alguna manipulación en la cadena y no es permanente y / o mantiene el valor original, string_view es perfecto porque no necesito hacer una copia de la cadena. Pero si solo necesito verificar algo en una cadena usando string :: find por ejemplo, entonces la referencia es mejor.
TheArquitect
@QuestionC lo usa cuando no desea que su API se restrinja a std::string(string_view puede aceptar matrices sin procesar, vectores, std::basic_string<>con asignadores no predeterminados, etc., etc., etc. Ah, y otras string_views obviamente)
sehe

Respuestas:

213

std::string_view Es más rápido en algunos casos.

Primero, std::string const&requiere que los datos estén en una std::stringmatriz C sin procesar, una char const*devuelta por una API C, std::vector<char>producida por algún motor de deserialización, etc. La conversión de formato evitada evita copiar bytes y (si la cadena es más larga que SBO¹ para la std::stringimplementación particular ) evita una asignación de memoria.

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] );
}

No se realizan asignaciones en el string_viewcaso, pero las habría si se footomara una en std::string const&lugar de una string_view.

La segunda razón realmente importante es que permite trabajar con subcadenas sin una copia. Supongamos que está analizando una cadena json de 2 gigabytes (!) ². Si lo analiza std::string, cada uno de estos nodos de análisis donde almacenan el nombre o el valor de un nodo copia los datos originales de la cadena de 2 gb a un nodo local.

En cambio, si lo analiza en std::string_views, los nodos hacen referencia a los datos originales. Esto puede ahorrar millones de asignaciones y reducir a la mitad los requisitos de memoria durante el análisis.

La aceleración que puedes obtener es simplemente ridícula.

Este es un caso extremo, pero otros casos de "obtener una subcadena y trabajar con él" también pueden generar aceleraciones decentes string_view.

Una parte importante de la decisión es lo que pierde con el uso std::string_view. No es mucho, pero es algo.

Pierdes la terminación nula implícita, y eso es todo. Entonces, si la misma cadena se pasará a 3 funciones, todas las cuales requieren un terminador nulo, la conversión a std::stringuna vez puede ser sabio. Por lo tanto, si se sabe que su código necesita un terminador nulo, y no espera cadenas alimentadas desde buffers de fuente de estilo C o similares, tal vez tome un std::string const&. De lo contrario, tome un std::string_view.

Si std::string_viewtuviera una bandera que indicara si fue anulada (o algo más elegante), eliminaría incluso esa última razón para usar a std::string const&.

Hay un caso donde tomar un std::stringcon no const&es óptimo sobre un std::string_view. Si necesita poseer una copia de la cadena de forma indefinida después de la llamada, la toma por valor es eficiente. Usted estará en el caso de SBO (y sin asignaciones, solo unas pocas copias de caracteres para duplicarlo), o podrá mover el búfer asignado por el montón a un local std::string. Tener dos sobrecargas std::string&&y std::string_viewpodría ser más rápido, pero solo marginalmente, y causaría una modesta hinchazón de código (lo que podría costarle todas las ganancias de velocidad).


¹ Optimización de búfer pequeño

² Caso de uso real.

Yakk - Adam Nevraumont
fuente
8
También pierdes la propiedad. Lo cual solo es de interés si se devuelve la cadena y podría tener que ser algo más que una subcadena de un búfer que se garantiza que sobrevivirá el tiempo suficiente. En realidad, la pérdida de propiedad es un arma de dos filos.
Deduplicador
SBO suena extraño. Siempre he escuchado SSO (optimización de cadenas pequeñas)
phuclv
@phu Claro; pero las cadenas no son lo único en lo que usas el truco.
Yakk - Adam Nevraumont el
@phuclv SSO es solo un caso específico de SBO, que significa pequeña optimización del búfer . Los términos alternativos son optar por datos pequeños. , objeto pequeño opt. , o tamaño pequeño opt. .
Daniel Langr
59

Una forma en que string_view mejora el rendimiento es que permite eliminar prefijos y sufijos fácilmente. Debajo del capó, string_view puede simplemente agregar el tamaño del prefijo a un puntero a algún búfer de cadena, o restar el tamaño del sufijo del contador de bytes, esto generalmente es rápido. std :: string por otro lado tiene que copiar sus bytes cuando haces algo como substr (de esta manera obtienes una nueva cadena que posee su búfer, pero en muchos casos solo quieres obtener parte de la cadena original sin copiar). Ejemplo:

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

Con std :: string_view:

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

Actualizar:

Escribí un punto de referencia muy simple para agregar algunos números reales. Utilicé la impresionante biblioteca de Google benchmark . Las funciones comparadas son:

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
fuente
2
Es genial que hayas proporcionado un punto de referencia real. Esto realmente muestra lo que se puede ganar en casos de uso relevantes.
Daniel Kamil Kozar
1
@DanielKamilKozar Gracias por los comentarios. También creo que los puntos de referencia son valiosos, a veces cambian todo.
Pavel Davydov
47

Hay 2 razones principales:

  • string_view es un segmento en un búfer existente, no requiere una asignación de memoria
  • string_view se pasa por valor, no por referencia

Las ventajas de tener un corte son múltiples:

  • puedes usarlo con char const*o char[]sin asignar un nuevo buffer
  • puede tomar múltiples rebanadas y sublices en un búfer existente sin asignar
  • la subcadena es O (1), no O (N)
  • ...

Mejor y más consistente rendimiento en todo.


Pasar por valor también tiene ventajas sobre pasar por referencia, porque el aliasing.

Específicamente, cuando tienes un std::string const& parámetro, no hay garantía de que la cadena de referencia no se modifique. Como resultado, el compilador debe volver a buscar el contenido de la cadena después de cada llamada en un método opaco (puntero a datos, longitud, ...).

Por otro lado, al pasar un string_viewvalor por, el compilador puede determinar estáticamente que ningún otro código puede modificar la longitud y los punteros de datos ahora en la pila (o en los registros). Como resultado, puede "almacenarlos en caché" en llamadas a funciones.

Matthieu M.
fuente
36

Una cosa que puede hacer es evitar construir un std::stringobjeto en el caso de una conversión implícita de una cadena terminada en nulo:

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
fuente
12
Vale la pena decir que const std::string str{"goodbye!"}; foo(str);probablemente no será más rápido con string_view que con string &
Martin Bonner apoya a Monica el
1
¿No string_viewserá lento ya que tiene que copiar dos punteros en lugar de un puntero const string&?
balki
9

std::string_viewes básicamente un envoltorio alrededor de a const char*. Y pasar const char*significa que habrá un puntero menos en el sistema en comparación con pasar const string*(o const string&), porque string*implica algo como:

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

Claramente con el propósito de pasar argumentos constantes, el primer puntero es superfluo.

ps Una diferencia sustancial entre std::string_viewy const char*, sin embargo, es que no se requiere que las vistas_cadena estén terminadas en nulo (tienen un tamaño incorporado), y esto permite el empalme aleatorio en el lugar de cadenas más largas.

n.caillou
fuente
44
¿Qué pasa con los votos negativos? std::string_views son simplemente elegantes const char*, punto. GCC los implementa así:class basic_string_view {const _CharT* _M_str; size_t _M_len;}
n.caillou
44
solo llegue a 65K rep (de sus 65 actuales) y esta sería la respuesta aceptada (saluda a las multitudes de culto de carga) :)
mlvljr
77
@mlvljr Nadie pasa std::string const*. Y ese diagrama es ininteligible. @ n.caillou: su propio comentario ya es más preciso que la respuesta. Eso hace string_viewmás que "fantasía char const*": es realmente bastante obvio.
sehe
@sehe podría ser que nadie, no hay problema (es decir, pasar un puntero (o referencia) a una cadena constante, ¿por qué no?) :)
mlvljr
2
@sehe Lo entiendes desde una perspectiva de optimización o ejecución, std::string const*y std::string const&son lo mismo, ¿no?
n.caillou