Por que dividir uma string é mais lento em C ++ do que em Python?

93

Estou tentando converter alguns códigos de Python para C ++ em um esforço para ganhar um pouco de velocidade e aprimorar minhas habilidades enferrujadas de C ++. Ontem fiquei chocado quando uma implementação ingênua de ler linhas de stdin foi muito mais rápida em Python do que C ++ (veja isto ). Hoje, finalmente descobri como dividir uma string em C ++ com delimitadores de mesclagem (semântica semelhante ao split () do python), e agora estou experimentando um déjà vu! Meu código C ++ leva muito mais tempo para fazer o trabalho (embora não uma ordem de magnitude a mais, como foi o caso da lição de ontem).

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

Observe que tentei duas implementações de divisão diferentes. One (split1) usa métodos de string para pesquisar tokens e é capaz de mesclar vários tokens, bem como lidar com vários tokens (vem a partir daqui ). O segundo (split2) usa getline para ler a string como um stream, não mescla delimitadores e só oferece suporte a um único caractere delimitador (aquele foi postado por vários usuários do StackOverflow em respostas a perguntas sobre divisão de string).

Eu executei isso várias vezes em vários pedidos. Minha máquina de teste é um Macbook Pro (2011, 8GB, Quad Core), não que isso importe muito. Estou testando com um arquivo de texto de 20 milhões de linhas com três colunas separadas por espaço, cada uma semelhante a esta: "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

O que estou fazendo de errado? Existe uma maneira melhor de fazer a divisão de string em C ++ que não dependa de bibliotecas externas (ou seja, sem aumento), suporte a mesclagem de sequências de delimitadores (como a divisão de python), seja thread-safe (portanto, não use strtok) e cujo desempenho seja pelo menos no mesmo nível de python?

Editar 1 / Solução parcial ?:

Tentei fazer uma comparação mais justa fazendo com que o python redefinisse a lista fictícia e acrescentasse a ela todas as vezes, como o C ++ faz. Isso ainda não é exatamente o que o código C ++ está fazendo, mas é um pouco mais próximo. Basicamente, o loop é agora:

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

O desempenho do python agora é quase o mesmo que a implementação split1 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

Ainda estou surpreso de que, mesmo que o Python seja tão otimizado para o processamento de strings (como sugeriu Matt Joiner), essas implementações C ++ não seriam mais rápidas. Se alguém tiver ideias sobre como fazer isso de maneira otimizada usando C ++, compartilhe seu código. (Acho que minha próxima etapa será tentar implementar isso em C puro, embora não vá trocar a produtividade do programador para reimplementar meu projeto geral em C, então este será apenas um experimento para velocidade de divisão de string.)

Obrigado a todos por sua ajuda.

Edição / solução final:

Por favor, veja a resposta aceita de Alf. Visto que o python lida com strings estritamente por referência e strings STL são frequentemente copiadas, o desempenho é melhor com implementações vanilla python. Para comparação, eu compilei e executei meus dados através do código de Alf, e aqui está o desempenho na mesma máquina de todas as outras execuções, essencialmente idêntico à implementação python ingênua (embora mais rápida do que a implementação python que redefine / acrescenta a lista, como mostrado na edição acima):

$ /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

Minha única queixa restante é em relação à quantidade de código necessária para fazer o C ++ funcionar neste caso.

Uma das lições aqui tiradas deste problema e do problema de leitura da linha stdin de ontem (link acima) é que sempre se deve avaliar em vez de fazer suposições ingênuas sobre o desempenho "padrão" relativo das linguagens. Agradeço a educação.

Obrigado novamente a todos por suas sugestões!

JJC
fonte
2
Como você compilou o programa C ++? Você tem otimizações ativadas?
intervalo de
2
@interjay: Está no último comentário de sua fonte: g++ -Wall -O3 -o split1 split_1.cpp@JJC: Como seu benchmark se sai quando você realmente usa dummye spline, respectivamente, talvez o Python remova a chamada para line.split()porque não tem efeitos colaterais?
Eric
2
Que resultados você obtém se remover a divisão e deixar apenas as linhas de leitura de stdin?
intervalo de
2
Python é escrito em C. Isso significa que há uma maneira eficiente de fazer isso, em C. Talvez haja uma maneira melhor de dividir uma string do que usar STL?
ixe013
3
possível duplicata de Por que as operações std :: string funcionam mal?
Matt Joiner de

Respostas:

57

Como suposição, as strings Python são strings imutáveis ​​contadas por referência, de forma que nenhuma string é copiada no código Python, enquanto C ++ std::stringé um tipo de valor mutável e é copiado na menor oportunidade.

Se o objetivo for a divisão rápida, então usaremos operações de substring de tempo constante, o que significa apenas referir - se a partes da string original, como em Python (e Java e C # ...).

A std::stringclasse C ++ tem um recurso redentor, entretanto: é padrão , de modo que pode ser usada para passar strings com segurança e portabilidade onde a eficiência não é uma consideração principal. Mas chega de conversa. Código - e na minha máquina isso é, obviamente, mais rápido do que Python, uma vez que o tratamento de strings do Python é implementado em C, que é um 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

Aviso: Espero que não haja nenhum bug. Não testei a funcionalidade, apenas verifiquei a velocidade. Mas eu acho que, mesmo que haja um bug ou dois, corrigir isso não afetará significativamente a velocidade.

Saúde e hth. - Alf
fonte
2
Sim, as strings do Python são objetos contados por referência, então o Python faz muito menos cópias. Eles ainda contêm strings C terminadas em nulo sob o capô, embora, não pares (ponteiro, tamanho) como o seu código.
Fred Foo
13
Em outras palavras - para trabalho de nível superior, como manipulação de texto, use uma linguagem de nível superior, onde o esforço para fazê-lo com eficiência foi colocado cumulativamente por dezenas de desenvolvedores ao longo de dezenas de anos - ou apenas prepare-se para trabalhar tanto quanto todos os desenvolvedores por ter algo comparável em nível inferior.
jsbueno
2
@JJC: para o StringRef, você pode copiar a substring para um std::stringmuito facilmente string( sr.begin(), sr.end() ).
Saúde e hth. - Alf
3
Gostaria que as strings de CPython fossem menos copiadas. Sim, eles são contados por referência e imutáveis, mas str.split () aloca novas strings para cada item usando PyString_FromStringAndSize()essas chamadas PyObject_MALLOC(). Portanto, não há otimização com uma representação compartilhada que explora que as strings são imutáveis ​​em Python.
jfs de
3
Mantenedores: por favor, não introduza bugs tentando consertar bugs percebidos (especialmente não com referência a cplusplus.com ). TIA.
Saúde e hth. - Alf
9

Não estou fornecendo nenhuma solução melhor (pelo menos em termos de desempenho), mas alguns dados adicionais que podem ser interessantes.

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);
}

Além disso, usando cadeias de caracteres para parâmetros e fgets para 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);
}

E, em alguns casos, onde destruir a string de entrada é aceitável:

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));
    }
}

Os tempos para isso são os seguintes (incluindo meus resultados para as outras variantes da pergunta e a resposta aceita):

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, a solução da resposta aceita ainda é a mais rápida.

Para quem quiser fazer mais testes, também coloquei um repositório Github com todos os programas da pergunta, a resposta aceita, esta resposta e, adicionalmente, um Makefile e um script para gerar dados de teste: https: // github. com / tobbez / divisão de corda .

Tobbez
fonte
2
Fiz uma solicitação de pull ( github.com/tobbez/string-splitting/pull/2 ) que torna o teste um pouco mais realista ao "usar" os dados (contando o número de palavras e caracteres). Com essa mudança, todas as versões C / C ++ superaram as versões Python (espere aquela baseada no tokenizer de Boost que eu adicionei) e o valor real dos métodos baseados em "string view" (como o de split6) brilham.
Dave Johansen
Você deve usar memcpy, não strcpy, caso o compilador deixe de notar essa otimização. strcpynormalmente usa uma estratégia de inicialização mais lenta que atinge um equilíbrio entre rápido para sequências curtas e rampa até SIMD completo para sequências longas. memcpysabe o tamanho imediatamente e não precisa usar nenhum truque SIMD para verificar o final de uma string de comprimento implícito. (Não é grande coisa no x86 moderno). Criar std::stringobjetos com o (char*, len)construtor pode ser mais rápido também, se você conseguir fazer isso saveptr-token. Obviamente, seria mais rápido apenas armazenar char*tokens: P
Peter Cordes
4

Suspeito que isso seja devido à maneira como ele std::vectoré redimensionado durante o processo de uma chamada de função push_back (). Se você tentar usar std::listou std::vector::reserve()reservar espaço suficiente para as frases, terá um desempenho muito melhor. Ou você pode usar uma combinação de ambos como a seguir 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());
}

EDIT : A outra coisa óbvia que vejo é que a variável Python dummyé atribuída a cada vez, mas não modificada. Portanto, não é uma comparação justa com o C ++. Você deve tentar modificar seu código Python dummy = []para inicializá-lo e então fazer dummy += line.split(). Você pode relatar o tempo de execução depois disso?

EDIT2 : Para torná-lo ainda mais justo, você pode modificar o loop while no código C ++ para ser:

    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
fonte
Obrigado pela ideia. Eu implementei e essa implementação é realmente mais lenta do que o split1 original, infelizmente. Também tentei spline.reserve (16) antes do loop, mas isso não teve impacto na velocidade do meu split1. Existem apenas três tokens por linha, e o vetor é apagado após cada linha, então não esperava que isso ajudasse muito.
JJC de
Eu tentei sua edição também. Por favor, veja a pergunta atualizada. O desempenho agora está no mesmo nível do split1.
JJC de
Tentei seu EDIT2. O desempenho foi um pouco pior: $ / usr / bin / time cat test_lines_double | ./split7 33,39 real 0,01 usuário 0,49 sys C ++: viu 20.000.000 linhas em 33 segundos. Velocidade de compressão: 606060
JJC
3

Acho que o código a seguir é melhor, usando alguns recursos do C ++ 17 e 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));
}

A escolha do recipiente:

  1. std::vector.

    Assumindo que o tamanho inicial do array interno alocado é 1, e o tamanho final é N, você vai alocar e desalocar para log2 (N) vezes e copiar o (2 ^ (log2 (N) + 1) - 1) = (2N - 1) vezes. Conforme apontado em O baixo desempenho de std :: vector é devido a não chamar realloc um número logarítmico de vezes? , isso pode ter um desempenho ruim quando o tamanho do vetor é imprevisível e pode ser muito grande. Mas, se você puder estimar o tamanho dele, isso não será um problema.

  2. std::list.

    Para cada push_back, o tempo consumido é uma constante, mas provavelmente levará mais tempo do que std :: vector em push_back individual. Usar um pool de memória por thread e um alocador personalizado pode aliviar esse problema.

  3. std::forward_list.

    O mesmo que std :: list, mas ocupa menos memória por elemento. Requer que uma classe de wrapper funcione devido à falta de API push_back.

  4. std::array.

    Se você souber o limite de crescimento, poderá usar std :: array. Claro, você não pode usá-lo diretamente, uma vez que não tem o push_back da API. Mas você pode definir um wrapper, e acho que é o caminho mais rápido aqui e pode economizar alguma memória se sua estimativa for bastante precisa.

  5. std::deque.

    Esta opção permite que você troque memória por desempenho. Não haverá (2 ^ (N + 1) - 1) vezes a cópia do elemento, apenas N vezes a alocação e nenhuma desalocação. Além disso, você terá tempo de acesso aleatório constante e a capacidade de adicionar novos elementos em ambas as extremidades.

De acordo com std :: deque-cppreference

Por outro lado, os deques normalmente têm um grande custo mínimo de memória; um deque contendo apenas um elemento deve alocar seu array interno completo (por exemplo, 8 vezes o tamanho do objeto em libstdc ++ de 64 bits; 16 vezes o tamanho do objeto ou 4096 bytes, o que for maior, em libc ++ de 64 bits)

ou você pode usar a combinação destes:

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

    Isso é semelhante a std :: deque, a diferença é que esse contêiner não oferece suporte para adicionar elemento na frente. Mas ainda é mais rápido no desempenho, devido ao fato de que ele não copiará o std :: array subjacente por (2 ^ (N + 1) - 1) vezes, ele apenas copiará o array do ponteiro para (2 ^ (N - M + 1) - 1) vezes, e alocar nova matriz apenas quando a corrente estiver cheia e não precisar desalocar nada. A propósito, você pode obter tempo de acesso aleatório constante.

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

    Facilita muito a pressão da fragmentação da memória. Ele só alocará um novo array quando o atual estiver cheio e não precisa copiar nada. Você ainda terá que pagar o preço por um ponteiro adicional em comparação com o combo 1.

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

    O mesmo que 2, mas custa a mesma memória do combo 1.

JiaHao Xu
fonte
Se você usar std :: vector com algum tamanho inicial razoável, como 128 ou 256, o total de cópias (assumindo um fator de crescimento de 2), você evita qualquer cópia para tamanhos até esse limite. Você pode então reduzir a alocação para ajustar o número de elementos que você realmente usou, de forma que não seja terrível para pequenas entradas. Isso não ajuda muito com o número total de cópias para o Ncaso muito grande . É uma pena que o std :: vector não pode ser usado reallocpara permitir potencialmente o mapeamento de mais páginas no final da alocação atual , então é cerca de 2x mais lento.
Peter Cordes
É stringview::remove_prefixtão barato quanto apenas manter o controle de sua posição atual em uma string normal? std::basic_string::findtem um segundo argumento opcional pos = 0para permitir que você comece a pesquisar a partir de um deslocamento.
Peter Cordes
@ Peter Cordes Correto. Eu verifiquei libcxx impl
JiaHao Xu
Eu também verifiquei libstdc ++ impl , que é o mesmo.
JiaHao Xu
Sua análise do desempenho do vetor está desativada. Considere um vetor que tem uma capacidade inicial de 1 quando você insere pela primeira vez e que dobra toda vez que precisa de uma nova capacidade. Se você precisar incluir 17 itens, a primeira alocação abre espaço para 1, depois 2, 4, 8, 16 e finalmente 32. Isso significa que houve 6 alocações no total ( log2(size - 1) + 2usando o log de inteiros). A primeira alocação moveu 0 strings, a segunda moveu 1, depois 2, então 4, depois 8 e finalmente 16, para um total de 31 movimentos ( 2^(log2(size - 1) + 1) - 1)). Este é O (n), não O (2 ^ n). Isso terá um desempenho muito melhor std::list.
David Stone
2

Você está supondo erroneamente que a implementação C ++ escolhida é necessariamente mais rápida do que a do Python. O manuseio de strings em Python é altamente otimizado. Veja esta pergunta para mais informações: Por que as operações std :: string têm um desempenho ruim?

Matt Joiner
fonte
4
Não estou fazendo nenhuma afirmação sobre o desempenho geral da linguagem, apenas sobre meu código particular. Portanto, sem suposições aqui. Obrigado pela boa indicação para a outra pergunta. Não tenho certeza se você está dizendo que esta implementação particular em C ++ é subótima (sua primeira frase) ou que C ++ é apenas mais lento do que Python no processamento de string (sua segunda frase). Além disso, se você souber de uma maneira rápida de fazer o que estou tentando fazer em C ++, compartilhe-a para o benefício de todos. Obrigado. Só para esclarecer, adoro python, mas não sou um fanboy cego, por isso estou tentando aprender a maneira mais rápida de fazer isso.
JJC de
1
@JJC: Considerando que a implementação do Python é mais rápida, eu diria que a sua está abaixo do ideal. Lembre-se de que as implementações de linguagem podem cortar atalhos para você, mas, em última análise, a complexidade algorítmica e as otimizações manuais vencem. Nesse caso, Python tem a vantagem neste caso de uso por padrão.
Matt Joiner
2

Se você pegar a implementação da divisão1 e alterar a assinatura para corresponder mais à divisão2, alterando isto:

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

para isso:

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

Você obtém uma diferença mais dramática entre split1 e split2, e uma comparação mais 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
fonte
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;
        }
    }
}
n. 'pronomes' m.
fonte
Obrigado nm! Infelizmente, isso parece funcionar quase na mesma velocidade da implementação original (divisão 1) em meu conjunto de dados e máquina: $ / usr / bin / time cat test_lines_double | ./split8 21,89 real 0,01 usuário 0,47 sys C ++: viu 20.000.000 linhas em 22 segundos. Velocidade de compressão: 909090
JJC
Na minha máquina: split1 - 54s, split.py - 35s, split5 - 16s. Eu não faço ideia.
n. 'pronomes' m.
Hmm, seus dados correspondem ao formato que observei acima? Suponho que você executou cada várias vezes para eliminar efeitos transitórios, como população inicial do cache de disco.
JJC
0

Suspeito que isso esteja relacionado ao armazenamento em buffer em sys.stdin em Python, mas nenhum armazenamento em buffer na implementação de C ++.

Veja esta postagem para obter detalhes sobre como alterar o tamanho do buffer e, em seguida, tente a comparação novamente: Definindo um tamanho de buffer menor para sys.stdin?

Alex Collins
fonte
1
Hmmm ... eu não entendo. Apenas ler linhas (sem a divisão) é mais rápido em C ++ do que Python (depois de incluir a linha cin.sync_with_stdio (false);). Esse foi o problema que tive ontem, referenciado acima.
JJC de