Почему печать на stdout так медленно? Можно ли его ускорить?

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

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

Я написал script ('print_timer.py' в нижней части этого вопроса), чтобы сравнить время при написании строк 100k в stdout, в файл и с 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 или что-то еще, я сделал перенаправление вне 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

Итак, это не трюк python, это просто терминал. Я всегда знал, что вывод данных на /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

Это, конечно, лучше (~ 4x), но не меняет мой вопрос. Это только добавляет к моему вопросу, поскольку я не понимаю, почему рендеринг экрана терминала должен замедлять приложение, записывающее в stdout. Почему моя программа должна ждать продолжения рендеринга экрана?

Не созданы ли все приложения terminal/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

Ответ 1

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

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

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

Вопрос 2: Может ли он ускоряться?

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

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

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

-----
timing summary (100k lines each)
-----
print                         : 0.261 s
write to file (+fsync)        : 0.110 s
print with stdout = /dev/null : 0.050 s

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

Итак - если у вас длинный script, который вы чувствуете медленным, и он извергает огромное количество текста на stdout... попробуйте другой терминал и посмотрите, лучше ли это!

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


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

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

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

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

Ответ 2

Как может случиться так, что запись на физический диск происходит быстрее, чем запись на "экран" (предположительно, все оперативной памяти), и эффективно так же быстро, как просто сбрасывать мусор с помощью /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 строк.

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

Это ставит терминал на 11 секунд в перспективу, не так ли?

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

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

Ответ 3

Ваше перенаправление, вероятно, ничего не делает, поскольку программы могут определить, указывает ли их выходной 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

Ответ 4

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

Ответ 5

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

Ответ 6

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