¿Por qué dividir una cadena es más lento en C ++ que en Python?

93

Estoy tratando de convertir algo de código de Python a C ++ en un esfuerzo por ganar un poco de velocidad y agudizar mis oxidadas habilidades en C ++. Ayer me sorprendió cuando una implementación ingenua de leer líneas de stdin era mucho más rápida en Python que en C ++ (ver esto ). Hoy, finalmente descubrí cómo dividir una cadena en C ++ con delimitadores de fusión (semántica similar a split () de Python), ¡y ahora estoy experimentando un deja vu! Mi código C ++ tarda mucho más en hacer el trabajo (aunque no un orden de magnitud más, como fue el caso de la lección de ayer).

Código Python:

#!/usr/bin/env python
from __future__ import print_function                                            
import time
import sys

count = 0
start_time = time.time()
dummy = None

for line in sys.stdin:
    dummy = line.split()
    count += 1

delta_sec = int(time.time() - start_time)
print("Python: Saw {0} lines in {1} seconds. ".format(count, delta_sec), end='')
if delta_sec > 0:
    lps = int(count/delta_sec)
    print("  Crunch Speed: {0}".format(lps))
else:
    print('')

Código C ++:

#include <iostream>                                                              
#include <string>
#include <sstream>
#include <time.h>
#include <vector>

using namespace std;

void split1(vector<string> &tokens, const string &str,
        const string &delimiters = " ") {
    // Skip delimiters at beginning
    string::size_type lastPos = str.find_first_not_of(delimiters, 0);

    // Find first non-delimiter
    string::size_type pos = str.find_first_of(delimiters, lastPos);

    while (string::npos != pos || string::npos != lastPos) {
        // Found a token, add it to the vector
        tokens.push_back(str.substr(lastPos, pos - lastPos));
        // Skip delimiters
        lastPos = str.find_first_not_of(delimiters, pos);
        // Find next non-delimiter
        pos = str.find_first_of(delimiters, lastPos);
    }
}

void split2(vector<string> &tokens, const string &str, char delim=' ') {
    stringstream ss(str); //convert string to stream
    string item;
    while(getline(ss, item, delim)) {
        tokens.push_back(item); //add token to vector
    }
}

int main() {
    string input_line;
    vector<string> spline;
    long count = 0;
    int sec, lps;
    time_t start = time(NULL);

    cin.sync_with_stdio(false); //disable synchronous IO

    while(cin) {
        getline(cin, input_line);
        spline.clear(); //empty the vector for the next line to parse

        //I'm trying one of the two implementations, per compilation, obviously:
//        split1(spline, input_line);  
        split2(spline, input_line);

        count++;
    };

    count--; //subtract for final over-read
    sec = (int) time(NULL) - start;
    cerr << "C++   : Saw " << count << " lines in " << sec << " seconds." ;
    if (sec > 0) {
        lps = count / sec;
        cerr << "  Crunch speed: " << lps << endl;
    } else
        cerr << endl;
    return 0;

//compiled with: g++ -Wall -O3 -o split1 split_1.cpp

Tenga en cuenta que probé dos implementaciones divididas diferentes. Uno (split1) usa métodos de cadena para buscar tokens y es capaz de fusionar múltiples tokens, así como manejar numerosos tokens (viene de aquí ). El segundo (split2) usa getline para leer la cadena como una secuencia, no fusiona delimitadores y solo admite un único carácter delimitador (ese fue publicado por varios usuarios de StackOverflow en las respuestas a las preguntas de división de cadenas).

Ejecuté esto varias veces en varios órdenes. Mi máquina de prueba es una Macbook Pro (2011, 8GB, Quad Core), no es que importe mucho. Estoy probando con un archivo de texto de línea de 20M con tres columnas separadas por espacios que se parecen a esto: "foo.bar 127.0.0.1 home.foo.bar"

Resultados:

$ /usr/bin/time cat test_lines_double | ./split.py
       15.61 real         0.01 user         0.38 sys
Python: Saw 20000000 lines in 15 seconds.   Crunch Speed: 1333333
$ /usr/bin/time cat test_lines_double | ./split1
       23.50 real         0.01 user         0.46 sys
C++   : Saw 20000000 lines in 23 seconds.  Crunch speed: 869565
$ /usr/bin/time cat test_lines_double | ./split2
       44.69 real         0.02 user         0.62 sys
C++   : Saw 20000000 lines in 45 seconds.  Crunch speed: 444444

¿Qué estoy haciendo mal? ¿Hay una mejor manera de dividir cadenas en C ++ que no dependa de bibliotecas externas (es decir, sin impulso), que admita la combinación de secuencias de delimitadores (como la división de Python), sea segura para subprocesos (por lo tanto, no strtok) y cuyo rendimiento sea al menos a la par con Python?

Editar 1 / ¿Solución parcial ?:

Intenté hacer una comparación más justa haciendo que Python restableciera la lista ficticia y la agregara cada vez, como lo hace C ++. Esto todavía no es exactamente lo que está haciendo el código C ++, pero está un poco más cerca. Básicamente, el ciclo es ahora:

for line in sys.stdin:
    dummy = []
    dummy += line.split()
    count += 1

El rendimiento de Python ahora es aproximadamente el mismo que el de la implementación split1 de C ++.

/usr/bin/time cat test_lines_double | ./split5.py
       22.61 real         0.01 user         0.40 sys
Python: Saw 20000000 lines in 22 seconds.   Crunch Speed: 909090

Todavía me sorprende que, incluso si Python está tan optimizado para el procesamiento de cadenas (como sugirió Matt Joiner), estas implementaciones de C ++ no serían más rápidas. Si alguien tiene ideas sobre cómo hacer esto de una manera más óptima usando C ++, por favor comparta su código. (Creo que mi próximo paso será intentar implementar esto en C puro, aunque no voy a sacrificar la productividad del programador para volver a implementar mi proyecto general en C, por lo que este será solo un experimento para la velocidad de división de cadenas).

Gracias a todos por vuestra ayuda.

Edición / solución final:

Consulte la respuesta aceptada de Alf. Dado que Python trata con cadenas estrictamente por referencia y las cadenas STL a menudo se copian, el rendimiento es mejor con las implementaciones vanilla de Python. A modo de comparación, compilé y ejecuté mis datos a través del código de Alf, y aquí está el rendimiento en la misma máquina que todas las demás ejecuciones, esencialmente idéntico a la implementación de Python ingenua (aunque más rápida que la implementación de Python que restablece / agrega la lista, como mostrado en la edición anterior):

$ /usr/bin/time cat test_lines_double | ./split6
       15.09 real         0.01 user         0.45 sys
C++   : Saw 20000000 lines in 15 seconds.  Crunch speed: 1333333

Mi única queja restante es la cantidad de código necesario para que C ++ funcione en este caso.

Una de las lecciones aquí de este problema y del problema de lectura de la línea stdin de ayer (vinculado arriba) es que siempre se debe comparar en lugar de hacer suposiciones ingenuas sobre el rendimiento relativo "predeterminado" de los idiomas. Agradezco la educación.

¡Gracias nuevamente a todos por sus sugerencias!

JJC
fuente
2
¿Cómo compiló el programa C ++? ¿Tiene las optimizaciones activadas?
interjay
2
@interjay: Está en el último comentario en su fuente: g++ -Wall -O3 -o split1 split_1.cpp@JJC: ¿Cómo le va a su punto de referencia cuando realmente usa dummyy splinerespectivamente, tal vez Python elimine la llamada a line.split()porque no tiene efectos secundarios?
Eric
2
¿Qué resultados obtiene si elimina la división y deja solo las líneas de lectura de stdin?
interjay
2
Python está escrito en C. Significa que hay una forma eficiente de hacerlo, en C. ¿Quizás hay una mejor manera de dividir una cadena que usando STL?
ixe013

Respuestas:

57

Como conjetura, las cadenas de Python son cadenas inmutables contadas por referencia, de modo que no se copian cadenas en el código de Python, mientras que C ++ std::stringes un tipo de valor mutable y se copia en la menor oportunidad.

Si el objetivo es una división rápida, entonces se usarían operaciones de subcadena de tiempo constante, lo que significa que solo se hace referencia a partes de la cadena original, como en Python (y Java, y C #…).

Sin std::stringembargo, la clase C ++ tiene una característica redentora: es estándar , por lo que se puede usar para pasar cadenas de manera segura y portátil donde la eficiencia no es una consideración principal. Pero suficiente charla. Código, y en mi máquina esto es, por supuesto, más rápido que Python, ya que el manejo de cadenas de Python se implementa en C, que es un subconjunto de C ++ (he he):

#include <iostream>                                                              
#include <string>
#include <sstream>
#include <time.h>
#include <vector>

using namespace std;

class StringRef
{
private:
    char const*     begin_;
    int             size_;

public:
    int size() const { return size_; }
    char const* begin() const { return begin_; }
    char const* end() const { return begin_ + size_; }

    StringRef( char const* const begin, int const size )
        : begin_( begin )
        , size_( size )
    {}
};

vector<StringRef> split3( string const& str, char delimiter = ' ' )
{
    vector<StringRef>   result;

    enum State { inSpace, inToken };

    State state = inSpace;
    char const*     pTokenBegin = 0;    // Init to satisfy compiler.
    for( auto it = str.begin(); it != str.end(); ++it )
    {
        State const newState = (*it == delimiter? inSpace : inToken);
        if( newState != state )
        {
            switch( newState )
            {
            case inSpace:
                result.push_back( StringRef( pTokenBegin, &*it - pTokenBegin ) );
                break;
            case inToken:
                pTokenBegin = &*it;
            }
        }
        state = newState;
    }
    if( state == inToken )
    {
        result.push_back( StringRef( pTokenBegin, &*str.end() - pTokenBegin ) );
    }
    return result;
}

int main() {
    string input_line;
    vector<string> spline;
    long count = 0;
    int sec, lps;
    time_t start = time(NULL);

    cin.sync_with_stdio(false); //disable synchronous IO

    while(cin) {
        getline(cin, input_line);
        //spline.clear(); //empty the vector for the next line to parse

        //I'm trying one of the two implementations, per compilation, obviously:
//        split1(spline, input_line);  
        //split2(spline, input_line);

        vector<StringRef> const v = split3( input_line );
        count++;
    };

    count--; //subtract for final over-read
    sec = (int) time(NULL) - start;
    cerr << "C++   : Saw " << count << " lines in " << sec << " seconds." ;
    if (sec > 0) {
        lps = count / sec;
        cerr << "  Crunch speed: " << lps << endl;
    } else
        cerr << endl;
    return 0;
}

//compiled with: g++ -Wall -O3 -o split1 split_1.cpp -std=c++0x

Descargo de responsabilidad: espero que no haya errores. No he probado la funcionalidad, solo verifiqué la velocidad. Pero creo que, incluso si hay un error o dos, corregir eso no afectará significativamente la velocidad.

Saludos y hth. - Alf
fuente
2
Sí, las cadenas de Python son objetos contados por referencias, por lo que Python copia mucho menos. Aún contienen cadenas C terminadas en nulo bajo el capó, sin embargo, no pares (puntero, tamaño) como su código.
Fred Foo
13
En otras palabras, para un trabajo de nivel superior, como la manipulación de texto, apéguese a un lenguaje de nivel superior, donde decenas de desarrolladores se han esforzado por hacerlo de manera acumulativa durante decenas de años, o simplemente prepárese para trabajar tanto como todos esos desarrolladores. por tener algo comparable en nivel inferior.
jsbueno
2
@JJC: para el StringRef, puede copiar la subcadena a un std::stringmuy fácilmente, simplemente string( sr.begin(), sr.end() ).
Saludos y hth. - Alf
3
Ojalá las cadenas de CPython se copiaran menos. Sí, son referencias contadas e inmutables, pero str.split () asigna nuevas cadenas para cada elemento usando PyString_FromStringAndSize()esas llamadas PyObject_MALLOC(). Entonces, no hay optimización con una representación compartida que explote que las cadenas son inmutables en Python.
jfs
3
Mantenedores: por favor no introduzca errores al intentar corregir los errores percibidos (especialmente no con referencia a cplusplus.com ). TIA.
Saludos y hth. - Alf
9

No estoy proporcionando mejores soluciones (al menos en cuanto al rendimiento), pero algunos datos adicionales que podrían ser interesantes.

Usando strtok_r(variante reentrante de strtok):

void splitc1(vector<string> &tokens, const string &str,
        const string &delimiters = " ") {
    char *saveptr;
    char *cpy, *token;

    cpy = (char*)malloc(str.size() + 1);
    strcpy(cpy, str.c_str());

    for(token = strtok_r(cpy, delimiters.c_str(), &saveptr);
        token != NULL;
        token = strtok_r(NULL, delimiters.c_str(), &saveptr)) {
        tokens.push_back(string(token));
    }

    free(cpy);
}

Además, utilizando cadenas de caracteres para los parámetros y fgetspara la entrada:

void splitc2(vector<string> &tokens, const char *str,
        const char *delimiters) {
    char *saveptr;
    char *cpy, *token;

    cpy = (char*)malloc(strlen(str) + 1);
    strcpy(cpy, str);

    for(token = strtok_r(cpy, delimiters, &saveptr);
        token != NULL;
        token = strtok_r(NULL, delimiters, &saveptr)) {
        tokens.push_back(string(token));
    }

    free(cpy);
}

Y, en algunos casos, donde es aceptable destruir la cadena de entrada:

void splitc3(vector<string> &tokens, char *str,
        const char *delimiters) {
    char *saveptr;
    char *token;

    for(token = strtok_r(str, delimiters, &saveptr);
        token != NULL;
        token = strtok_r(NULL, delimiters, &saveptr)) {
        tokens.push_back(string(token));
    }
}

Los tiempos para estos son los siguientes (incluidos mis resultados para las otras variantes de la pregunta y la respuesta aceptada):

split1.cpp:  C++   : Saw 20000000 lines in 31 seconds.  Crunch speed: 645161
split2.cpp:  C++   : Saw 20000000 lines in 45 seconds.  Crunch speed: 444444
split.py:    Python: Saw 20000000 lines in 33 seconds.  Crunch Speed: 606060
split5.py:   Python: Saw 20000000 lines in 35 seconds.  Crunch Speed: 571428
split6.cpp:  C++   : Saw 20000000 lines in 18 seconds.  Crunch speed: 1111111

splitc1.cpp: C++   : Saw 20000000 lines in 27 seconds.  Crunch speed: 740740
splitc2.cpp: C++   : Saw 20000000 lines in 22 seconds.  Crunch speed: 909090
splitc3.cpp: C++   : Saw 20000000 lines in 20 seconds.  Crunch speed: 1000000

Como podemos ver, la solución de la respuesta aceptada sigue siendo la más rápida.

Para cualquiera que quiera hacer más pruebas, también puse un repositorio de Github con todos los programas de la pregunta, la respuesta aceptada, esta respuesta y, además, un Makefile y un script para generar datos de prueba: https: // github. com / tobbez / string-splitting .

tobbez
fuente
2
Hice una solicitud de extracción ( github.com/tobbez/string-splitting/pull/2 ) que hace que la prueba sea un poco más realista "usando" los datos (contando el número de palabras y caracteres). Con este cambio, todas las versiones de C / C ++ superan a las versiones de Python (excepto la que se basa en el tokenizador de Boost que agregué) y el valor real de los métodos basados ​​en la "vista de cadena" (como el de split6) brilla.
Dave Johansen
Debe usar memcpy, no strcpy, en caso de que el compilador no advierta esa optimización. strcpyPor lo general, utiliza una estrategia de inicio más lenta que logra un equilibrio entre rápido para cadenas cortas y aumento de SIMD completo para cadenas largas. memcpyconoce el tamaño de inmediato y no tiene que usar ningún truco SIMD para verificar el final de una cadena de longitud implícita. (No es gran cosa en x86 moderno). La creación de std::stringobjetos con el (char*, len)constructor también podría ser más rápido, si puede aprovechar eso saveptr-token. Obviamente, sería más rápido almacenar char*tokens: P
Peter Cordes
4

Sospecho que esto se debe a la forma en que std::vectorse cambia el tamaño durante el proceso de una llamada a la función push_back (). Si intenta usar std::listo std::vector::reserve()para reservar suficiente espacio para las oraciones, debería obtener un rendimiento mucho mejor. O puede usar una combinación de ambos como se muestra a continuación para split1 ():

void split1(vector<string> &tokens, const string &str,
        const string &delimiters = " ") {
    // Skip delimiters at beginning
    string::size_type lastPos = str.find_first_not_of(delimiters, 0);

    // Find first non-delimiter
    string::size_type pos = str.find_first_of(delimiters, lastPos);
    list<string> token_list;

    while (string::npos != pos || string::npos != lastPos) {
        // Found a token, add it to the list
        token_list.push_back(str.substr(lastPos, pos - lastPos));
        // Skip delimiters
        lastPos = str.find_first_not_of(delimiters, pos);
        // Find next non-delimiter
        pos = str.find_first_of(delimiters, lastPos);
    }
    tokens.assign(token_list.begin(), token_list.end());
}

EDITAR : La otra cosa obvia que veo es que la variable de Python dummyse asigna cada vez pero no se modifica. Por tanto, no es una comparación justa con C ++. Debería intentar modificar su código Python dummy = []para inicializarlo y luego hacerlo dummy += line.split(). ¿Puede informar el tiempo de ejecución después de esto?

EDIT2 : Para hacerlo aún más justo, puede modificar el ciclo while en el código C ++ para que sea:

    while(cin) {
        getline(cin, input_line);
        std::vector<string> spline; // create a new vector

        //I'm trying one of the two implementations, per compilation, obviously:
//        split1(spline, input_line);  
        split2(spline, input_line);

        count++;
    };
Vite Falcon
fuente
Gracias por la idea. Lo implementé y esta implementación es en realidad más lenta que la split1 original, desafortunadamente. También probé spline.reserve (16) antes del bucle, pero esto no tuvo ningún impacto en la velocidad de mi split1. Solo hay tres tokens por línea, y el vector se borra después de cada línea, por lo que no esperaba que eso ayudara mucho.
JJC
Probé tu edición también. Consulte la pregunta actualizada. El rendimiento ahora está a la par con split1.
JJC
Probé su EDIT2. El rendimiento fue un poco peor: $ / usr / bin / time cat test_lines_double | ./split7 33,39 real 0,01 usuario 0,49 sys C ++: Vio 20000000 líneas en 33 segundos. Velocidad de crunch: 606060
JJC
3

Creo que el siguiente código es mejor, usando algunas características de C ++ 17 y C ++ 14:

// These codes are un-tested when I write this post, but I'll test it
// When I'm free, and I sincerely welcome others to test and modify this
// code.

// C++17
#include <istream>     // For std::istream.
#include <string_view> // new feature in C++17, sizeof(std::string_view) == 16 in libc++ on my x86-64 debian 9.4 computer.
#include <string>
#include <utility>     // C++14 feature std::move.

template <template <class...> class Container, class Allocator>
void split1(Container<std::string_view, Allocator> &tokens, 
            std::string_view str,
            std::string_view delimiter = " ") 
{
    /* 
     * The model of the input string:
     *
     * (optional) delimiter | content | delimiter | content | delimiter| 
     * ... | delimiter | content 
     *
     * Using std::string::find_first_not_of or 
     * std::string_view::find_first_not_of is a bad idea, because it 
     * actually does the following thing:
     * 
     *     Finds the first character not equal to any of the characters 
     *     in the given character sequence.
     * 
     * Which means it does not treeat your delimiters as a whole, but as
     * a group of characters.
     * 
     * This has 2 effects:
     *
     *  1. When your delimiters is not a single character, this function
     *  won't behave as you predicted.
     *
     *  2. When your delimiters is just a single character, the function
     *  may have an additional overhead due to the fact that it has to 
     *  check every character with a range of characters, although 
     * there's only one, but in order to assure the correctness, it still 
     * has an inner loop, which adds to the overhead.
     *
     * So, as a solution, I wrote the following code.
     *
     * The code below will skip the first delimiter prefix.
     * However, if there's nothing between 2 delimiter, this code'll 
     * still treat as if there's sth. there.
     *
     * Note: 
     * Here I use C++ std version of substring search algorithm, but u
     * can change it to Boyer-Moore, KMP(takes additional memory), 
     * Rabin-Karp and other algorithm to speed your code.
     * 
     */

    // Establish the loop invariant 1.
    typename std::string_view::size_type 
        next, 
        delimiter_size = delimiter.size(),  
        pos = str.find(delimiter) ? 0 : delimiter_size;

    // The loop invariant:
    //  1. At pos, it is the content that should be saved.
    //  2. The next pos of delimiter is stored in next, which could be 0
    //  or std::string_view::npos.

    do {
        // Find the next delimiter, maintain loop invariant 2.
        next = str.find(delimiter, pos);

        // Found a token, add it to the vector
        tokens.push_back(str.substr(pos, next));

        // Skip delimiters, maintain the loop invariant 1.
        //
        // @ next is the size of the just pushed token.
        // Because when next == std::string_view::npos, the loop will
        // terminate, so it doesn't matter even if the following 
        // expression have undefined behavior due to the overflow of 
        // argument.
        pos = next + delimiter_size;
    } while(next != std::string_view::npos);
}   

template <template <class...> class Container, class traits, class Allocator2, class Allocator>
void split2(Container<std::basic_string<char, traits, Allocator2>, Allocator> &tokens, 
            std::istream &stream,
            char delimiter = ' ')
{
    std::string<char, traits, Allocator2> item;

    // Unfortunately, std::getline can only accept a single-character 
    // delimiter.
    while(std::getline(stream, item, delimiter))
        // Move item into token. I haven't checked whether item can be 
        // reused after being moved.
        tokens.push_back(std::move(item));
}

La elección del contenedor:

  1. std::vector.

    Suponiendo que el tamaño inicial de la matriz interna asignada es 1, y el tamaño final es N, asignará y desasignará para log2 (N) veces, y copiará (2 ^ (log2 (N) + 1) - 1) = (2N - 1) veces. Como se señaló en ¿El bajo rendimiento de std :: vector se debe a que no se llama a realloc un número logarítmico de veces? , esto puede tener un rendimiento deficiente cuando el tamaño del vector es impredecible y podría ser muy grande. Pero, si puede estimar el tamaño, esto será un problema menor.

  2. std::list.

    Para cada push_back, el tiempo que consume es una constante, pero probablemente lleve más tiempo que std :: vector en push_back individual. El uso de un grupo de memoria por subproceso y un asignador personalizado puede aliviar este problema.

  3. std::forward_list.

    Igual que std :: list, pero ocupa menos memoria por elemento. Requiere que funcione una clase contenedora debido a la falta de API push_back.

  4. std::array.

    Si puede conocer el límite de crecimiento, puede usar std :: array. Por supuesto, no puede usarlo directamente, ya que no tiene la API push_back. Pero puede definir un contenedor, y creo que es la forma más rápida aquí y puede ahorrar algo de memoria si su estimación es bastante precisa.

  5. std::deque.

    Esta opción le permite intercambiar memoria por rendimiento. No habrá (2 ^ (N + 1) - 1) veces la copia del elemento, solo N veces la asignación y no habrá desasignación. Además, tendrá un tiempo de acceso aleatorio constante y la capacidad de agregar nuevos elementos en ambos extremos.

Según std :: deque-cppreference

Por otro lado, los deques suelen tener un gran coste de memoria mínimo; una deque que contiene solo un elemento tiene que asignar su matriz interna completa (por ejemplo, 8 veces el tamaño del objeto en libstdc ++ de 64 bits; 16 veces el tamaño del objeto o 4096 bytes, el que sea mayor, en libc ++ de 64 bits)

o puede usar una combinación de estos:

  1. std::vector< std::array<T, 2 ^ M> >

    Esto es similar a std :: deque, la diferencia es que este contenedor no admite agregar elementos al frente. Pero aún tiene un rendimiento más rápido, debido al hecho de que no copiará la matriz std :: subyacente durante (2 ^ (N + 1) - 1) veces, solo copiará la matriz de punteros para (2 ^ (N - M + 1) - 1) veces, y asignando una nueva matriz solo cuando la corriente está llena y no necesita desasignar nada. Por cierto, puede obtener un tiempo de acceso aleatorio constante.

  2. std::list< std::array<T, ...> >

    Alivia enormemente la presión de la fractura de la memoria. Solo asignará una nueva matriz cuando la actual esté llena y no necesita copiar nada. Aún tendrá que pagar el precio de un puntero adicional en comparación con el combo 1.

  3. std::forward_list< std::array<T, ...> >

    Igual que 2, pero cuesta la misma memoria que el combo 1.

JiaHao Xu
fuente
Si usa std :: vector con un tamaño inicial razonable, como 128 o 256, el total de copias (asumiendo un factor de crecimiento de 2), evita cualquier copia para tamaños hasta ese límite. Luego, puede reducir la asignación para que se ajuste a la cantidad de elementos que realmente usó para que no sea terrible para pequeñas entradas. Sin embargo, esto no ayuda mucho con el número total de copias para el Ncaso muy grande . Es una lástima que std :: vector no pueda usar reallocpara permitir potencialmente mapear más páginas al final de la asignación actual , por lo que es aproximadamente 2 veces más lento.
Peter Cordes
¿Es stringview::remove_prefixtan económico como realizar un seguimiento de su posición actual en una cadena normal? std::basic_string::findtiene un segundo argumento opcional pos = 0que le permite comenzar a buscar desde un desplazamiento.
Peter Cordes
@ Peter Cordes Eso es correcto. Revisé impl libcxx
Jia Hao Xu
También verifiqué libstdc ++ impl , que es lo mismo.
JiaHao Xu
Su análisis del rendimiento del vector está desactivado. Considere un vector que tiene una capacidad inicial de 1 cuando lo inserta por primera vez y se duplica cada vez que necesita nueva capacidad. Si necesita ingresar 17 elementos, la primera asignación deja espacio para 1, luego 2, luego 4, luego 8, luego 16 y finalmente 32. Esto significa que hubo 6 asignaciones en total ( log2(size - 1) + 2utilizando el registro de números enteros). La primera asignación movió 0 cadenas, la segunda movió 1, luego 2, luego 4, luego 8, luego finalmente 16, para un total de 31 movimientos ( 2^(log2(size - 1) + 1) - 1)). Esto es O (n), no O (2 ^ n). Esto superará en gran medida std::list.
David Stone
2

Está asumiendo erróneamente que la implementación de C ++ elegida es necesariamente más rápida que la de Python. El manejo de cadenas en Python está altamente optimizado. Consulte esta pregunta para obtener más información: ¿Por qué las operaciones std :: string funcionan mal?

Matt Joiner
fuente
4
No estoy haciendo ninguna afirmación sobre el rendimiento general del lenguaje, solo sobre mi código en particular. Entonces, no hay suposiciones aquí. Gracias por el buen indicio de la otra pregunta. No estoy seguro si está diciendo que esta implementación en particular en C ++ es subóptima (su primera oración) o que C ++ es más lento que Python en el procesamiento de cadenas (su segunda oración). Además, si conoce una forma rápida de hacer lo que estoy tratando de hacer en C ++, compártala para beneficio de todos. Gracias. Solo para aclarar, me encanta Python, pero no soy un fanático ciego, por eso estoy tratando de aprender la forma más rápida de hacerlo.
JJC
1
@JJC: Dado que la implementación de Python es más rápida, diría que la suya no es óptima. Tenga en cuenta que las implementaciones del lenguaje pueden reducir sus costos, pero en última instancia, la complejidad algorítmica y las optimizaciones manuales ganan. En este caso, Python tiene la ventaja para este caso de uso de forma predeterminada.
Matt Joiner
2

Si toma la implementación de split1 y cambia la firma para que coincida más con la de split2, cambie esto:

void split1(vector<string> &tokens, const string &str, const string &delimiters = " ")

a esto:

void split1(vector<string> &tokens, const string &str, const char delimiters = ' ')

Obtiene una diferencia más dramática entre split1 y split2, y una comparación más justa:

split1  C++   : Saw 10000000 lines in 41 seconds.  Crunch speed: 243902
split2  C++   : Saw 10000000 lines in 144 seconds.  Crunch speed: 69444
split1' C++   : Saw 10000000 lines in 33 seconds.  Crunch speed: 303030
Paul Beckingham
fuente
1
void split5(vector<string> &tokens, const string &str, char delim=' ') {

    enum { do_token, do_delim } state = do_delim;
    int idx = 0, tok_start = 0;
    for (string::const_iterator it = str.begin() ; ; ++it, ++idx) {
        switch (state) {
            case do_token:
                if (it == str.end()) {
                    tokens.push_back (str.substr(tok_start, idx-tok_start));
                    return;
                }
                else if (*it == delim) {
                    state = do_delim;
                    tokens.push_back (str.substr(tok_start, idx-tok_start));
                }
                break;

            case do_delim:
                if (it == str.end()) {
                    return;
                }
                if (*it != delim) {
                    state = do_token;
                    tok_start = idx;
                }
                break;
        }
    }
}
norte. 'pronombres' m.
fuente
¡Gracias nm! Desafortunadamente, esto parece funcionar aproximadamente a la misma velocidad que la implementación original (división 1) en mi conjunto de datos y máquina: $ / usr / bin / time cat test_lines_double | ./split8 21,89 real 0,01 usuario 0,47 sys C ++: Vio 20000000 líneas en 22 segundos. Velocidad de crunch: 909090
JJC
En mi máquina: split1 - 54s, split.py - 35s, split5 - 16s. No tengo idea.
n. 'pronombres' m.
Hmm, ¿sus datos coinciden con el formato que anoté arriba? Supongo que ejecutó cada uno varias veces para eliminar los efectos transitorios, como la ocupación inicial de la memoria caché del disco.
JJC
0

Sospecho que esto está relacionado con el almacenamiento en búfer en sys.stdin en Python, pero sin almacenamiento en búfer en la implementación de C ++.

Consulte esta publicación para obtener detalles sobre cómo cambiar el tamaño del búfer, luego intente la comparación nuevamente: ¿ Establecer un tamaño de búfer más pequeño para sys.stdin?

Alex Collins
fuente
1
Hmmm ... no lo sigo. Solo leer líneas (sin la división) es más rápido en C ++ que en Python (después de incluir la línea cin.sync_with_stdio (false);). Ese fue el problema que tuve ayer, mencionado anteriormente.
JJC