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*
ochar[]
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.