Почему печать на стандартный вывод так медленно? Это может быть ускорено?

166

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

Можно ли как-то ускорить запись в stdout?

Я написал скрипт (' print_timer.py' внизу этого вопроса), чтобы сравнить время при записи 100 тыс. Строк в стандартный вывод, в файл и с перенаправленным на стандартный вывод /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-сервером для прокрутки окна и т. Д.», Но я не полностью получить это. Что может быть так долго?

Я ожидаю, что нет никакого выхода (если не считать более быструю реализацию 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 раза), но не меняет моего вопроса. Это только добавляет к моему вопросу, так как я не понимаю, почему рендеринг экрана терминала должен замедлять запись приложения в стандартный вывод. Почему моя программа должна ждать продолжения рендеринга экрана?

Все терминальные / tty приложения не созданы равными? Я еще не экспериментировал. Мне действительно кажется, что терминал должен иметь возможность буферизовать все входящие данные, анализировать / визуализировать их невидимым образом и отображать только самый последний фрагмент, видимый в текущей конфигурации экрана, с разумной частотой кадров. Поэтому, если я могу записать + fsync на диск за ~ 0,1 секунды, терминал должен быть в состоянии выполнить ту же операцию в каком-то порядке (возможно, с несколькими обновлениями экрана, пока он это делал).

Я все еще надеюсь, что есть параметр 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
Russ
источник
9
Вся цель записи в стандартный вывод состоит в том, чтобы человек мог прочитать вывод. Ни один человек в мире не может прочитать 10 000 строк текста за 12 секунд, так какой смысл в том, чтобы сделать stdout быстрее ???
Сеун Осева
14
@Seun Osewa: Один пример (который привел мой вопрос), когда делаю такие вещи, как отладка операторов печати . Вы хотите запустить свою программу и видеть результаты по мере их появления. Вы, очевидно, правы в том, что большинство строк будут пролетать мимо того, что вы не видите, но когда возникает исключение (или вы удаляете условный оператор getch / raw_input / sleep, который вы аккуратно разместили), вы хотите смотреть на вывод печати напрямую, а не постоянно приходится открывать или обновлять вид файла.
Расс
3
Отладка оператора print является одной из причин, по которой устройства tty (то есть терминалы) по умолчанию используют буферизацию строк вместо буферизации блоков: вывод отладки мало используется, если программа зависает, а последние несколько строк вывода отладки все еще находятся в буфере вместо того, чтобы промыть до терминала.
Стивен С. Сталь
@Stephen: Вот почему я не особо беспокоился о том, чтобы добиться огромных улучшений, о которых заявил один комментатор, увеличив размер буфера. Это полностью побеждает цель печати отладки! Я немного экспериментировал во время расследования, но не увидел чистого улучшения. Мне по-прежнему любопытно расхождение, но не совсем.
Расс
Иногда для очень долго работающих программ я просто печатаю текущий вывод строки каждые n секунд - подобно тому, как в приложении curses есть задержка обновления. Это не идеально, но дает представление о том, где я нахожусь одновременно.
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 на строку - это безумие. Действительно ли tty нужно эффективно делать это? Разве нет терминальной / осевой буферизации, эквивалентной для файлов? То есть: приложения записывают в stdout и возвращаются до того, как терминал выполняет рендеринг на экран, причем терминал (или ОС) буферизует все это. Терминал может затем визуально отобразить хвост на экране с видимой частотой кадров. Эффективно блокировка на каждой линии кажется глупой. Я чувствую, что все еще чего-то не хватает.
Расс
Вы можете просто открыть дескриптор для вывода с большим буфером самостоятельно, используя что-то вроде os.fdopen(sys.stdout.fileno(), 'w', BIGNUM). Это почти никогда не будет полезно: почти все приложения должны будут помнить, что нужно явно сбрасывать после каждой строки вывода, предназначенного для пользователя.
Пи Дельпорт
1
Ранее я экспериментировал с огромными (до 10 МБ fp = os.fdopen(sys.__stdout__.fileno(), 'w', 10000000)) буферами на стороне Python. Влияние было ноль. то есть: все еще долгие задержки tty. Это заставило меня подумать / осознать, что вы просто откладываете медленную проблему tty ... когда буфер python, наконец, сбрасывает tty, кажется, все еще выполняет тот же общий объем обработки потока перед возвратом.
Расс
8
Обратите внимание, что этот ответ вводит в заблуждение и неправильно (извините!). В частности, неправильно говорить: «не так много места, чтобы сделать это быстрее [чем за 11 секунд]». Пожалуйста, смотрите мой собственный ответ на вопрос, где я показываю, что терминал wterm достиг того же результата 11 с за 0,26 с.
Расс
2
Расс: спасибо за отзыв! С моей стороны, больший fdopenбуфер (2 МБ) определенно имел огромное значение: время печати сократилось со многих секунд до 0,05 с, так же как и вывод файла (использование gnome-terminal).
Пи Дельпорт
88

Спасибо за все комментарии! Я закончил тем, что отвечал на это сам с вашей помощью. Чувствую себя грязно, когда отвечаешь на твой вопрос.

Вопрос 1: Почему медленная печать на стандартный вывод?

Ответ. Печать на стандартный вывод не является медленной. Это терминал, с которым ты работаешь медленно. И он практически не связан с буферизацией ввода-вывода на стороне приложения (например, буферизация файла python). Увидеть ниже.

Вопрос 2: это может быть ускорено?

Ответ: Да, это возможно, но, по-видимому, не со стороны программы (со стороны, выполняющей «печать» на стандартный вывод). Чтобы ускорить его, используйте другой эмулятор терминала.

Объяснение ...

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

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

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

Итак, если у вас есть долго работающий скрипт, который вы считаете медленным, и он изливает огромное количество текста на стандартный вывод ... попробуйте другой терминал и посмотрите, будет ли он лучше!

Обратите внимание, что я почти случайно вытащил wtermиз репозиториев Ubuntu / Debian. Эта ссылка может быть тем же терминалом, но я не уверен. Я не тестировал другие эмуляторы терминала.


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

Wterm 0,3 с
Атерм 0.3с
RXVT 0,3 с
mrxvt 0.4s
консоль 0.6s
Якуаке 0,7 с
lxterminal 7s
Xterm 9s
гном-терминал 12с
xfce4-терминал 12с
Вала-Терминал 18с
xvt 48s

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

В качестве бонуса это был интересный тур по различным эмуляторам терминала, доступным там! Я поражен, что мой первый «альтернативный» тест оказался лучшим из всех.

Russ
источник
1
Вы могли бы также попробовать aterm. Вот результаты моего теста с использованием вашего скрипта. Aterm - печать: 0,491 с, запись в файл (+ fsync): 0,110 с, печать с использованием stdout = / dev / null: 0,087 с wterm - печать: 0,521 с, запись в файл (+ fsync): 0,105 с, печать с использованием stdout = / dev / null: 0,085 с
frogstarr78
1
Как urxvt сравнивается с rxvt?
Дейнит
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 на tty.

Вероятно, что 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
Hasturkun
источник
Я не думал о том, чтобы python проверял свою выходную FS. Интересно, питон тянет трюк за кулисами? Я ожидаю, что нет, но не знаю.
Расс
+1 за указание на важнейшую разницу в буферизации
Питер Г.
@Russ: -uопционные силыstdin , stdoutи stderrбыть небуферизован, который будет медленнее , чем быть блок - буферном (из - за накладных расходов)
Hasturkun
4

Я не могу говорить о технических деталях, потому что я их не знаю, но меня это не удивляет: терминал не был предназначен для печати большого количества подобных данных. Действительно, вы даже предоставляете ссылку на множество графических элементов, которые он должен делать каждый раз, когда вы хотите что-то напечатать! Обратите внимание, что если вы вызываете скрипт pythonwвместо этого, это не займет 15 секунд; это полностью проблема с графическим интерфейсом. Перенаправить 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):
    ...
Katriel
источник
3

Печать в терминал будет медленной. К сожалению, кроме написания новой реализации терминала, я не могу понять, как вы значительно ускорите это.

shuttle87
источник
2

В дополнение к выводу, который, вероятно, по умолчанию использует режим линейной буферизации, вывод на терминал также заставляет ваши данные поступать в терминальную и последовательную линию с максимальной пропускной способностью или в псевдотерминал и отдельный процесс, который обрабатывает отображение цикл обработки событий, рендеринг символов из некоторого шрифта, перемещение битов дисплея для реализации прокрутки дисплея. Последний сценарий, вероятно, распространяется на несколько процессов (например, telnet-сервер / клиент, приложение терминала, сервер отображения X11), поэтому существуют проблемы переключения контекста и задержки.

Людвикас Букис
источник
Правда! Это побудило меня попробовать уменьшить размер окна моего терминала (в Gnome) до чего-то маленького (с 1920x1200). Конечно же ... 2,8 с время печати против 11,5 с. Намного лучше, но все же ... почему он останавливается? Можно подумать, что буфер stdout (hmm) может обрабатывать все 100 тыс. Строк, а дисплей терминала просто захватывает все, что может поместиться на экране, из хвостовой части буфера и делает это одним быстрым выстрелом.
Расс
Xterm (или gterm, в данном случае) отобразит ваш возможный экран быстрее, если он не подумает, что ему также нужно будет отображать все остальные выходные данные. Если бы он попытался пойти по этому пути, это, вероятно, сделало бы общий случай небольших обновлений экрана менее гибким. При написании этого типа программного обеспечения вы можете иногда иметь дело с ним, имея разные режимы и пытаясь определить, когда вам нужно перейти в / из небольшого к массовому режиму работы. Вы можете использовать cat big_file | tailили даже cat big_file | tee big_file.cpy | tailочень часто для этого ускорить.
Nategoose