[Перевод] Производительность консолей и оболочек
Есть хорошее демо MSR от 2012 года, которое показывает эффект времени отклика при работе на планшете. Если не хотите смотреть три минуты видео, они по сути создали устройство, которое симулирует произвольные задержки вплоть до доли миллисекунды. Задержка 100 мс (0,1 секунды), типичная для современных планшетов, выглядит ужасно. На 10 мс (0,01 секунды) задержка заметна, но уже можно нормально работать, а при задержке менее 1 мс всё просто идеально — как будто вы пишете карандашом по бумаге. Если хотите проверить это самостоятельно, возьмите любой Android-планшет со стилусом и сравните с нынешним поколением iPad Pro со стилусом Apple. У устройства Apple время отклика намного больше 10 мс, но разница всё равно кардинальная — она такая, что я реально использую новые iPad Pro для записи заметок и рисования диаграмм, в то время как Android-планшеты считаю совершенно неприемлемыми в качестве замены карандашу и бумаге.
Что-то похожее вы увидите в шлемах VR с разными задержками. 20 мс выглядит нормально, 50 мс лагает, а 150 мс уже непереносимо.
Странно, но редко приходится слышать жалобы на задержку ввода с клавиатуры или мыши. Казалось бы, причиной может быть то, что ввод с клавиатуры и мыши очень быстрый — и происходит практически мгновенно. Часто мне говорят, что так оно и есть, но я думаю, что ситуация совершенно обратная. Идея того, что компьютеры быстро реагируют на ввод данных — настолько быстро, что человек не замечает разницы — самое распространённое заблуждение, которое мне приходилось слышать от профессиональных программистов.
Когда тестеры измеряют реальную задержку от начала до конца в играх на нормальных компьютерных конфигурациях, то обычно выясняется, что задержка находится в диапазоне 100 мс.
Если посмотреть на распределение задержки в игровом конвейере, которую сделал Роберт Менцель, то несложно понять, откуда берутся 100+ мс:
- ~2 мс (мышь)
- 8 мс (среднее время ожидания начала обработки ввода игрой)
- 16,6 (симуляция игры)
- 16,6 (код рендеринга)
- 16,6 (GPU отрисовывает предыдущий кадр, текущий кадр кешируется)
- 16,6 (рендеринг GPU)
- 8 (среднее время несовпадения по vsync)
- 16,6 (кешрование фрейма в дисплее)
- 16,6 (перерисовка фрейма)
- 5 (переключение пикселей)
Обратите внимание, что здесь предполагается использование игровой мыши и довольно приличного ЖК-дисплея;, но на практике нередко можно увидеть гораздо большую задержку мыши и переключения пикселей.
Можно настроить систему и вписаться в диапазон 40 мс, но абсолютное большинство пользователей этого не делает. А даже если делают, это всё равно очень далеко от диапазона 10–20 мс, в котором планшеты и шлемы VR начинают вести себя «как следует».
Измерение задержки между нажатием клавиши и выводом на экран обычно производится в играх, потому что геймерам это важнее, чем большинству остальных людей, но я не думаю, что большинство других приложений сильно отличаются от игр по времени отклика. Хотя игры обычно делают больше работы на каждый фрейм, чем «типичные» приложения, они при этом гораздо лучше оптимизированы. Менцель отводит игре бюджет в 33 мс, в том числе половину для игровой логики и половину для отрисовки. Каково же время отклика в неигровых приложениях? Павел Фатин измерил его в текстовых редакторах и обнаружил задержки от нескольких миллисекунд до сотен миллисекунд — и он написал для проведения измерений специальное приложение, которое мы тоже можем использовать для оценки других приложений. Здесь используется java.awt.Robot для генерации нажатий клавиш и захвата экрана.
Лично я по определённым причинам хотел бы посмотреть на время отклика разных консолей и оболочек. Во-первых, я провожу значительную часть времени в консоли и обычно редактирую в ней, так что задержки на ввод здесь частично относятся на счёт консоли. Во-вторых, чаще всего (примерно на два порядка чаще) в качестве бенчмарка консолей приводится скорость выдачи текста, часто измеряемая путём запуска cat
на большом файле. Мне кажется, что это довольно бесполезный бенчмарк. Не могу вспомнить, когда в последний раз выполняемая мной задача была ограничена скоростью обработки файла командой cat
с выдачей stdout
в консоль (ну, если только я не использую eshell в emacs). И я не могу представить ни одной задачи, для которой подобное специализированное измерение было бы полезным. Ближайшая задача, которая для меня могла бы иметь значение — это скорость выполнения команды прерывания ^C
, когда я случайно отправил слишком большую выдачу в stdout
. Но как мы увидим из реальных измерений, способность консоли поглощать большое количество входных данных с выдачей в stdout
очень слабо относится к времени отклика на ^C
. Скорость прокрутки целой страницы вверх и вниз как будто имеет отношение, но в реальных измерениях эти два параметра не сильно коррелируют (например, emacs-eshell быстро осуществляет прокрутку, но исключительно медленно поглощает stdout
). Что ещё меня заботит — это время отклика, но информация о том, что какая-то определённая консоль быстро обрабатывает stdout
, мало говорит о её времени отклика.
Посмотрим на время отклика в некоторых консолях — добавляет ли какая-нибудь из них заметную задержку. Если измерять время отклика от нажатия клавиши до внутреннего захвата экрана на моём ноутбуке, то для разных консолей задержки следующие:
Эти графики показывают распределение задержек для разных консолей. По вертикальной оси — задержка в миллисекундах. По горизонтальной оси — процентиль (например, 50 означает, что 50% данных находятся ниже 50-го процентиля, то есть это среднее по медиане нажатие). Измерения проведены в macOS, если не указано иное. График слева соответствует незагруженной машине, а справа — под нагрузкой. Если смотреть только на средние по медиане значения, то некоторые терминалы выглядят неплохо — terminal.app и emacs-eshell примерно в районе 5 мс на незагруженной системе. Это достаточно мало, чтобы большинство людей не заметило задержки. Но большинство консолей (st, alacritty, hyper и iterm2) находятся в диапазоне, где пользователи уже могут заметить дополнительную задержку даже на незагруженной системе. Если же посмотреть на хвост графика, скажем, на отклик для 99,9-ного процентиля, то все консоли входят в диапазон, где дополнительная задержка должна быть заметна, в соответствии с исследованиями восприятия пользователями. Для сравнения, задержка между внутренне сгенерированным нажатием клавиши и попаданием в память GPU для некоторых консолей превышает время путешествия пакета из Бостона в Сиэтл и обратно, которое составляет около 70 мс.
Все измерения проведены при тестировании каждой консоли в отдельности, на полном заряде батареи без питания от кабеля A/C. Измерения под нагрузкой произведены при компиляции Rust (как и раньше, на полном заряде батареи без питания от кабеля A/C, а ради воспроизводимости результатов каждое измерение начиналось через 15 секунд после чистой сборки Rust после скачивания всех зависимостей, с достаточным временем между тестами для избежания помех от терморегуляции между тестами).
Если посмотреть на средние по медиане задержки под нагрузкой, то кроме emacs-term результаты остальных консолей не намного хуже, чем на незагруженной машине. Однако в хвосте графика, вроде 90-го процентиля или 99,9-го, каждая консоль становится гораздо менее отзывчивой. Переключение с macOS на Linux не слишком меняет картину, хотя на разных консолях по-разному.
Эти результаты гораздо лучше худшего сценария (на низком заряде аккумулятора, если подождать 10 минут с момента начала компиляции, чтобы усугубить помехи из-за терморегулирования, то там задержки бывают и в сотни миллисекунд), но даже так у каждой консоли задержка в хвосте графика должна быть заметна человеку. Также помните, что это лишь доля общего времени отклика от начала и до конца конвейера обработки ввода и вывода на экран.
Почему же люди не жалуются на задержку между вводом с клавиатуры и выводом на экран так, как они жалуются на задержку при рисовании стилусом или в шлемах VR? Моя теория состоит в том, что для VR и планшетов у людей есть большой опыт работы в аналогичных «приложениях» с гораздо меньшей задержкой. Для планшетов таким «приложением» является карандаш и бумага, а для виртуальной реальности — обычный мир вокруг, в котором мы тоже поворачиваем голову, но только без шлема VR. Но время отклика между вводом с клавиатуры и выводом на экран настолько велико во всех приложениях, что большинство людей просто принимают большую задержку как данность.
Альтернативная теория может быть в том, что ввод с клавиатуры и мыши фундаментально отличается от ввода на планшете, из-за чего задержка становится менее заметной. Даже без моих данных такая теория кажется неправдоподобной, потому что когда я подключаюcь через удалённый терминал с десятками лишних миллисекунд, то я чувствую заметный лаг при нажатии клавиш. И известно, что при добавлении дополнительной задержки в A/B-тестировании люди могут заметить и действительно замечают задержку в диапазоне, который мы обсуждали ранее.
Итак, если мы хотим сравнить самый популярный бенчмарк (производительность stdout) с задержкой в разных консолях, то давайте измерим, насколько быстро разные консоли обрабатывают входные данные для выдачи в stdout:
Консоль | stdout (МБ/с) |
idle50 (мс) |
load50 (мс) |
idle99.9 (мс) |
load99.9 (мс) |
mem (МБ) |
^C |
---|---|---|---|---|---|---|---|
alacritty | 39 | 31 | 28 | 36 | 56 | 18 | ok |
terminal.app | 20 | 6 | 13 | 25 | 30 | 45 | ok |
st | 14 | 25 | 27 | 63 | 111 | 2 | ok |
alacritty tmux | 14 | ||||||
terminal.app tmux | 13 | ||||||
iterm2 | 11 | 44 | 45 | 60 | 81 | 24 | ok |
hyper | 11 | 32 | 31 | 49 | 53 | 178 | fail |
emacs-eshell | 0.05 | 5 | 13 | 17 | 32 | 30 | fail |
emacs-term | 0.03 | 13 | 30 | 28 | 49 | 30 | ok |
Взаимосвязь между производительностью stdout
и тем, насколько быстрой выглядит консоль, неочевидна. В этом тесте terminal.app внешне выглядел очень плохо. При прокрутке текст передвигался рывками, как будто экран редко обновляется. Проблемы наблюдались также у hyper и emacs-term. Emacs-term вообще не успевал за выдачей — после окончания теста ему требовалось несколько секунд, чтобы обновиться до конца (строка состояния, которая показывает количество вышедших строчек, вроде была актуальной, так что число перестало увеличиваться до окончания теста). Hyper отстал ещё больше и моргнув пару разу практически не обновлял экран. Процесс Hyper Helper
искусственно поддерживался при 100%-ной загрузке CPU в течение примерно двух минут, а консоль всё время абсолютно не реагировала на действия.
Alacritty тестировался с менеджером tmux, поскольку эта консоль не поддерживает прокрутку назад вверх, и в документации указано, что для этого следует использовать tmux. Просто для сравнения terminal.app тоже тестировался с tmux. В большинстве консолей tmux вроде бы не уменьшает скорость stdout
, но alacritty и terminal.app оказались достаточно быстрыми, чтобы в реальности их производительность всё-таки ограничивалась скоростью tmux.
Emacs-eshell технически не является консолью, но я также протестировал eshell, поскольку эту программу в некоторых случаях можно использовать как замену консоли. На самом деле Emacs, как с eshell, так и с term, оказался настолько медленным, что уже неважно, с какой реально скоростью он выдаёт stdout
. В прошлом при использовании eshell или term мне иногда приходилось ждать прокрутки нескольких тысяч строк текста, если запустить команду с подробным журналированием в stdout
или stderr
. Поскольку такое происходит довольно редко, для меня это не слишком большая проблема, пока задержка не достигает 0,5 или 1 секунды, хотя в любой другой консоли всё работает нормально.
И наоборот, я набираю символы достаточно быстро, чтобы заметить задержку длинного хвоста. Например, если я набирают 120 слов в минуту, то есть 10 символов в секунду, то хвост из 99,9-го процентиля (1 из 1000) будет проявляться каждые 100 секунд!
В любом случае, вместо «бенчмарка» cat
меня больше заботит, могу ли я прервать процесс по ^C
, если случайно запустил команду с миллионами строчек вывода на экран вместо тысяч строчек. Этот тест проходит практически каждая консоль, кроме hyper и emacs-eshell — они обе зависают минимум на десять минут (после десяти минут я убиваю задачи и больше не жду окончания процесса).
В таблицу также включено использование памяти при загрузке программы, поскольку я видел, что этот параметр люди тоже часто используют при тестировании консолей. Хотя мне кажется немного странным, что консоли при загрузке могут занимать 40 МБ в памяти, но даже на трёхлетнем бэушном ноутбуке у меня установлено 16 ГБ ОЗУ, так что оптимизация этих 40 МБ до 2 МБ никак особо не повлияет на работу с программой. Блин, даже на «хромбуке» за $300, который мы недавно купили, установлено 16 ГБ ОЗУ.
Заключение
В большинстве консолей достаточно большое время отклика, которое можно оптимизировать для улучшения впечатления пользователей от работы с программой, если бы разработчики сконцентрировались на этом параметре, а не на добавлении новых функций или других аспектах производительности. Но когда я искал бенчмарки консолей, то обнаружил, что если авторы программ и замеряли производительность чего-либо, то это была или скорость выдачи в stdout, или использование памяти при загрузке. Это печально, поскольку большинство «медленных» консолей уже выдают stdout
на несколько порядков быстрее, чем люди способны понять, так что дальнейшая оптимизация скорости stdout
относительно слабо влияет на реальное удобство работы для большинства пользователей. То же самое можно сказать по поводу сокращения использования памяти при загрузке, если консоль использует 0,01% памяти на моём старом ноутбуке или на современной дешёвой модели.
Если вы работаете в консоли, то для вас может быть важнее относительно бóльшая оптимизация времени отклика и интерактивности (например, реакция на ^C
) и относительно меньшая оптимизация пропускной способности и использовании памяти при загрузке.
Обновление. В ответ на эту статью автор alacritty объяснил, откуда возникает задержка alacrity, и описал, как её можно уменьшить.
Приложение: отрицательные результаты
Tmux и задержка. Я испытывал менеджер tmux с разными консолями и обнаружил, что разница находится в пределах погрешности измерения.
Оболочки и задержка. Я проверял разные оболочки, но даже в самой быстрой консоли разница между ними находилась в пределах погрешности измерения. В моей экспериментальной установке было немного сложно проверить Powershell, потому что он некорректно обрабатывает цвета (первый набранный символ набирается цветом, установленным в консоли, но остальные символы жёлтые независимо от настроек, этот баг вроде собираются закрыть), что сбивает настройку распознавания изображений, которую я использовал. Powershell также не всегда помещает курсор в нужное место — тот случайным образом прыгает по строке, что тоже сбивает настройку распознавания изображений. Но несмотря на эти проблемы, у Powershell производительность вполне сравнимая с другими оболочками.
Оболочки и пропускная способность stdout. Как и в предыдущих случаях, разница между разными оболочками находится в пределах погрешности измерения.
Однострочный и многострочный текст и пропускная способность. Хотя некоторые текстовые редакторы работают с исключительно длинными строками, пропускная способность практически не изменяется, либо я запихивал в консоль файл с одной такой строкой, либо она была разбита на строки по 80 символов.
Блокировка очереди / ошибка пропуска данных. Эти тесты я запускал на скорости ввода 10,3 символа в секунду. Но выяснилось, что скорость ввода не оказывает особого влияния на задержку. Теоретически консоль можно переполнить, и hyper первым начал сбоить на очень высоких скоростях ввода, но эти скорости намного превышают скорость ввода текста у людей, которые мне известны.
Приложение: экспериментальная установка
Все тесты проводились на двухъядерном Macbook Pro 13» 2,6 ГГц середины 2014 года. У этой машины 16 ГБ оперативной памяти и разрешение экрана 2560×1600 символов. OS X версии 10.12.5. Некоторые тесты проводились в Linux (Lubuntu 16.04) для сравнения macOS и Linux. Каждое измерение задержки ограничивалось 10 тысячами нажатий клавиш.
Измерения проводились путём нажатия кнопки .
с выдачей в кодировке по умолчанию base32
, то есть простом тексте ASCII. Джордж Кинг обратил внимание, что разные типы текста могут влиять на скорость выдачи:
Я заметил, что Terminal.app кардинально замедляется при выдаче нелатинских кодировок. Думаю, тому могут быть три причины: необходимость загрузки различных страниц шрифтов, необходимость разбирать кодовые точки за пределами Basic Multilingual Plane (BMP) и символы в многобайтовой кодировке Юникода.Вероятно, первое сводится к очень сложному сочетанию отложенной загрузки глифов шрифтов, вычислений резервных шрифтов и кеширования страниц глифов или иному способу, как это делается.
Второе носит немного умозрительный характер, но я бы предположил, что Terminal.app использует Cocoa NSString на базе UTF16, что почти наверняка приводит к подтормаживанию, если кодовые точки выше BMP из-за суррогатных пар.
Консоли разворачивались на весь экран перед запуском тестов. Это влияет на результат, а изменение размера окна консоли может значительно изменить производительность (например, можно сделать hyper значительно медленнее iterm2, изменив размер окна при остальных неизменных факторах). st на macOS запускался как клиент X под XQuartz. Для проверки версии, что XQuartz по своей сути медленно работает, я попробовал runes, другую «нативную» консоль Linux, которая использует XQuartz. Оказалось, что у runes гораздо меньшая задержка в хвосте, чем у st и iterm2.
Тесты задержки на «незагруженной» системе проводились сразу после перезагрузки системы. Все терминалы были открыты, но текст вводился только в один из них.
Тесты «под нагрузкой» проводились во время фоновой компиляции Rust, через 15 секунд после начала компиляции.
Тесты пропускной способности консоли осуществлялись путём создания большого файла с псевдослучайным текстом:
timeout 64 sh -c 'cat /dev/urandom | base32 > junk.txt'
с последующим запуском
timeout 8 sh -c 'cat junk.txt | tee junk.term_name'
Terminator и urxvt не тестировались, поскольку их установка на macOS представляет собой нетривиальную процедуру и я не хотел возиться, пытаясь заставить их работать. Terminator легко собрать из исходников, но он зависает при загрузке и не показывает командную строку. Urxvt устанавливается через brew, но одна из его зависимостей (которая тоже устанавливается через brew) была неправильной версии, из-за чего консоль не загружалась.