Por que a impressão em stdout é tão lenta? Pode ser acelerado?

166

Sempre fiquei surpreso / frustrado com o tempo que leva para simplesmente enviar para o terminal com uma declaração de impressão. Após um registro lento e doloroso recente, decidi investigar e fiquei surpreso ao descobrir que quase todo o tempo gasto está aguardando o terminal processar os resultados.

A escrita no stdout pode ser acelerada de alguma forma?

Eu escrevi um script (' print_timer.py' na parte inferior desta pergunta) para comparar o tempo ao escrever 100 mil linhas no stdout, no arquivo e com o stdout redirecionado para/dev/null . Aqui está o resultado do tempo:

$ python print_timer.py
this is a test
this is a test
<snipped 99997 lines>
this is a test
-----
timing summary (100k lines each)
-----
print                         :11.950 s
write to file (+ fsync)       : 0.122 s
print with stdout = /dev/null : 0.050 s

Uau. Para garantir que o python não esteja fazendo algo nos bastidores, como reconhecer que eu redesignei o stdout para / dev / null ou algo assim, fiz o redirecionamento fora do script ...

$ python print_timer.py > /dev/null
-----
timing summary (100k lines each)
-----
print                         : 0.053 s
write to file (+fsync)        : 0.108 s
print with stdout = /dev/null : 0.045 s

Portanto, não é um truque de python, é apenas o terminal. Eu sempre soube que descarregar a saída para / dev / null acelerava as coisas, mas nunca imaginei que fosse tão significativo!

Me surpreende o quão lento o tty é. Como pode ser que gravar no disco físico é MUITO mais rápido do que gravar na "tela" (presumivelmente uma operação totalmente com RAM) e é tão rápido quanto simplesmente despejar no lixo com / dev / null?

Este link fala sobre como o terminal bloqueará a E / S para que ele possa "analisar [a entrada], atualizar seu buffer de quadros, se comunicar com o servidor X para rolar a janela e assim por diante" ... mas eu não entendi completamente. O que pode demorar tanto?

Espero que não haja saída (com exceção de uma implementação tty mais rápida?), Mas acho que perguntaria assim mesmo.


ATUALIZAÇÃO: depois de ler alguns comentários, me perguntei quanto impacto meu tamanho de tela realmente teria no tempo de impressão e isso tem algum significado. Os números realmente lentos acima estão com o meu terminal Gnome ampliado para 1920x1200. Se reduzi-lo muito pequeno, fico ...

-----
timing summary (100k lines each)
-----
print                         : 2.920 s
write to file (+fsync)        : 0.121 s
print with stdout = /dev/null : 0.048 s

Isso é certamente melhor (~ 4x), mas não muda minha pergunta. Isso só aumenta a minha pergunta, pois não entendo por que a renderização da tela do terminal deve diminuir a velocidade de um aplicativo gravando no stdout. Por que meu programa precisa aguardar a renderização da tela para continuar?

Todos os aplicativos terminal / tty não são criados iguais? Eu ainda tenho que experimentar. Parece-me realmente que um terminal deve ser capaz de armazenar em buffer todos os dados recebidos, analisá-los / renderizá-los invisivelmente e renderizar apenas o bloco mais recente que é visível na configuração atual da tela a uma taxa de quadros sensata. Portanto, se eu puder gravar + fsync no disco em ~ 0,1 segundos, um terminal poderá concluir a mesma operação em algo dessa ordem (com talvez algumas atualizações na tela enquanto o fazia).

Eu ainda espero que exista uma configuração tty que possa ser alterada do lado do aplicativo para melhorar esse comportamento para o programador. Se isso for estritamente um problema de aplicativo de terminal, talvez isso nem pertença ao StackOverflow?

o que estou perdendo?


Aqui está o programa python usado para gerar o tempo:

import time, sys, tty
import os

lineCount = 100000
line = "this is a test"
summary = ""

cmd = "print"
startTime_s = time.time()
for x in range(lineCount):
    print line
t = time.time() - startTime_s
summary += "%-30s:%6.3f s\n" % (cmd, t)

#Add a newline to match line outputs above...
line += "\n"

cmd = "write to file (+fsync)"
fp = file("out.txt", "w")
startTime_s = time.time()
for x in range(lineCount):
    fp.write(line)
os.fsync(fp.fileno())
t = time.time() - startTime_s
summary += "%-30s:%6.3f s\n" % (cmd, t)

cmd = "print with stdout = /dev/null"
sys.stdout = file(os.devnull, "w")
startTime_s = time.time()
for x in range(lineCount):
    fp.write(line)
t = time.time() - startTime_s
summary += "%-30s:%6.3f s\n" % (cmd, t)

print >> sys.stderr, "-----"
print >> sys.stderr, "timing summary (100k lines each)"
print >> sys.stderr, "-----"
print >> sys.stderr, summary
Russ
fonte
9
Todo o objetivo de escrever no stdout é para que um humano possa ler a saída. Nenhum ser humano no mundo pode ler 10.000 linhas de texto em 12 segundos, então qual é o objetivo de tornar o stdout mais rápido ???
Seun Osewa 5/10/10
14
@ Seun Osewa: Um exemplo (que levou minha pergunta) é ao fazer coisas como a depuração de instruções de impressão . Você deseja executar seu programa e ver os resultados à medida que eles acontecem. Você está obviamente certo de que a maioria das linhas voará por você que não pode ver, mas quando uma exceção acontece (ou você pressiona a instrução condicional getch / raw_input / sleep que você colocou com cuidado), deseja observar diretamente a saída de impressão em vez de constantemente tendo que abrir ou atualizar uma exibição de arquivo.
62610 Russ
3
A depuração de instrução de impressão é uma das razões pelas quais os dispositivos tty (por exemplo, terminais) padronizam o buffer de linha em vez do buffer de bloco: a saída de depuração não é muito útil se o programa travar e as últimas linhas de saída de depuração ainda estiverem em um buffer. em vez de liberado para o terminal.
Stephen C. Steel,
@ Stephen: É por isso que não me preocupei muito em buscar as enormes melhorias que um comentarista afirmou ao aumentar o tamanho do buffer. Derrota inteiramente o objetivo da impressão de depuração! Eu experimentei um pouco enquanto investigava, mas não vi nenhuma melhoria líquida. Ainda estou curioso sobre a discrepância, mas não realmente.
Russ
Às vezes, para programas em execução muito longa, imprimo a linha atual stdout a cada n segundos - semelhante a ter um atraso de atualização em um aplicativo de xingamentos. Não é perfeito, mas dá uma idéia do que estou fazendo de vez em quando.
rkulla

Respostas:

155

Como pode ser que gravar no disco físico é MUITO mais rápido do que gravar na "tela" (presumivelmente uma operação totalmente com RAM) e é tão rápido quanto simplesmente despejar no lixo com / dev / null?

Parabéns, você acabou de descobrir a importância do buffer de E / S. :-)

O disco parece ser mais rápido, porque é altamente armazenado em buffer: todas as write()chamadas do Python retornam antes que qualquer coisa seja realmente gravada no disco físico. (O sistema operacional faz isso mais tarde, combinando milhares de gravações individuais em grandes e eficientes blocos.)

O terminal, por outro lado, faz pouco ou nenhum buffer: cada indivíduo print/ write(line)espera pela gravação completa (por exemplo, exibição no dispositivo de saída) para concluir.

Para tornar a comparação justa, você deve fazer com que o teste de arquivo use o mesmo buffer de saída do terminal, o que você pode fazer modificando seu exemplo para:

fp = file("out.txt", "w", 1)   # line-buffered, like stdout
[...]
for x in range(lineCount):
    fp.write(line)
    os.fsync(fp.fileno())      # wait for the write to actually complete

Eu executei seu teste de gravação de arquivos na minha máquina e, com buffer, ele também 0,05s aqui para 100.000 linhas.

No entanto, com as modificações acima para gravar sem buffer, são necessários 40 segundos para gravar apenas 1.000 linhas no disco. Desisti de esperar por 100.000 linhas para escrever, mas extrapolando do anterior, levaria mais de uma hora .

Isso coloca os 11 segundos do terminal em perspectiva, não é?

Portanto, para responder à sua pergunta original, escrever em um terminal é realmente incrivelmente rápido, considerando todas as coisas, e não há muito espaço para torná-lo muito mais rápido (mas terminais individuais variam em quanto trabalho eles fazem; veja o comentário de Russ sobre isso responda).

(Você pode adicionar mais buffer de gravação, como na E / S do disco, mas não verá o que foi gravado no seu terminal até que o buffer seja liberado. É uma troca: interatividade versus eficiência em massa.)

Pi Delport
fonte
6
Recebo buffer de E / S ... você certamente me lembrou que eu deveria ter feito o fsync para uma comparação real do tempo de conclusão (atualizarei a pergunta), mas um fsync por linha é insanidade. Um tty realmente precisa efetivamente fazer isso? Não há buffer de terminal / lado do sistema operacional equivalente aos arquivos? ou seja: os aplicativos gravam em stdout e retornam antes que o terminal seja renderizado para a tela, com o terminal (ou os) armazenando tudo em buffer. O terminal poderia renderizar sensivelmente a cauda para a tela a uma taxa de quadros visível. Bloquear efetivamente todas as linhas parece bobagem. Eu sinto que ainda estou perdendo alguma coisa.
Russ
Você pode simplesmente abrir um identificador para stdout com um grande buffer, usando algo como os.fdopen(sys.stdout.fileno(), 'w', BIGNUM). Porém, isso quase nunca seria útil: quase todos os aplicativos precisariam se lembrar de liberar explicitamente após cada linha de saída pretendida pelo usuário.
Delport Pi
1
Eu experimentei anteriormente com enormes (até 10 MB com fp = os.fdopen(sys.__stdout__.fileno(), 'w', 10000000)) buffers do lado do python. O impacto foi nulo. ou seja: ainda longos atrasos tty. Isso me fez pensar / perceber que você acabou de adiar o problema lento do tty ... quando o buffer do python finalmente libera o tty ainda parece fazer a mesma quantidade total de processamento no fluxo antes de retornar.
russ
8
Observe que esta resposta é enganosa e errada (desculpe!). Especificamente, é errado dizer "não há muito espaço para torná-lo mais rápido [de 11 segundos]". Por favor, veja minha própria resposta à pergunta em que mostro que o terminal wterm alcançou o mesmo resultado de 11s em 0,26s.
russ
2
Russ: obrigado pelo feedback! Do meu lado, um fdopenbuffer maior (2 MB) definitivamente fez uma enorme diferença: o tempo de impressão foi reduzido de muitos segundos para 0,05s, o mesmo que a saída do arquivo (usando gnome-terminal).
Delport Pi
88

Obrigado por todos os comentários! Acabei respondendo pessoalmente com sua ajuda. Parece sujo responder à sua própria pergunta, no entanto.

Pergunta 1: Por que a impressão em stdout é lenta?

Resposta: A impressão em stdout não é inerentemente lenta. É o terminal com o qual você trabalha que é lento. E tem praticamente zero a ver com o buffer de E / S no lado do aplicativo (por exemplo: buffer de arquivo python). Ver abaixo.

Pergunta 2: Pode ser acelerado?

Resposta: Sim, pode, mas aparentemente não do lado do programa (o lado que está 'imprimindo' no stdout). Para acelerar, use um emulador de terminal diferente mais rápido.

Explicação...

Tentei um programa de terminal 'leve' auto-descrito chamado wterme obtive resultados significativamente melhores. Abaixo está a saída do meu script de teste (na parte inferior da pergunta) ao executar wterma 1920x1200 no mesmo sistema em que a opção básica de impressão levou 12 segundos usando o gnome-terminal:

-----
resumo de tempo (100 mil linhas cada)
-----
impressão: 0,261 s
gravação no arquivo (+ fsync): 0.110 s
imprimir com stdout = / dev / null: 0.050 s

0.26s é MUITO melhor que 12s! Não sei se wtermé mais inteligente sobre como torna a tela ao longo das linhas de como eu estava sugerindo (renderize a cauda 'visível' a uma taxa de quadros razoável) ou se apenas "faz menos" que gnome-terminal. Para os fins da minha pergunta, eu tenho a resposta, no entanto. gnome-terminalé lento.

Então - Se você tem um script de execução prolongado que considera lento e envia grandes quantidades de texto ao stdout ... tente um terminal diferente e veja se é melhor!

Note que eu peguei aleatoriamente wtermdos repositórios ubuntu / debian. Esse link pode ser o mesmo terminal, mas não tenho certeza. Não testei nenhum outro emulador de terminal.


Atualização: Como tive que coçar a coceira, testei uma pilha inteira de outros emuladores de terminal com o mesmo script e tela cheia (1920x1200). Minhas estatísticas coletadas manualmente estão aqui:

wterm 0.3s
aterm 0.3s
rxvt 0.3s
mrxvt 0.4s
konsole 0.6s
yakuake 0.7s
lxterminal 7s
xterm 9s
gnome-terminal 12s
xfce4-terminal 12s
vala-terminal 18s
xvt 48s

Os tempos registrados são coletados manualmente, mas eram bastante consistentes. Eu gravei o melhor valor (ish). YMMV, obviamente.

Como bônus, foi um passeio interessante por alguns dos vários emuladores de terminal disponíveis no mercado! Estou surpreso que meu primeiro teste 'alternativo' tenha sido o melhor do grupo.

Russ
fonte
1
Você também pode tentar aterm. Aqui estão os resultados do meu teste usando seu script. Aterm - imprimir: 0,491 s, gravar no arquivo (+ fsync): 0,110 s, imprimir com stdout = / dev / null: 0,087 s wterm - imprimir: 0,521 s, gravar no arquivo (+ fsync): 0,105 s, imprimir com stdout = / dev / null: 0.085 s
frogstarr78
1
Como o urxvt se compara ao rxvt?
Daenyth 7/10/10
3
Além disso, screen(o programa) deve ser incluído na lista! (Ou byobu, que é um invólucro para screenaprimoramentos) Esse utilitário permite ter vários terminais, como as guias nos terminais X. Presumo que imprimir no screenterminal atual é o mesmo que imprimir em um terminal comum, mas e quanto a imprimir em um screenterminal e depois mudar para outro sem atividade?
Armando Pérez Marqués
1
Estranho, há algum tempo, eu estava comparando diferentes terminais em termos de velocidade e o gnome-terminal saiu melhor em testes bastante sérios, enquanto o xterm era mais lento. Talvez eles tenham trabalhado duro no buffer desde então. Além disso, o suporte a Unicode pode fazer uma grande diferença.
Tomas Pruzina
2
iTerm2 no OSX me deu: print: 0.587 s, write to file (+fsync): 0.034 s, print with stdout = /dev/null : 0.041 s. E com 'screen' rodando no iTerm2:print: 1.286 s, write to file (+fsync): 0.043 s, print with stdout = /dev/null : 0.033 s
rkulla 27/01
13

Seu redirecionamento provavelmente não faz nada, pois os programas podem determinar se o FD de saída aponta para um tty.

É provável que stdout seja buffer de linha ao apontar para um terminal (o mesmo que o stdoutcomportamento do fluxo de C ).

Como um experimento divertido, tente canalizar a saída para cat.


Eu tentei meu próprio experimento divertido, e aqui estão os resultados.

$ python test.py 2>foo
...
$ cat foo
-----
timing summary (100k lines each)
-----
print                         : 6.040 s
write to file                 : 0.122 s
print with stdout = /dev/null : 0.121 s

$ python test.py 2>foo |cat
...
$ cat foo
-----
timing summary (100k lines each)
-----
print                         : 1.024 s
write to file                 : 0.131 s
print with stdout = /dev/null : 0.122 s
Hasturkun
fonte
Eu não pensei em python verificando sua saída FS. Gostaria de saber se python está puxando um truque nos bastidores? Espero que não, mas não sei.
russ
+1 para apontar a diferença muito importante no buffer
Peter G.
@Russ: as -uforças de opção stdin, stdoute stderrser não tamponada, que será mais lenta do que a ser bloco tamponada (devido a sobrecarga)
Hasturkun
4

Não posso falar sobre os detalhes técnicos porque não os conheço, mas isso não me surpreende: o terminal não foi projetado para imprimir muitos dados como este. De fato, você ainda fornece um link para um monte de coisas da GUI que ele precisa fazer toda vez que você deseja imprimir algo! Observe que, se você chamar o script pythonw, ele não levará 15 segundos; isso é totalmente um problema da GUI. Redirecione stdoutpara um arquivo para evitar isso:

import contextlib, io
@contextlib.contextmanager
def redirect_stdout(stream):
    import sys
    sys.stdout = stream
    yield
    sys.stdout = sys.__stdout__

output = io.StringIO
with redirect_stdout(output):
    ...
Katriel
fonte
3

A impressão no terminal será lenta. Infelizmente, antes de escrever uma nova implementação de terminal, não consigo ver como você aceleraria isso significativamente.

shuttle87
fonte
2

Além da saída provavelmente padrão para o modo de buffer de linha, a saída para um terminal também está fazendo com que seus dados fluam para um terminal e uma linha serial com uma taxa de transferência máxima, ou um pseudo-terminal e um processo separado que esteja manipulando uma exibição loop de evento, renderizando caracteres de alguma fonte, movendo bits de exibição para implementar uma exibição de rolagem. O último cenário provavelmente está espalhado por vários processos (por exemplo, servidor / cliente de telnet, aplicativo de terminal, servidor de exibição X11), de modo que também existem problemas de alternância de contexto e latência.

Liudvikas Bukys
fonte
Verdade! Isso me levou a tentar reduzir o tamanho da janela do meu terminal (no Gnome) para algo insignificante (de 1920x1200). Com certeza ... 2.8s tempo de impressão vs 11.5s. Muito melhor, mas ainda assim ... por que está parado? Você pensaria que o buffer stdout (hmm) poderia lidar com todas as linhas de 100k e a exibição do terminal apenas pegaria o que caber na tela a partir da extremidade traseira do buffer e o executaria de uma só vez.
russ
O xterm (ou gterm, nesse caso) tornaria sua tela eventual mais rápida se não achasse que também precisava exibir todas as outras saídas ao longo do caminho. Se tentasse seguir esse caminho, provavelmente faria com que o caso comum de pequenas atualizações de tela parecesse menos responsivo. Ao escrever esse tipo de software, às vezes é possível lidar com ele, com modos diferentes e tentando detectar quando você precisa mudar para / de um modo de operação pequeno para um volume maior. Você pode usar cat big_file | tailou mesmo com cat big_file | tee big_file.cpy | tailmuita frequência para acelerar esse processo.
Nataliaose #