Concatenación de cadenas eficiente en C ++

108

Escuché a algunas personas expresar preocupaciones sobre el operador "+" en std :: string y varias soluciones para acelerar la concatenación. ¿Alguno de estos es realmente necesario? Si es así, ¿cuál es la mejor manera de concatenar cadenas en C ++?

burlarse
fuente
13
Básicamente, + NO es un operador de concatenación (ya que genera una nueva cadena). Utilice + = para la concatenación.
Martin York
1
Desde C ++ 11, hay un punto importante: el operador + puede modificar uno de sus operandos y devolverlo por movimiento si ese operando fue pasado por la referencia rvalue. libstdc++ hace esto, por ejemplo . Entonces, cuando se llama a operator + con temporales, puede lograr un rendimiento casi tan bueno, tal vez un argumento a favor de no cumplirlo, en aras de la legibilidad, a menos que uno tenga puntos de referencia que muestren que es un cuello de botella. Sin embargo, una variada estandarizada append()sería óptima y legible ...
subrayado_d

Respuestas:

85

El trabajo adicional probablemente no valga la pena, a menos que realmente necesite eficiencia. Probablemente tendrá una eficiencia mucho mejor simplemente usando el operador + = en su lugar.

Ahora, después de ese descargo de responsabilidad, responderé a su pregunta real ...

La eficiencia de la clase de cadena STL depende de la implementación de STL que esté utilizando.

Puede garantizar la eficiencia y tener un mayor control si realiza la concatenación manualmente mediante las funciones integradas de c.

Por qué operator + no es eficiente:

Eche un vistazo a esta interfaz:

template <class charT, class traits, class Alloc>
basic_string<charT, traits, Alloc>
operator+(const basic_string<charT, traits, Alloc>& s1,
          const basic_string<charT, traits, Alloc>& s2)

Puede ver que se devuelve un nuevo objeto después de cada +. Eso significa que se utiliza un búfer nuevo cada vez. Si está haciendo un montón de operaciones adicionales, no es eficiente.

Por qué puede hacerlo más eficiente:

  • Estás garantizando eficiencia en lugar de confiar en que un delegado lo haga de manera eficiente por ti
  • la clase std :: string no sabe nada sobre el tamaño máximo de su cadena, ni la frecuencia con la que la concatena. Es posible que tenga este conocimiento y pueda hacer cosas basándose en tener esta información. Esto dará lugar a menos reasignaciones.
  • Controlará los búferes manualmente para asegurarse de que no copiará toda la cadena en búferes nuevos cuando no desee que eso suceda.
  • Puede usar la pila para sus búferes en lugar del montón, que es mucho más eficiente.
  • string + operator creará un nuevo objeto de cadena y lo devolverá usando un nuevo búfer.

Consideraciones para la implementación:

  • Mantenga un registro de la longitud de la cuerda.
  • Mantenga un puntero al final de la cadena y al inicio, o solo al inicio y use el inicio + la longitud como un desplazamiento para encontrar el final de la cadena.
  • Asegúrese de que el búfer en el que está almacenando su cadena sea lo suficientemente grande para que no necesite reasignar datos
  • Use strcpy en lugar de strcat para que no necesite iterar sobre la longitud de la cadena para encontrar el final de la cadena.

Estructura de datos de la cuerda:

Si necesita concatenaciones realmente rápidas, considere usar una estructura de datos de cuerdas .

Brian R. Bondy
fuente
6
Nota: "STL" se refiere a una biblioteca de código abierto completamente separada, originalmente de HP, parte de la cual se usó como base para partes de la Biblioteca C ++ del estándar ISO. "std :: string", sin embargo, nunca fue parte del STL de HP, por lo que es completamente incorrecto hacer referencia a "STL y" string "juntos.
James Curran
1
No diría que está mal usar STL y encadenar juntos. Ver sgi.com/tech/stl/table_of_contents.html
Brian R. Bondy
1
Cuando SGI se hizo cargo del mantenimiento del STL de HP, se ajustó para que coincidiera con la Biblioteca estándar (por eso dije "nunca forma parte del STL de HP"). Sin embargo, el creador de std :: string es el Comité ISO C ++.
James Curran
2
Nota al margen: El empleado de SGI que estuvo a cargo del mantenimiento de STL durante muchos años fue Matt Austern, quien, al mismo tiempo, encabezó el subgrupo de bibliotecas del Comité de Normalización ISO C ++.
James Curran
4
¿Puede aclarar o dar algunos puntos sobre por qué puede usar la pila para sus búferes en lugar del montón, que es mucho más eficiente? ? ¿De dónde proviene esta diferencia de eficiencia?
h7r
76

Reserve su espacio final antes, luego use el método de adición con un búfer. Por ejemplo, supongamos que espera que la longitud de la cadena final sea de 1 millón de caracteres:

std::string s;
s.reserve(1000000);

while (whatever)
{
  s.append(buf,len);
}
Carlos A. Ibarra
fuente
17

Yo no me preocuparía por eso. Si lo hace en un bucle, las cadenas siempre preasignarán memoria para minimizar las reasignaciones; solo úselas operator+=en ese caso. Y si lo haces manualmente, algo como esto o más

a + " : " + c

Luego está creando temporales, incluso si el compilador pudiera eliminar algunas copias del valor de retorno. Esto se debe a que en un llamado sucesivamente operator+no se sabe si el parámetro de referencia hace referencia a un objeto con nombre o un retorno temporal de una operator+subinvocación. Preferiría no preocuparme por eso antes de no haber perfilado primero. Pero tomemos un ejemplo para demostrarlo. Primero introducimos paréntesis para aclarar el enlace. Pongo los argumentos directamente después de la declaración de función que se usa para mayor claridad. Debajo de eso, muestro cuál es la expresión resultante:

((a + " : ") + c) 
calls string operator+(string const&, char const*)(a, " : ")
  => (tmp1 + c)

Ahora, en esa adición, tmp1 es lo que devolvió la primera llamada al operador + con los argumentos mostrados. Suponemos que el compilador es realmente inteligente y optimiza la copia del valor de retorno. Así que terminamos con una nueva cadena que contiene la concatenación de ay " : ". Ahora, esto sucede:

(tmp1 + c)
calls string operator+(string const&, string const&)(tmp1, c)
  => tmp2 == <end result>

Compare eso con lo siguiente:

std::string f = "hello";
(f + c)
calls string operator+(string const&, string const&)(f, c)
  => tmp1 == <end result>

¡Está usando la misma función para una cadena temporal y para una cadena con nombre! Entonces el compilador tiene que copiar el argumento en una nueva cadena y agregarlo y devolverlo desde el cuerpo de operator+. No puede tomar la memoria de un temporal y agregarlo. Cuanto más grande sea la expresión, más copias de cadenas deberán realizarse.

Siguiente Visual Studio y GCC admitirán la semántica de movimiento de c ++ 1x (complementando la semántica de copia ) y las referencias rvalue como una adición experimental. Eso permite averiguar si el parámetro hace referencia a un temporal o no. Esto hará que las adiciones sean increíblemente rápidas, ya que todo lo anterior terminará en una "tubería de adición" sin copias.

Si resulta ser un cuello de botella, aún puede hacerlo

 std::string(a).append(" : ").append(c) ...

Las appendllamadas añaden el argumento ay *thisluego devuelven una referencia a sí mismas. Por lo tanto, no se realiza ninguna copia de temporales allí. O alternativamente, operator+=se puede usar, pero necesitaría paréntesis feos para arreglar la precedencia.

Johannes Schaub - litb
fuente
Tuve que comprobar que los implementadores de stdlib realmente hacen esto. : P libstdc++de operator+(string const& lhs, string&& rhs)hace return std::move(rhs.insert(0, lhs)). Entonces, si ambos son temporales, operator+(string&& lhs, string&& rhs)si lhstiene suficiente capacidad disponible, lo hará directamente append(). Donde creo que esto corre el riesgo de ser más lento de lo que operator+=es si lhsno tiene suficiente capacidad, ya que luego retrocede rhs.insert(0, lhs), lo que no solo debe extender el búfer y agregar los nuevos contenidos como append(), sino que también debe desplazarse a lo largo del contenido original de rhsright.
underscore_d
La otra parte de la sobrecarga en comparación con operator+=es que operator+aún debe devolver un valor, por lo que tiene que move()depender del operando al que se anexó. Aún así, supongo que es una sobrecarga bastante menor (copiar un par de punteros / tamaños) en comparación con la copia profunda de toda la cadena, ¡así que es bueno!
underscore_d
11

Para la mayoría de las aplicaciones, simplemente no importa. Simplemente escriba su código, felizmente inconsciente de cómo funciona exactamente el operador +, y solo tome el asunto en sus propias manos si se convierte en un cuello de botella aparente.

pesto
fuente
7
Por supuesto, no vale la pena en la mayoría de los casos, pero esto realmente no responde a su pregunta.
Brian R. Bondy
1
Si. Estoy de acuerdo con solo decir "perfil y luego optimizar" se puede poner como comentario sobre la pregunta :)
Johannes Schaub - litb
6
Técnicamente, preguntó si estos son "necesarios". No lo son, y esto responde a esa pregunta.
Samantha Branham
Bastante justo, pero definitivamente es necesario para algunas aplicaciones. Entonces, en esas aplicaciones, la respuesta se reduce a: 'tome el asunto en sus propias manos'
Brian R. Bondy
4
@Pesto Existe una noción pervertida en el mundo de la programación de que el rendimiento no importa y podemos simplemente ignorar todo el asunto porque las computadoras son cada vez más rápidas. La cuestión es que no es por eso que la gente programa en C ++ y no es por eso que publican preguntas en el desbordamiento de pila sobre la concatenación eficiente de cadenas.
MrFox
7

A diferencia de .NET System.Strings, std :: strings de C ++ son mutables y, por lo tanto, se pueden construir mediante una simple concatenación tan rápido como con otros métodos.

James Curran
fuente
2
Especialmente si usa reserve () para hacer que el búfer sea lo suficientemente grande para el resultado antes de comenzar.
Mark Ransom
creo que está hablando de operador + =. también está concatenando, aunque es un caso degenerado. James era un mvp de vc ++, así que espero que tenga alguna pista de c ++: p
Johannes Schaub - litb
1
No dudo ni por un segundo que tiene un amplio conocimiento de C ++, solo que hubo un malentendido sobre la pregunta. La pregunta sobre la eficiencia del operador + que devuelve nuevos objetos de cadena cada vez que se llama y, por lo tanto, usa nuevos búferes de caracteres.
Brian R. Bondy
1
Si. pero luego preguntó por el operador de caso + es lento, cuál es la mejor manera de hacer una concatenación. y aquí entra en juego el operador + =. pero estoy de acuerdo en que la respuesta de James es un poco corta. hace que parezca que todos podríamos usar operator + y es
sumamente
@ BrianR.Bondy operator+no tiene que devolver una nueva cadena. Los implementadores pueden devolver uno de sus operandos, modificado, si ese operando fue pasado por la referencia rvalue.libstdc++ hace esto, por ejemplo . Por lo tanto, cuando se llama operator+con provisionales, puede lograr el mismo o casi el mismo rendimiento, lo que podría ser otro argumento a favor de no hacerlo, a menos que se tengan puntos de referencia que muestren que representa un cuello de botella.
underscore_d
4

En Imperfect C ++ , Matthew Wilson presenta un concatenador dinámico de cadenas que calcula previamente la longitud de la cadena final para tener solo una asignación antes de concatenar todas las partes. También podemos implementar un concatenador estático jugando con plantillas de expresión .

Ese tipo de idea se ha implementado en la implementación de STLport std :: string, que no se ajusta al estándar debido a este truco preciso.

Luc Hermitte
fuente
Glib::ustring::compose()de los enlaces glibmm a GLib hace eso: estima y reserve()s la longitud final basada en la cadena de formato proporcionada y los varargs, luego append()s cada uno (o su reemplazo formateado) en un bucle. Espero que esta sea una forma bastante común de trabajar.
underscore_d
4

std::string operator+asigna una nueva cadena y copia las dos cadenas de operandos cada vez. repite muchas veces y se vuelve caro, O (n).

std::string appendy, operator+=por otro lado, aumente la capacidad en un 50% cada vez que la cuerda necesite crecer. Lo que reduce significativamente el número de asignaciones de memoria y operaciones de copia, O (log n).

timmerov
fuente
No estoy muy seguro de por qué fue rechazado. La cifra del 50% no es requerida por el Estándar, pero IIRC que o el 100% son medidas comunes de crecimiento en la práctica. Todo lo demás en esta respuesta parece inobjetable.
underscore_d
Meses después, supongo que no es tan preciso, ya que se escribió mucho después del debut de C ++ 11, y las sobrecargas de operator+dónde se pasa uno o ambos argumentos mediante la referencia rvalue pueden evitar la asignación de una nueva cadena por completo concatenando en el búfer existente de uno de los operandos (aunque es posible que tengan que reasignarlo si no tiene capacidad suficiente).
underscore_d
2

Para cuerdas pequeñas, no importa. Si tiene cadenas grandes, será mejor que las almacene como están en vector o en alguna otra colección como partes. Y adapte su algoritmo para trabajar con ese conjunto de datos en lugar de una cadena grande.

Prefiero std :: ostringstream para la concatenación compleja.

Mykola Golubyev
fuente
2

Como ocurre con la mayoría de las cosas, es más fácil no hacer algo que hacerlo.

Si desea generar cadenas grandes en una GUI, puede ser que lo que sea que esté generando pueda manejar las cadenas en partes mejor que como una cadena grande (por ejemplo, concatenando texto en un editor de texto; por lo general, mantienen las líneas separadas estructuras).

Si desea exportar a un archivo, transmita los datos en lugar de crear una cadena grande y generarla.

Nunca he encontrado la necesidad de hacer que la concatenación sea más rápida si eliminé la concatenación innecesaria del código lento.

Pete Kirkham
fuente
2

Probablemente el mejor rendimiento si preasigna (reserva) espacio en la cadena resultante.

template<typename... Args>
std::string concat(Args const&... args)
{
    size_t len = 0;
    for (auto s : {args...})  len += strlen(s);

    std::string result;
    result.reserve(len);    // <--- preallocate result
    for (auto s : {args...})  result += s;
    return result;
}

Uso:

std::string merged = concat("This ", "is ", "a ", "test!");
LanDenLabs
fuente
0

Una matriz simple de caracteres, encapsulada en una clase que realiza un seguimiento del tamaño de la matriz y el número de bytes asignados es la más rápida.

El truco consiste en hacer solo una gran asignación al principio.

a

https://github.com/pedro-vicente/table-string

Benchmarks

Para Visual Studio 2015, compilación de depuración x86, mejora sustancial sobre C ++ std :: string.

| API                   | Seconds           
| ----------------------|----| 
| SDS                   | 19 |  
| std::string           | 11 |  
| std::string (reserve) | 9  |  
| table_str_t           | 1  |  
Pedro Vicente
fuente
1
El OP está interesado en cómo concatenar de manera eficiente std::string. No están pidiendo una clase de cadena alternativa.
underscore_d
0

Puede probar este con reservas de memoria para cada elemento:

namespace {
template<class C>
constexpr auto size(const C& c) -> decltype(c.size()) {
  return static_cast<std::size_t>(c.size());
}

constexpr std::size_t size(const char* string) {
  std::size_t size = 0;
  while (*(string + size) != '\0') {
    ++size;
  }
  return size;
}

template<class T, std::size_t N>
constexpr std::size_t size(const T (&)[N]) noexcept {
  return N;
}
}

template<typename... Args>
std::string concatStrings(Args&&... args) {
  auto s = (size(args) + ...);
  std::string result;
  result.reserve(s);
  return (result.append(std::forward<Args>(args)), ...);
}
voltento
fuente