конструкцию с наименьшей возможной задержкой и игнорировать затраты полосы пропускания до тех пор, пока профайлеры не укажут обратное. Проблемы, связанные с полосой пропускания, можно решить позднее при разработке с помощью таких технических приемов, как сжатие данных протокола на лету. Однако освободиться от высокой задержки, встроенной в существующую конструкцию, гораздо труднее (часто практически невозможно).
Несмотря на то, что данный эффект наиболее четко проявляется в конструкции сетевых протоколов, компромисс между пропускной способностью и задержкой является гораздо более общим феноменом. При написании приложений программист иногда сталкивается с необходимостью выбора: однократное выполнение дорогостоящих вычислений в расчете на то, что результаты будут использоваться несколько раз, или выполнение вычислений только в случае действительной необходимости (даже если это означает частое перевычисление результатов). В большинстве подобных случаев правильный подход склоняется в сторону низкой задержки. То есть не следует пытаться выполнить дорогостоящее предвычисление в случае, если нет определенных требований к пропускной способности или если изменения не показывают слишком низкую пропускную способность. Предвычисления могут показаться эффективными, поскольку они минимизируют общее использование процессорных циклов, но процессорные циклы дешевы. Если создается простая программа, а не одно из немногочисленных гигантских приложений с интенсивными вычислениями, например, для анализа больших массивов информации, визуализации анимации или упомянутое выше модулирование взрывов, то обычно наилучший путь — предпочесть небольшое время запуска и быстрый отклик.
В ранние дни Unix данную рекомендацию сочли бы еретической. В то время процессоры были гораздо медленнее, а соотношения затрат сильно отличались. Кроме того, модель использования Unix больше склонялась к серверным операциям. Отчасти отметить значение низкой задержки необходимо потому, что даже более молодые Unix-разработчики иногда наследуют давние культурные предубеждения по поводу оптимизации по пропускной способности. Однако времена изменились.
Разработаны три общие стратегии сокращения задержки: (а) пакетные транзакции, способные распределить начальные затраты, (b) разрешение совмещения транзакций и (с) кэширование.
12.4.1. Пакетные операции
Графические API-интерфейсы часто пишутся в предположении, что фиксированные начальные затраты для обновления физического экрана достаточно высоки. Следовательно, операции записи фактически модифицируют внутренний буфер. Программисту придется решить, когда накоплено достаточное количество обновлений, и использовать вызов, который превратит их в обновление физического экрана. Верный выбор промежутка времени между физическими обновлениями может создавать значительные различия в восприятии графических клиентов. Таким способом организованы как X-сервер, так и библиотека
Постоянные служебные демоны представляют собой более характерный для Unix пример пакетирования. Существует две причины для написания постоянно работающих демонов (в противоположность CLI-серверам, которые запускаются заново для каждого сеанса) — очевидная и неочевидная. Очевидной причиной является управление обновлениями общего ресурса. Менее очевидная причина, которая имеет место даже для демонов, не обрабатывающих обновления, заключается в том, что при многочисленных запросах сокращаются затраты на чтение в базе данных демона. Идеальным примером такого демона является DNS-служба
12.4.2. Совмещение операций
В главе 5 сравнивались протоколы POP3 и IMAP для опроса удаленных почтовых серверов. При этом было отмечено, что IMAP-запросы (в отличие от POP3-запросов) маркируются идентификатором запроса, сгенерированным клиентом. Сервер, отправляя обратно ответ, включает в него метку запроса, к которому относится данный ответ.
POP3-запросы должны обрабатываться клиентом и сервером в строгом порядке. Клиент отправляет запрос, ожидает ответ на данный запрос и только после его получения может подготовить и отправить следующий запрос. IMAP-запросы, с другой стороны, маркируются, поэтому их передачу можно совместить. Если IMAP-клиенту известно, что требуется доставить несколько сообщений, то он может отправить IMAP- серверу поток из нескольких запросов на доставку (каждый со своей меткой), не ожидая ответов между ними. Маркированные ответы отправятся обратно, как только сервер будет готов. Ответы на более ранние запросы могут поступить в то время, когда клиент все еще отправляет более поздние запросы.
Описанная стратегия является распространенной не только в области сетевых протоколов. Когда требуется сократить задержку, блокировка или ожидание немедленных результатов — крайне неэффективные методы.
12.4.3. Кэширование результатов операций
Иногда можно получить оба преимущества (низкую задержку и хорошую пропускную способность) путем вычисления дорогостоящих результатов по мере необходимости и их кэширования для последующего использования. Выше было сказано, что в службе
Кэширование имеет собственные проблемы и компромиссные решения, которые хорошо иллюстрируются одним примером: использование двоичного кэша для устранения издержек синтаксического анализа, связанного с файлами текстовых баз данных. В некоторых вариантах операционной системы Unix данная методика использовалась для ускорения доступа к парольной информации (обычным обоснованием было сокращение задержки при регистрации в системе на очень крупных узлах).
Для того чтобы обеспечить соответствующую отдачу от использования данной методики, необходимо чтобы весь код, обращающийся к бинарному кэшу, проверял временные метки на обоих файлах и обновлял кэш в случае, если мастер-текст имеет более позднюю метку. С другой стороны, все изменения мастер-текста должны выполнятся посредством упаковщика, который обновляет двоичный формат.
Несмотря на то, что данный подход оказывается работоспособным, он обладает всеми недостатками, которые можно ожидать при нарушении правила SPOT. Дублирование данных означает, что в результате не будет никакой экономии пространства, т.е. это исключительно оптимизация скорости. Однако реальная проблема данного подхода заключается в том, что код для обеспечения когерентности между кэшем и мастер-текстом печально известен как 'текучий' и чреватый ошибками. Очень частое обновление файлов кэша может привести к неочевидной конкуренции просто ввиду односекундного разрешения временных меток.
Но когерентность все-таки можно гарантировать в простых случаях. Одним из них является интерпретатор языка Python, который компилирует и сохраняет на диске файл p-кода с расширением .pyc
, когда файл библиотеки Python импортируется впервые. При последующих проходах