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

Resuelto Patryk asked hace 7 años • 5 respuestas

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 cómo es/será exactamente más rápido que cuando se usa como tipo de parámetro? (supongamos que no se hacen copias en el destinatario de la llamada)std::string_viewconst std::string&

Patryk avatar Oct 19 '16 16:10 Patryk
Aceptado

std::string_viewes más rápido en algunos casos.

Primero, std::string const&requiere que los datos estén en un archivo std::string, y no en una matriz C sin formato, char const*devueltos por una API de C, std::vector<char>producidos por algún motor de deserialización, etc. La conversión de formato evitada evita la copia de bytes y (si la cadena es más larga que el 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 sí las habría si se footomara a std::string const&en lugar de a string_view.

La segunda razón realmente importante es que permite trabajar con subcadenas sin una copia. Suponga que está analizando una cadena json de 2 gigabytes (!)². Si lo analiza en std::string, cada uno de esos nodos de análisis donde almacenan el nombre o 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 conseguir es simplemente ridícula.

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

Una parte importante de la decisión es lo que se pierde al consumir std::string_view.

Primero pierdes la propiedad. string_viewes un puntero tonto a la memoria de otra persona. Seguimiento de eso es un dolor de cabeza. Por supuesto, lo mismo ocurre con a const&a a std::string.

En segundo lugar, perder 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, std::stringpuede ser aconsejable convertir a una vez. Por lo tanto, si se sabe que su código necesita un terminador nulo y no espera cadenas alimentadas desde buffers de estilo C o similares, tal vez tome un archivo std::string const&. De lo contrario, tome un std::string_view.

Si std::string_viewtuviera una bandera que indicara si tenía terminación nula (o algo más sofisticado), eliminaría incluso esa última razón para usar un archivo std::string const&.

Hay un caso en el que tomar a std::stringcon no const&es óptimo en lugar de a std::string_view. Si necesita poseer una copia de la cadena indefinidamente después de la llamada, tomar por valor es eficiente. Estará en el caso SBO (y sin asignaciones, solo unas pocas copias de caracteres para duplicarlo), o podrá mover el búfer asignado al montón a un archivo std::string. Tener dos sobrecargas std::string&&podría std::string_viewser más rápido, pero solo marginalmente, y causaría una modesta sobrecarga 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 avatar Oct 19 '2016 10:10 Yakk - Adam Nevraumont

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 suele ser 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 deseas 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 sumar algunos números reales. Utilicé la increíble biblioteca de referencia de Google . 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 avatar Oct 19 '2016 10:10 Pavel Davydov

Hay 2 razones principales:

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

Las ventajas de tener una loncha son múltiples:

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

Rendimiento mejor y más consistente en todas partes.


Pasar por valor también tiene ventajas sobre pasar por referencia, debido al alias.

Específicamente, cuando tiene 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 a un método opaco (puntero a datos, longitud, ...).

Por otro lado, al pasar un string_viewvalor by, 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é" entre llamadas a funciones.

Matthieu M. avatar Oct 19 '2016 11:10 Matthieu M.

Una cosa que puede hacer es evitar la construcción de 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 avatar Oct 19 '2016 09:10 juanchopanza