c++ string c++17 string-view

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



c++17 string-view (5)

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:

  • puede 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 tiene un parámetro std::string const& , 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_view por valor, 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.

std::string_view ha llegado a C ++ 17 y se recomienda su uso en lugar de const std::string& .

Una de las razones es el rendimiento.

¿Alguien puede explicar cómo 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)


Una cosa que puede hacer es evitar construir un objeto std::string 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.


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


std::string_view es básicamente un contenedor alrededor de un 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 Sin embargo, una diferencia sustancial entre std::string_view y const char* es que las string_views no tienen que estar terminadas en nulo (tienen un tamaño incorporado), y esto permite un empalme aleatorio en el lugar de cadenas más largas.


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

Primero, std::string const& requiere que los datos estén en std::string , y no en una matriz C sin procesar, un char const* devuelto por una API de C, un std::vector<char> producido 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 el SBO¹ para la implementación particular de std::string ) 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 caso string_view , pero habría si foo tomara std::string const& lugar de 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 en std::string , cada nodo de análisis en el que 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_view s, 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 con string_view .

Una parte importante de la decisión es lo que pierde al usar 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::string una vez puede ser sabio. Por lo tanto, si se sabe que su código necesita un terminador nulo, y no espera que las cadenas se alimenten de 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_view tuviera una bandera que indicara si fue terminada en nulo (o algo más elegante), eliminaría incluso esa última razón para usar un std::string const& .

Hay un caso en el que tomar un std::string sin 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 std::string local. Tener dos sobrecargas std::string&& y std::string_view podría ser más rápido, pero solo marginalmente, y causaría una modesta expansión del código (lo que podría costarle todas las ganancias de velocidad).

¹ Optimización de búfer pequeño

² Caso de uso real.