Чому надрукування друку настільки повільне? Чи можна це прискорити?


166

Я завжди був вражений / засмучений тим, скільки часу потрібно просто вивести на термінал із заявою про друк. Після деякої недавно болісно повільної реєстрації я вирішив заглянути в це і здивовано виявив, що майже весь витрачений час чекає, поки термінал обробить результати.

Чи можна записати в stdout якось швидше?

Я написав сценарій (' print_timer.py' внизу цього питання) для порівняння часу при написанні 100k рядків для 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

Ого. Щоб переконатися, що python не робить щось за кадром, як-от визнання того, що я переписав stdout на / dev / null чи щось таке, я зробив перенаправлення поза сценарієм ...

$ 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

Тож це не трюк пітона, це лише термінал. Я завжди знав демпінговий вихід на / dev / null прискорив речі, але ніколи не вважав, що це таке суттєве!

Мене дивує, наскільки повільний TTY. Яким чином може бути те, що запис на фізичний диск ШЛЮШЕ швидше, ніж запис на "екран" (імовірно, всеоперативна пам'ять), і це так само швидко, як просто скидання на сміття з / dev / null?

Це посилання говорить про те, як термінал блокує введення-виведення, щоб він міг "розібрати [вхід], оновити буфер кадру, спілкуватися з сервером X для того, щоб прокрутити вікно тощо", але я не повністю отримати це. Що може зайняти так довго?

Я сподіваюся, що виходу немає (окрім швидшого впровадження?), Але я все одно попрошу.


ОНОВЛЕННЯ: прочитавши деякі коментарі, я задумався, як сильно впливає розмір екрана на час друку, і це має певне значення. Дійсно повільні цифри вище, коли мій термінал 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 додатки не створені рівними? Мені ще потрібно експериментувати. Мені справді здається, що термінал повинен мати змогу буферизувати всі вхідні дані, проаналізувати / зробити їх непомітними, а лише зробити найсвіжіший фрагмент, який видно в поточній конфігурації екрану з розумною частотою кадрів. Отже, якщо я можу записати + fsync на диск за ~ 0,1 секунди, термінал повинен мати можливість виконати ту саму операцію в чомусь такому порядку (можливо, з кількома оновленнями екрана, поки це робив).

Я все ще сподіваюся, що існує ціла настройка, яку можна змінити з боку програми, щоб зробити цю поведінку кращою для програміста. Якщо це суворо проблема термінальної програми, то, можливо, це навіть не належить до 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 полягає в тому, щоб людина могла прочитати вихід. Жодна людина у світі не може прочитати 10000 рядків тексту за 12 секунд, тож який сенс робити stdout швидше ???
Seun Osewa

14
@Seun Osewa: Один із прикладів (що викликав моє запитання) - це робити такі речі, як налагодження операції print . Ви хочете запустити свою програму і побачити результати, як вони відбуваються. Ви, очевидно, праві, що більшість рядків пролітає через те, що ви не можете бачити, але коли трапляється виняток (або ви потрапляєте на умовно заяву getch / raw_input / sleep, яку ви ретельно розмістили), ви хочете дивитись на вихід друку безпосередньо, а не постійно доводиться відкривати або оновлювати подання файлів.
Russ

3
Налагодження оператора друку є однією з причин того, що tty пристрої (тобто термінали) за замовчуванням для буферизації рядків замість блокування буферизуються: вихід налагодження не дуже корисний, якщо програма висить і останні кілька рядків виводу налагодження все ще знаходяться в буфері замість того, щоб пролітати до терміналу.
Стівен К. Сталь

@Stephen: Ось чому я не сильно переймався переслідуванням величезних удосконалень, про які писав один коментатор, стягуючи розмір буфера. Це повністю перемагає призначення налагодження друку! Я трохи експериментував, досліджуючи, але не побачив жодного покращення. Мені все ще цікаво розбіжність, але не дуже.
Russ

Іноді для дуже тривалих програм я просто надрукую поточну строку рядка кожні n секунд - подібно до затримки оновлення у програмі прокльонів. Це не ідеально, але дає уявлення про те, що я тут раз за часом.
rkulla

Відповіді:


155

Яким чином може бути те, що запис на фізичний диск ШЛЮШЕ швидше, ніж запис на "екран" (імовірно, всеоперативна пам'ять), і це так само швидко, як просто скидання на сміття з / dev / null?

Вітаємо, щойно ви виявили важливість буферизації вводу / виводу. :-)

Диск, здається, швидший, тому що він сильно забуференний: всі write()дзвінки Python повертаються ще до того, як щось насправді буде записано на фізичний диск. (ОС робить це пізніше, поєднуючи багато тисяч окремих записів у великі ефективні шматки.)

З іншого боку, термінал робить мало або взагалі буферизацією: кожен індивід 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

Я запустив ваш тест із написання файлів на своїй машині, а також із буферизацією він також становить 0,05 за 100 000 рядків.

Однак із зазначеними вище модифікаціями, щоб записати небуферовані, для запису на диск лише 1000 рядків потрібно 40 секунд. Я відмовився чекати написання 100 000 рядків, але екстраполяція від попередньої зайняла б більше години .

Це ставить термінал 11 секунд у перспективу, чи не так?

Отже, щоб відповісти на своє первісне запитання, написання на термінал насправді є надзвичайно швидким, все враховано, і не так багато місця, щоб зробити це набагато швидше (але окремі термінали відрізняються залежно від кількості роботи; дивіться коментар Русса до цього відповідь).

(Ви можете додати більше буферизації запису, як, наприклад, з дисковим введенням-виведенням, але тоді ви б не побачили, що було записано на ваш термінал, поки після того, як буфер не змиється. Це компроміс: інтерактивність та об'ємна ефективність.)


6
Я отримую буферизацію вводу / виводу ... ви, звичайно, нагадали мені, що я повинен мати fsync'd для справжнього порівняння часу завершення (я оновлю питання), але fsync на рядок - це божевілля. Чи дійсно потрібно, щоб це було ефективно для цього? Чи немає буферизації терміналу / ос на стороні, еквівалентного файлам? тобто: Програми записують у stdout та повертаються перед тим, як термінал відображається на екран, термінал (або os) буферизує все це. Потім термінал міг би сприйняти хвіст на екрані з видимою швидкістю кадру. Ефективно блокування в кожній лінії здається нерозумним. Я відчуваю, що все ще чогось сумую.
Russ

Ви можете просто відкрити ручку для stdout з великим буфером самостійно, використовуючи щось подібне os.fdopen(sys.stdout.fileno(), 'w', BIGNUM). Це майже ніколи не стане корисним: майже всі програми повинні пам’ятати, щоб вони явно промивали після кожного рядка призначеного користувачем виводу.
Пі-Дельпорт

1
Раніше я експериментував з величезними (до 10 Мб fp = os.fdopen(sys.__stdout__.fileno(), 'w', 10000000)) пітоновими буферами. Вплив було нульовим. тобто: ще тривалі затримки. Це змусило мене подумати / зрозуміти, що ви просто відкладаєте проблему повільного tty ... коли буфер python нарешті змиває, tty все одно, здається, робить такий самий загальний обсяг обробки в потоці перед поверненням.
Russ

8
Зауважте, що ця відповідь є оманливою та неправильною (вибачте!). Зокрема, неправильно сказати: "Не так багато місця, щоб зробити це швидше [ніж 11 секунд]". Будь ласка, дивіться мою власну відповідь на питання, де я показую, що термінал wterm досягнув того самого результату 11-х за 0,26 с.
Русс

2
Russ: дякую за відгуки! З мого боку, більший fdopenбуфер (2 Мб), безумовно, змінив величезну зміну: час друку скоротився з багатьох секунд до 0,05 секунди, як і виведення файлів (використання gnome-terminal).
Пі-Дельпорт

88

Дякую за всі коментарі! Нарешті я відповів на це з вашою допомогою. Однак, почуваючись брудно, відповідаючи на власне запитання.

Запитання 1: Чому друк на stdout повільний?

Відповідь: Друк до stdout не є по суті повільним. Це термінал, з яким ви працюєте, це повільно. І це має майже нульове значення для буферизації вводу / виводу на стороні програми (наприклад: буферизація файлів python). Дивись нижче.

Питання 2: Чи можна прискорити його?

Відповідь: Так, але це може бути, але, мабуть, не з боку програми (сторона, що робить 'друк' на stdout). Щоб пришвидшити його, використовуйте більш швидкий інший термінальний емулятор.

Пояснення ...

Я спробував самостійно описану «легку» термінальну програму, яка називається, wtermі отримала значно кращі результати. Нижче наводиться висновок мого тестового сценарію (внизу питання) при запуску в wterm1920x1200 у тій же системі, де основний параметр друку займав 12 секунд за допомогою gnome-terminal:

-----
підсумок часу (по 100 тис. рядків у кожному)
-----
друк: 0,261 с
запис у файл (+ fsync): 0,110 с
друк з stdout = / dev / null: 0,050 с

0,26с набагато кращий за 12с! Я не знаю, чи wtermрозумніший у тому, як він відображається на екрані у відповідності з тим, як я пропонував (виводити "видимий" хвіст із розумною швидкістю кадрів), чи він просто "робить менше", ніж gnome-terminal. Для мого запитання я отримав відповідь. gnome-terminalповільно.

Отже - Якщо у вас тривалий сценарій, який ви відчуваєте, що він повільний, і він вимагає величезної кількості тексту для stdout ... спробуйте інший термінал і подивіться, чи це краще!

Зауважте, що я досить випадково витягнувся wtermіз сховищ ubuntu / debian. Це посилання може бути тим самим терміналом, але я не впевнений. Я не перевіряв жодних інших емуляторів терміналів.


Оновлення: Оскільки мені довелося подряпати свербіж, я протестував цілу купу інших емуляторів терміналів з тим же сценарієм і повним екраном (1920x1200). Мої статистичні дані, зібрані вручну:

wterm 0,3s
aterm 0,3s
rxvt 0,3с
mrxvt 0,4с
консоль 0,6с
якуаке 0,7с
lxterminal 7s
xterm 9s
гном-термінал 12с
xfce4-термінал 12с
vala-термінал 18-х
xvt 48с

Записані часи збираються вручну, але вони були досить послідовними. Я записав найкраще (іш) значення. YMMV, очевидно.

Як бонус, це була цікава екскурсія деякими з різних емуляторів терміналів, доступних там! Я вражений, що мій перший "альтернативний" тест виявився найкращим з групи.


1
Ви також можете спробувати aterm. Ось результати мого тесту з використанням вашого сценарію. Aterm - друк: 0,491 с, запис у файл (+ fsync): 0,110 с, друк із stdout = / dev / null: 0,087 s wterm - друк: 0,521 s, запис у файл (+ fsync): 0,105 s, друк із stdout = / dev / null: 0,085 с
frogstarr78

1
Як urxvt порівнюється з rxvt?
Daenyth

3
Також, screen(програма) повинна бути включена до списку! (Або byobu, що є обгорткою для screenрозширень) Ця утиліта дозволяє мати декілька терміналів, подібно до вкладок у X-терміналах. Я припускаю, що друк на поточному screenтерміналі - це те саме, що друк на звичайному, але що робити з друком в одному з screenтерміналів, а потім переключенням на інший без активності?
Армандо Перес Маркес

1
Дивно, деякий час тому я порівнював різні термінали за швидкістю, і gnome-термінал вийшов найкращим у досить серйозних тестах, а xterm був найповільнішим. Можливо, вони відтоді багато працювали над буферизацією. Також підтримка unicode може призвести до великої різниці.
Томаш Прузіна

2
iTerm2 на OSX дав мені: 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 вказує на їх кількість.

Цілком ймовірно, що stdout є буферизованою лінією, коли вказує на термінал (те саме, що і stdoutпотік потоку C ).

Як цікавий експеримент, спробуйте підключити вихід 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. Цікаво, чи пітон витягує хитрість за лаштунками? Я не очікую, але не знаю.
Russ

+1 за вказівку на найважливішу різницю в буферизації
Пітер Г.

@Russ: по -uопціонах сил stdin, stdoutі 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 (хм) міг би обробляти всі 100k ліній, а дисплей терміналу просто захопить все, що може вміститися на екрані з хвостового кінця буфера, і зробить це за один швидкий знімок.
Russ

Xterm (або в такому випадку gterm) відобразить ваш можливий екран швидше, якби він не думав, що він повинен також відображати весь інший вихід. Якби спробувати пройти цей маршрут, це, ймовірно, зробить звичайний випадок оновлень невеликих екранів менш чуйним. Під час написання цього типу програмного забезпечення іноді ви можете впоратися з ним, використовуючи різні режими і намагаючись визначити, коли вам потрібно перейти до / з невеликого масового режиму роботи. Ви можете використовувати cat big_file | tailабо навіть cat big_file | tee big_file.cpy | tailдуже часто для цього прискорення.
nategoose
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.