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!
g++ -Wall -O3 -o split1 split_1.cpp
@JJC: Como seu benchmark se sai quando você realmente usadummy
espline
, respectivamente, talvez o Python remova a chamada paraline.split()
porque não tem efeitos colaterais?Respostas:
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::string
classe 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):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.
fonte
StringRef
, você pode copiar a substring para umstd::string
muito facilmentestring( sr.begin(), sr.end() )
.PyString_FromStringAndSize()
essas chamadasPyObject_MALLOC()
. Portanto, não há otimização com uma representação compartilhada que explora que as strings são imutáveis em Python.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 destrtok
):Além disso, usando cadeias de caracteres para parâmetros e
fgets
para entrada:E, em alguns casos, onde destruir a string de entrada é aceitável:
Os tempos para isso são os seguintes (incluindo meus resultados para as outras variantes da pergunta e a resposta aceita):
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 .
fonte
memcpy
, nãostrcpy
, caso o compilador deixe de notar essa otimização.strcpy
normalmente 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.memcpy
sabe 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). Criarstd::string
objetos com o(char*, len)
construtor pode ser mais rápido também, se você conseguir fazer issosaveptr-token
. Obviamente, seria mais rápido apenas armazenarchar*
tokens: PSuspeito que isso seja devido à maneira como ele
std::vector
é redimensionado durante o processo de uma chamada de função push_back (). Se você tentar usarstd::list
oustd::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 ():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 Pythondummy = []
para inicializá-lo e então fazerdummy += 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:
fonte
Acho que o código a seguir é melhor, usando alguns recursos do C ++ 17 e C ++ 14:
A escolha do recipiente:
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.
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.
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.
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.
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
ou você pode usar a combinação destes:
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.
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.
std::forward_list< std::array<T, ...> >
O mesmo que 2, mas custa a mesma memória do combo 1.
fonte
N
caso muito grande . É uma pena que o std :: vector não pode ser usadorealloc
para permitir potencialmente o mapeamento de mais páginas no final da alocação atual , então é cerca de 2x mais lento.stringview::remove_prefix
tão barato quanto apenas manter o controle de sua posição atual em uma string normal?std::basic_string::find
tem um segundo argumento opcionalpos = 0
para permitir que você comece a pesquisar a partir de um deslocamento.log2(size - 1) + 2
usando 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 melhorstd::list
.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?
fonte
Se você pegar a implementação da divisão1 e alterar a assinatura para corresponder mais à divisão2, alterando isto:
para isso:
Você obtém uma diferença mais dramática entre split1 e split2, e uma comparação mais justa:
fonte
fonte
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?
fonte