なぜstdoutへの出力がとても遅いのですか?スピードアップできますか?


166

私はいつも、printステートメントを使用して端末に出力するのにかかる時間に驚かされ、不満を感じてきました。最近痛々しいほど遅いロギングの後で、私はそれを調査することに決めました、そして費やされたほとんどすべての時間は端末が結果を処理するのを待っていることを発見してかなり驚きました。

stdoutへの書き込みをどうにかして高速化できますか?

print_timer.py100k行をstdout、file、およびstdoutにリダイレクトして書き込むときのタイミングを比較するスクリプト(この質問の最後にある' ')を書きました/dev/null。タイミングの結果は次のとおりです。

$ 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

ワオ。stdoutを/ dev / nullなどに再割り当てしたことを認識するなど、Pythonが舞台裏で何かをしていないことを確認するために、スクリプトの外部でリダイレクトを行いました...

$ 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

したがって、これはpythonのトリックではなく、単なるターミナルです。/ dev / nullに出力をダンプすることは常にスピードアップすることを知っていましたが、それがそれほど重要であるとは思いもしませんでした!

それは私がttyがどれほど遅いかを驚かせます。物理ディスクへの書き込みが「画面」(おそらくすべてRAMの操作)への書き込みよりもはるかに高速で、/ dev / nullを使用してガベージに単にダンプするのと同じくらい高速であるとしたらどうでしょうか。

このリンクは、ターミナルがどのようにI / Oをブロックするかについて説明しており、「[入力]を解析し、フレームバッファーを更新し、ウィンドウをスクロールするためにXサーバーと通信する」などができます...完全にそれを取得します。何がそんなに長くかかるのですか?

私は(より速いtty実装がない限り)解決策はないと予想しますが、とにかく私が尋ねる数字です。


更新:いくつかのコメントを読んだ後、私は自分の画面サイズが実際に印刷時間にどのくらい影響を与えるのか疑問に思いました、そしてそれはいくつかの重要性を持っています。上記の非常に遅い数値は、Gnomeターミナルが1920x1200にまで膨らんでいる場合です。非常に小さくすると、...

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

それは確かに優れています(約4倍)が、私の質問は変わりません。端末画面のレンダリングでアプリケーションがstdoutへの書き込みを遅くする理由がわからないため、私の質問に追加されるだけです。スクリーンレンダリングが続行するのをプログラムが待機する必要があるのはなぜですか?

すべての端末/ ttyアプリが同じではありませんか?まだ実験していない。端末はすべての着信データをバッファリングし、不可視に解析/レンダリングし、現在の画面構成に表示される最新のチャンクのみを適切なフレームレートでレンダリングできるように思われます。したがって、ディスクに〜0.1秒で+ fsyncを書き込むことができれば、端末は同じ操作をその順序で完了することができるはずです(実行中にいくつかの画面が更新される可能性があります)。

プログラマにとってこの動作をより良くするためにアプリケーション側から変更できるtty設定があることをまだ望んでいます。これが厳密にターミナルアプリケーションの問題である場合、StackOverflowに属していない可能性がありますか?

何が欠けていますか?


以下は、タイミングを生成するために使用されるpythonプログラムです。

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

9
stdoutに書き込む目的は、人間が出力を読み取れるようにすることです。世界中の人間が12秒で10,000行のテキストを読むことができないので、stdoutを速くする意味は何ですか?
Seun Osewa

14
@Seun Osewa:(私の疑問を駆り立てた)1つの例は、printステートメントのデバッグなどを行うときです。プログラムを実行し、結果が発生するのを確認したいとします。ほとんどの行が見えないほど飛ぶことは明らかですが、例外が発生したとき(または慎重に配置した条件付きのgetch / raw_input / sleepステートメントにヒットしたとき)は、印刷出力ではなく直接出力したい常にファイルビューを開いたり更新したりする必要があります。
ラス

3
印刷ステートメントのデバッグは、ttyデバイス(つまり、端末)がブロックバッファリングではなくラインバッファリングをデフォルトにする理由の1つです。プログラムがハングし、デバッグ出力の最後の数行がまだバッファにある場合、デバッグ出力はあまり役に立ちません。端末にフラッシュする代わりに。
スティーブンC.スティール

@Stephen:バッファサイズを増やすことで1人のコメント投稿者が主張した大幅な改善を追求することにあまり気を使っていなかったのはこのためです。デバッグ印刷の目的を完全に無効にします!調査中に少し実験をしましたが、純改善は見られませんでした。私はまだその違いに興味がありますが、実際にはそうではありません。
Russ

実行時間が非常に長いプログラムの場合は、現在の行の標準出力をn秒ごとに出力することがあります。これは、cursesアプリで更新遅延が発生するのと同じです。完璧ではありませんが、たまにどこにいるのかがわかります。
rkulla 2015年

回答:


155

物理ディスクへの書き込みが「画面」(おそらくすべてRAMの操作)への書き込みよりもはるかに高速で、/ dev / nullを使用してガベージに単にダンプするのと同じくらい高速であるとしたらどうでしょうか。

おめでとうございます。I/ Oバッファリングの重要性を発見しました。:-)

非常にバッファリングされているため、ディスクの方が高速に見えますwrite()。物理ディスクに実際に何かが書き込まれる前に、すべてのPythonの呼び出しが返されます。(OSはこれを後で行い、何千もの個別の書き込みを大きな効率的なチャンクに結合します。)

一方、端末はほとんどまたはまったくバッファリングを行いません。個々のprint/ write(line)は、完全な書き込み(つまり、出力デバイスへの表示)が完了するのを待ちます。

比較を公平にするために、ファイルテストでターミナルと同じ出力バッファリングを使用する必要があります。これは、例を次のように変更することで実行できます。

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

私はあなたのファイル書き込みテストを私のマシンで実行しました、そしてバッファリングで、ここでも100,000行で0.05秒です。

ただし、バッファなしで書き込むための上記の変更により、ディスクに1,000行のみを書き込むのに40秒かかります。私は100,000行が書き込まれるのを待つのをあきらめましたが、前のものから外挿すると、1時間以上かかります。

端末の11秒を考慮に入れますね。

したがって、元の質問に答えるために、端末への書き込みは実際には非常に高速であり、すべてのことを考慮し、それを大幅に高速化する余地はあまりありません(ただし、個々の端末は実行する作業量が異なります。これに対するRussのコメントを参照してください回答)。

(ディスクI / Oの場合と同様に、書き込みバッファリングを追加することはできますが、バッファーがフラッシュされるまで、端末に何が書き込まれたかはわかりません。これは、双方向性とバルク効率のトレードオフです。)


6
I / Oバッファリングを取得します...完了時間の真の比較のためにfsyncを実行する必要があることを確かに思い出しました(質問を更新します)が、1行あたりのfsync は狂気です。ttyは本当にそれを効果的に行う必要がありますか?ファイルと同等のターミナル/ OS側のバッファリングはありませんか?つまり、アプリケーションはstdoutに書き込み、ターミナルが画面にレンダリングする前に戻り、ターミナル(またはos)がすべてをバッファリングします。次に、端末は、目に見えるフレームレートで尾を画面にレンダリングします。すべての行を効果的にブロックするのはばかげているようです。私はまだ何かが足りないと感じています。
ラス

のようなものを使用して、大きなバッファーを使用してstdoutのハンドルを開くことができますos.fdopen(sys.stdout.fileno(), 'w', BIGNUM)。ただし、これはほとんど役に立ちません。ほとんどすべてのアプリケーションは、ユーザーが意図した出力の各行の後で明示的にフラッシュすることを覚えておく必要があります。
Pi Delport

1
以前、巨大な(最大で10MBのfp = os.fdopen(sys.__stdout__.fileno(), 'w', 10000000))Pythonサイドバッファを実験しました。影響はなかった。すなわち、まだ長いtty遅延。これにより、遅いttyの問題を延期するだけだと思いました。Pythonのバッファーが最終的にttyをフラッシュしたとき、ストリームに戻る前に、同じ合計量の処理を実行しているようです。
Russ

8
この回答は誤解を招くものであり、間違いです(申し訳ありません!)。特に、「11秒より速くするための余地があまりない」と言うのは間違っています。wterm端末が同じ11秒の結果を0.26秒で達成したことを示す質問に対する私自身の回答を参照してください。
ラス

2
ラス:フィードバックをありがとう!私の側では、より大きなfdopenバッファー(2MB)は間違いなく大きな違いをもたらしました。それは、印刷時間を数秒から0.05秒に短縮しました。これは、ファイル出力(を使用gnome-terminal)と同じです。
Pi Delport

88

すべてのコメントをありがとう!あなたの助けを借りて自分で答えてしまいました。しかし、あなた自身の質問に答えるのは汚い感じです。

質問1:標準出力への印刷が遅いのはなぜですか?

回答: stdoutへの印刷本質的に遅いわけではありません。動作が遅いのは端末です。また、アプリケーション側のI / Oバッファリング(Pythonファイルのバッファリングなど)とはほとんど関係ありません。下記参照。

質問2:高速化できますか?

回答:はい、できますが、プログラム側(標準出力への「印刷」を行う側)からではないようです。高速化するには、より高速な別のターミナルエミュレータを使用します。

説明...

私は呼ばれる自己記述型の「軽量」端末プログラムを試してみたところ、かなり良い結果wtermが得られました。wterm基本的な印刷オプションがgnome-terminalを使用して12秒かかったのと同じシステムで1920x1200で実行したときの(質問の下部にある)テストスクリプトの出力は次のとおりです。

-----
タイミングの概要(各10万行)
-----
印刷:0.261 s
ファイルに書き込む(+ fsync):0.110 s
stdout = / dev / nullを指定して印刷:0.050秒

0.26秒は12秒よりはるかに優れています!私wtermが提案していた方法(「目に見える」尾を適切なフレームレートでレンダリングする)に沿って画面にレンダリングする方法がよりインテリジェントであるのか、それとも「より少ない」だけであるのかはわかりませんgnome-terminal。私の質問のために、私は答えを持っています。 gnome-terminal遅い。

だから-あなたが遅いと感じる長い実行中のスクリプトがあり、それが標準出力に大量のテキストを吐き出すなら...別の端末を試して、それがより良いかどうか確かめてください!

私はかなりランダムwtermにubuntu / debianリポジトリからプルしたことに注意してください。 このリンクは同じ端末である可能性がありますが、よくわかりません。他の端末エミュレータはテストしていません。


更新:かゆみを掻く必要があったので、同じスクリプトと全画面(1920x1200)で他の端末エミュレーターの山全体をテストしました。私の手動で収集された統計はここにあります:

wterm 0.3秒
aterm 0.3s
rxvt 0.3s
mrxvt 0.4秒
konsole 0.6s
ヤクアケ0.7s
lxterminal 7s
xterm 9s
gnome-terminal 12s
xfce4-terminal 12s
バラ端子18s
xvt 48s

記録された時間は手動で収集されますが、かなり一貫性がありました。最高の値を記録しました。YMMV、明らかに。

おまけとして、それはそこで入手可能なさまざまな端末エミュレーターのいくつかの興味深いツアーでした!私の最初の「代替」テストが群を抜いて最高であることが判明したことに驚いています。


1
また、atermを試すこともできます。これは、スクリプトを使用したテストの結果です。Aterm-印刷:0.491秒、ファイルに書き込む(+ fsync):0.110秒、標準出力で印刷= / dev / null:0.087秒wterm-印刷:0.521秒、ファイルに書き込む(+ fsync):0.105秒、標準出力で印刷= / dev / null:0.085 s
frogstarr78

1
urxvtとrxvtの違いを教えてください。
Daenyth

3
また、screen(プログラム)もリストに含める必要があります!(またはbyobuscreen拡張機能のラッパーです)このユーティリティを使用すると、X端末のタブのように複数の端末を使用できます。現在screenのの端末への印刷は、通常の端末への印刷と同じだと思いますが、のいずれかの端末で印刷してから、screenアクティビティのない別の端末に切り替えるとどうなりますか?
アルマンドペレスマルケス2010年

1
奇妙なことに、少し前に速度の点で異なる端末を比較していましたが、xtermが最も低速だったのに対し、かなり深刻なテストではgnome-terminalが最も優れていました。おそらく彼らはそれ以来バッファリングに懸命に取り組んだのだろう。また、ユニコードのサポートは大きな違いをもたらす可能性があります。
Tomas Pruzina

2
OSX上のiTerm2は私に与えました:print: 0.587 s, write to file (+fsync): 0.034 s, print with stdout = /dev/null : 0.041 s。そして、iTerm2で実行されている「画面」で:print: 1.286 s, write to file (+fsync): 0.043 s, print with stdout = /dev/null : 0.033 s
rkulla

13

プログラムは出力FDがttyを指しているかどうかを判別できるため、リダイレクトはおそらく何もしません。

端末を指す場合、標準出力は行バッファリングされる可能性があります(Cのstdoutストリーム動作と同じ)。

面白い実験として、出力をにパイプしてみてくださいcat


私は自分で面白い実験をしてみましたが、結果は次のとおりです。

$ 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

私はpythonがその出力FSをチェックすることを考えていませんでした。Pythonが裏でトリックを引いているのだろうか?期待はしていませんが、わかりません。
Russ

+1バッファリングのすべての重要な違いを指摘するため
ピーターG.

@Russ:-uオプション力stdinstdout及びstderrブロック(によるオーバーヘッドに)緩衝されるよりも遅くされる、バッファなしであると
Hasturkun

4

技術的な詳細がわからないので話せませんが、これは私を驚かせません。ターミナルは、このような大量のデータを印刷するように設計されていません。実際、何かを印刷したいときに毎回実行する必要のあるGUI要素へのリンクを提供することもできます。pythonw代わりにでスクリプトを呼び出す場合、15秒はかかりません。これは完全にGUIの問題です。stdoutこれを回避するには、ファイルにリダイレクトします。

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):
    ...

3

端末への印刷が遅くなります。残念ながら、新しい端末の実装を書くまでは、これを大幅に高速化する方法を実際に確認することはできません。


2

おそらくデフォルトでラインバッファリングモードになっている出力に加えて、端末への出力により、データが最大スループットで端末とシリアル回線に流れるか、疑似端末とディスプレイを処理している別のプロセスに流れますイベントループ、一部のフォントからの文字のレンダリング、表示ビットの移動によるスクロール表示の実装。後者のシナリオはおそらく複数のプロセス(Telnetサーバー/クライアント、ターミナルアプリ、X11ディスプレイサーバーなど)に分散しているため、コンテキストの切り替えや遅延の問題もあります。


ほんとだ!これにより、(Gnomeでの)端末ウィンドウのサイズを(1920x1200から)気の利いたものに縮小してみるように促されました。案の定、印刷時間は2.8秒と11.5秒です。はるかに良いですが、それでも...なぜそれが行き詰まっているのですか?stdoutバッファ(hmm)は100k行すべてを処理でき、ターミナルディスプレイはバッファの末尾から画面上に収まるものをすべて取得し、1つのクイックショットで実行すると思います。
Russ

xterm(この場合はgterm)は、途中で他のすべての出力も表示する必要があると考えなかった場合、最終的な画面をより速くレンダリングします。このルートを使用しようとすると、小さな画面の更新という一般的なケースの応答性が低下する可能性があります。このタイプのソフトウェアを作成するときは、さまざまなモードを用意し、小規模な操作からバルク操作に移行する必要がある場合を検出しようとすることで対処できる場合があります。この高速化のために、cat big_file | tailまたはcat big_file | tee big_file.cpy | tail非常に頻繁に使用できます。
nategoose
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.