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

Я всегда удивлялся / расстраивался из-за того, сколько времени требуется, чтобы просто вывести на терминал оператор 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, который можно изменить со стороны приложения, чтобы сделать это лучше для программиста. Если это строго проблема с терминальным приложением, то, возможно, это даже не относится к Stackru?

Что мне не хватает?


Вот программа на 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

6 ответов

Решение

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

Вопрос 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, очевидно.

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

Как может быть так, что запись на физический диск является ПУТЬ быстрее, чем запись на "экран" (предположительно операционная система все-ОЗУ), и эффективно так же быстро, как простой вывод в мусор с помощью /dev/null?

Поздравляем, вы только что обнаружили важность буферизации ввода / вывода.:-)

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

Терминал, с другой стороны, практически не выполняет буферизацию: каждый отдельный 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 секунд терминала, не так ли?

Таким образом, чтобы ответить на ваш первоначальный вопрос, запись в терминал на самом деле невероятно быстрая, учитывая все обстоятельства, и не так уж много места, чтобы сделать это намного быстрее (но отдельные терминалы различаются в зависимости от того, сколько они выполняют; см. Комментарий Русса к этому). ответ).

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

Ваше перенаправление, вероятно, ничего не делает, так как программы могут определить, указывает ли их выходной FD на tty.

Скорее всего, stdout буферизуется при указании на терминал (так же, как 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

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

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

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

Другие вопросы по тегам