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 usa dummy
e spline
, respectivamente, talvez o Python remova a chamada para line.split()
porque não tem efeitos colaterais?