Eu queria comparar as linhas de leitura de entrada de string do stdin usando Python e C ++ e fiquei chocado ao ver meu código C ++ executar uma ordem de magnitude mais lenta que o código Python equivalente. Como meu C ++ está enferrujado e ainda não sou um especialista em Python, me diga se estou fazendo algo errado ou se estou entendendo errado.
(Resposta do TLDR: inclua a declaração: cin.sync_with_stdio(false)
ou use apenas fgets
.
Resultados do TLDR: role até o final da minha pergunta e veja a tabela.)
Código C ++:
#include <iostream>
#include <time.h>
using namespace std;
int main() {
string input_line;
long line_count = 0;
time_t start = time(NULL);
int sec;
int lps;
while (cin) {
getline(cin, input_line);
if (!cin.eof())
line_count++;
};
sec = (int) time(NULL) - start;
cerr << "Read " << line_count << " lines in " << sec << " seconds.";
if (sec > 0) {
lps = line_count / sec;
cerr << " LPS: " << lps << endl;
} else
cerr << endl;
return 0;
}
// Compiled with:
// g++ -O3 -o readline_test_cpp foo.cpp
Equivalente em Python:
#!/usr/bin/env python
import time
import sys
count = 0
start = time.time()
for line in sys.stdin:
count += 1
delta_sec = int(time.time() - start_time)
if delta_sec >= 0:
lines_per_sec = int(round(count/delta_sec))
print("Read {0} lines in {1} seconds. LPS: {2}".format(count, delta_sec,
lines_per_sec))
Aqui estão os meus resultados:
$ cat test_lines | ./readline_test_cpp
Read 5570000 lines in 9 seconds. LPS: 618889
$cat test_lines | ./readline_test.py
Read 5570000 lines in 1 seconds. LPS: 5570000
Devo observar que tentei isso tanto no Mac OS X v10.6.8 (Snow Leopard) quanto no Linux 2.6.32 (Red Hat Linux 6.2). O primeiro é um MacBook Pro e o último é um servidor muito robusto, não que isso seja pertinente demais.
$ for i in {1..5}; do echo "Test run $i at `date`"; echo -n "CPP:"; cat test_lines | ./readline_test_cpp ; echo -n "Python:"; cat test_lines | ./readline_test.py ; done
Test run 1 at Mon Feb 20 21:29:28 EST 2012
CPP: Read 5570001 lines in 9 seconds. LPS: 618889
Python:Read 5570000 lines in 1 seconds. LPS: 5570000
Test run 2 at Mon Feb 20 21:29:39 EST 2012
CPP: Read 5570001 lines in 9 seconds. LPS: 618889
Python:Read 5570000 lines in 1 seconds. LPS: 5570000
Test run 3 at Mon Feb 20 21:29:50 EST 2012
CPP: Read 5570001 lines in 9 seconds. LPS: 618889
Python:Read 5570000 lines in 1 seconds. LPS: 5570000
Test run 4 at Mon Feb 20 21:30:01 EST 2012
CPP: Read 5570001 lines in 9 seconds. LPS: 618889
Python:Read 5570000 lines in 1 seconds. LPS: 5570000
Test run 5 at Mon Feb 20 21:30:11 EST 2012
CPP: Read 5570001 lines in 10 seconds. LPS: 557000
Python:Read 5570000 lines in 1 seconds. LPS: 5570000
Minúsculo adendo e resumo
Para garantir a integridade, pensei em atualizar a velocidade de leitura do mesmo arquivo na mesma caixa com o código C ++ original (sincronizado). Novamente, isso é para um arquivo de linha de 100 milhões em um disco rápido. Aqui está a comparação, com várias soluções / abordagens:
Implementation Lines per second
python (default) 3,571,428
cin (default/naive) 819,672
cin (no sync) 12,500,000
fgets 14,285,714
wc (not fair comparison) 54,644,808
<iostream>
desempenho é péssimo. Não é a primeira vez que isso acontece. 2) Python é inteligente o suficiente para não copiar os dados no loop for porque você não os usa. Você pode testar novamente tentando usarscanf
e achar[]
. Como alternativa, você pode tentar reescrever o loop para que algo seja feito com a string (por exemplo, mantenha a quinta letra e concatene-a em um resultado).cin.eof()
!! Coloque agetline
chamada na instrução 'if'.wc -l
é rápido porque lê o fluxo mais de uma linha por vez (pode ser umafread(stdin)/memchr('\n')
combinação). Resultados em Python estão na mesma ordem de magnitude, por exemplo,wc-l.py
Respostas:
Por padrão,
cin
é sincronizado com o stdio, o que evita qualquer buffer de entrada. Se você adicionar isso à parte superior do seu principal, verá um desempenho muito melhor:Normalmente, quando um fluxo de entrada é armazenado em buffer, em vez de ler um caractere de cada vez, o fluxo será lido em partes maiores. Isso reduz o número de chamadas do sistema, que geralmente são relativamente caras. No entanto, uma vez que a
FILE*
basestdio
eiostreams
implementações geralmente têm separações e, portanto, buffers separados, isso pode levar a um problema se ambas forem usadas juntas. Por exemplo:Se mais entradas foram lidas
cin
do que realmente eram necessárias, o segundo valor inteiro não estaria disponível para ascanf
função, que possui seu próprio buffer independente. Isso levaria a resultados inesperados.Para evitar isso, por padrão, os fluxos são sincronizados com
stdio
. Uma maneira comum de conseguir isso é tercin
lido cada caractere um de cada vez, conforme necessário, usandostdio
funções. Infelizmente, isso introduz muita sobrecarga. Para pequenas quantidades de entrada, isso não é um grande problema, mas quando você está lendo milhões de linhas, a penalidade de desempenho é significativa.Felizmente, os designers da biblioteca decidiram que você também deveria desabilitar esse recurso para obter um desempenho aprimorado, se soubesse o que estava fazendo, para que eles fornecessem o
sync_with_stdio
método.fonte
fscanf
chamada, porque isso simplesmente não funciona tanto quanto o Python. O Python deve alocar memória para a string, possivelmente várias vezes, pois a alocação existente é considerada inadequada - exatamente como a abordagem C ++std::string
. Essa tarefa é quase certamente vinculada à E / S e há muito FUD por aí sobre o custo de criarstd::string
objetos em C ++ ou usar<iostream>
por si só.sync_with_stdio()
é uma função membro estática e uma chamada para essa função em qualquer objeto de fluxo (por exemplocin
) ativa ou desativa a sincronização de todos os objetos iostream padrão.Só por curiosidade, dei uma olhada no que acontece sob o capô e usei dtruss / strace em cada teste.
C ++
syscalls
sudo dtruss -c ./a.out < in
Pitão
syscalls
sudo dtruss -c ./a.py < in
fonte
Estou aqui há alguns anos, mas:
Em 'Editar 4/5/6' da postagem original, você está usando a construção:
Isso está errado de duas maneiras diferentes:
Na verdade, você está cronometrando a execução
cat
, não sua referência. O uso da CPU 'user' e 'sys' exibido portime
são aqueles quecat
não são o seu programa de benchmarking. Pior ainda, o tempo 'real' também não é necessariamente preciso. Dependendo da implementaçãocat
e dos pipelines no sistema operacional local, é possível que elecat
grave um buffer gigante final e saia muito antes do processo do leitor concluir seu trabalho.O uso de
cat
é desnecessário e de fato contraproducente; você está adicionando peças móveis. Se você estivesse em um sistema suficientemente antigo (ou seja, com uma única CPU e - em algumas gerações de computadores - E / S mais rápida que a CPU) - o simples fato decat
estar em execução poderia colorir substancialmente os resultados. Você também está sujeito a qualquer buffer de entrada e saída e outro processamento quecat
possa fazer. (Isso provavelmente lhe renderá um prêmio 'Uso inútil de gato' se eu fosse Randal Schwartz.Uma construção melhor seria:
Nesta declaração, é o shell que abre o big_file, passando-o para o seu programa (bem, na verdade, para o
time
qual o programa é executado como um subprocesso) como um descritor de arquivo já aberto. 100% da leitura do arquivo é estritamente de responsabilidade do programa que você está tentando comparar. Isso permite uma leitura real de seu desempenho, sem complicações espúrias.Mencionarei duas 'correções' possíveis, mas realmente erradas, que também podem ser consideradas (mas as 'numero' de maneira diferente, pois essas não são coisas erradas no post original):
R. Você pode 'consertar' isso cronometrando apenas seu programa:
B. ou cronometrando todo o pipeline:
Eles estão errados pelos mesmos motivos do item 2: eles ainda estão sendo usados
cat
desnecessariamente. Eu os mencionei por alguns motivos:eles são mais "naturais" para pessoas que não estão totalmente confortáveis com os recursos de redirecionamento de E / S do shell POSIX
pode haver casos em que
cat
é necessário (por exemplo: o arquivo a ser lido requer algum tipo de privilégio de acesso, e você não deseja conceder esse privilégio para o programa a ser aferido:sudo cat /dev/sda | /usr/bin/time my_compression_test --no-output
)na prática , em máquinas modernas, o acréscimo
cat
no pipeline provavelmente não tem nenhuma consequência real.Mas digo a última coisa com alguma hesitação. Se examinarmos o último resultado em 'Editar 5' -
- isso afirma que
cat
consumiu 74% da CPU durante o teste; e de fato 1,34 / 1,83 é de aproximadamente 74%. Talvez uma série de:levaria apenas os restantes 0,49 segundos! Provavelmente não:
cat
aqui tinha que pagar pelasread()
chamadas do sistema (ou equivalente) que transferiam o arquivo do 'disco' (na verdade, cache de buffer), bem como o pipe grava para entregá-laswc
. O teste correto ainda teria que fazer aquelesread()
chamadas; apenas as chamadas de gravação para pipe e leitura de pipe seriam salvas, e essas devem ser bem baratas.Ainda assim, eu prevejo que você seria capaz de medir a diferença entre
cat file | wc -l
ewc -l < file
e encontrar um (percentagem de 2 dígitos) diferença notável. Cada um dos testes mais lentos terá pago uma penalidade semelhante em tempo absoluto; o que equivaleria a uma fração menor do seu tempo total maior.Na verdade, eu fiz alguns testes rápidos com um arquivo de lixo de 1,5 gigabyte, em um sistema Linux 3.13 (Ubuntu 14.04), obtendo esses resultados (esses são realmente os melhores resultados de 3); depois de preparar o cache, é claro):
Observe que os dois resultados do pipeline afirmam ter demorado mais tempo da CPU (usuário + sys) do que o tempo real do relógio de parede. Isso ocorre porque estou usando o comando 'time' interno do shell (bash), que é conhecedor do pipeline; e estou em uma máquina com vários núcleos, em que processos separados em um pipeline podem usar núcleos separados, acumulando o tempo da CPU mais rapidamente do que em tempo real. Usando
/usr/bin/time
vejo um tempo de CPU menor que o tempo real - mostrando que ele só pode cronometrar o único elemento do pipeline passado para ele em sua linha de comando. Além disso, a saída do shell fornece milissegundos, enquanto/usr/bin/time
apenas centésimos de segundo.Portanto, no nível de eficiência de
wc -l
, issocat
faz uma enorme diferença: 409/283 = 1,453 ou 45,3% mais em tempo real e 775/280 = 2,768, ou uma CPU 177% maior usada! Na minha caixa de teste aleatória, estava lá na hora.Devo acrescentar que há pelo menos uma outra diferença significativa entre esses estilos de teste e não posso dizer se é um benefício ou falha; você tem que decidir isso sozinho:
Quando você executa
cat big_file | /usr/bin/time my_program
, seu programa está recebendo informações de um canal, exatamente no ritmo enviado porcat
e em partes não maiores que as escritas porcat
.Quando você executa
/usr/bin/time my_program < big_file
, seu programa recebe um descritor de arquivo aberto para o arquivo real. Seu programa - ou em muitos casos as bibliotecas de E / S do idioma em que foi gravado - pode executar ações diferentes quando apresentado a um descritor de arquivo que faz referência a um arquivo regular. Pode ser usadommap(2)
para mapear o arquivo de entrada em seu espaço de endereço, em vez de usarread(2)
chamadas explícitas do sistema. Essas diferenças podem ter um efeito muito maior nos resultados de benchmark do que o pequeno custo de execução docat
binário.Obviamente, é um resultado de referência interessante se o mesmo programa tiver um desempenho significativamente diferente entre os dois casos. Isso mostra que, de fato, o programa ou suas bibliotecas de E / S estão fazendo algo interessante, como usar
mmap()
. Portanto, na prática, pode ser bom executar os benchmarks nos dois sentidos; talvez descontando ocat
resultado por algum pequeno fator para "perdoar" o custo dacat
própria execução .fonte
$ < big_file time my_program
$ time < big_file my_program
Isso deve funcionar em qualquer shell POSIX (ou seja, `` csh `e não tenho certeza sobre exóticas como` rc`:)time
está medindo todo o pipeline em vez do primeiro programa.time seq 2 | while read; do sleep 1; done
imprime 2 segundos,/usr/bin/time seq 2 | while read; do sleep 1; done
imprime 0 seg.Reproduzi o resultado original no meu computador usando o g ++ em um Mac.
Adicionando as seguintes instruções à versão C ++, pouco antes do
while
loop, alinhar com a versão Python :O sync_with_stdio aumentou a velocidade para 2 segundos e a configuração de um buffer maior reduziu para 1 segundo.
fonte
getline
operadores de stream,scanf
pode ser conveniente se você não se importar com o tempo de carregamento do arquivo ou se estiver carregando pequenos arquivos de texto. Mas, se o desempenho é algo com o qual você se preocupa, você realmente deve colocar o arquivo inteiro na memória (supondo que ele caiba).Aqui está um exemplo:
Se desejar, você pode agrupar um fluxo em torno desse buffer para um acesso mais conveniente como este:
Além disso, se você estiver no controle do arquivo, considere usar um formato de dados binários simples em vez de texto. É mais confiável ler e escrever, porque você não precisa lidar com todas as ambiguidades do espaço em branco. Também é menor e muito mais rápido para analisar.
fonte
O código a seguir foi mais rápido do que o outro código postado aqui até agora: (Visual Studio 2013, arquivo de 500 bits e 64 MB com comprimento de linha uniforme em [0, 1000)].
Ele supera todas as minhas tentativas de Python por mais de um fator 2.
fonte
read
syscalls sem buffer em um buffer estático de comprimentoBUFSIZE
ou através dosmmap
syscalls correspondentes equivalentes e, em seguida, passa pelo buffer contando novas linhas à lafor (char *cp = buf; *cp; cp++) count += *cp == "\n"
. Você precisará ajustarBUFSIZE
o sistema, o que o stdio já terá feito por você. Mas essefor
loop deve ser compilado para instruções em linguagem assembler incrivelmente rápidas e gritantes para o hardware da sua caixa.A propósito, a razão pela qual a contagem de linhas para a versão C ++ é maior que a contagem para a versão Python é que o sinalizador eof só é definido quando é feita uma tentativa de ler além do eof. Portanto, o loop correto seria:
fonte
while (getline(cin, input_line)) line_count++;
++line_count;
e nãoline_count++;
.long
, e o compilador é capaz de dizer que o resultado do incremento não é usado. Se ele não gerar código idêntico para pós-incremento e pré-incremento, está quebrado.++line_count;
em vez deline_count++;
não ferir :)while
, certo? Importaria se houvesse algum tipo de erro e você quisesse verificar seline_count
estava correto? Estou apenas adivinhando, mas não entendo por que isso importaria.No seu segundo exemplo (com scanf ()), o motivo pelo qual isso ainda é mais lento pode ser porque o scanf ("% s") analisa a sequência e procura por qualquer caractere de espaço (espaço, guia, nova linha).
Além disso, sim, o CPython faz cache para evitar leituras de disco rígido.
fonte
Um primeiro elemento de uma resposta:
<iostream>
é lento. Muito lento. Recebo um enorme aumento de desempenhoscanf
como no exemplo abaixo, mas ainda é duas vezes mais lento que o Python.fonte
Bem, eu vejo que na sua segunda solução você mudou
cin
parascanf
, qual foi a primeira sugestão que eu ia fazer (cin é sloooooooooooow). Agora, se você mudar descanf
parafgets
, verá outro aumento no desempenho:fgets
é a função C ++ mais rápida para entrada de string.BTW, não sabia sobre essa coisa de sincronização, legal. Mas você ainda deve tentar
fgets
.fonte
fgets
que estará errado (em termos de contagem de linhas e em termos de divisão de linhas entre os loops, se você realmente precisar usá-las) para linhas suficientemente grandes, sem verificações adicionais para linhas incompletas (e tentar compensar isso envolve alocar buffers desnecessariamente grandes , ondestd::getline
lida com a realocação para corresponder perfeitamente à entrada real). Rápido e errado é fácil, mas quase sempre vale a pena usar "um pouco mais devagar, mas correto", o quesync_with_stdio
é desativado .