Когда «лишний» cat действительно вреден — и когда он может помочь
Часто в оболочке (shell) удаление «лишнего» cat при работе с конвейерами (pipeline) — правильное решение: он добавляет лишний процесс и обычно снижает производительность. В то же время существует несколько частных случаев, когда именно параллельное чтение и запись через cat может сократить время выполнения программы.
В этой статье разберём практические сценарии: от простой типографической пользы cat до редких случаев, когда он уменьшает число системных вызовов и ускоряет пакетные задания. Сохраним важные примеры и цифры, приведённые в исходном материале.
Почему обычно лучше убрать «extra» cat
Каждый запуск cat добавляет процесс в pipeline, что обычно снижает пропускную способность и увеличивает накладные расходы. При простых перенаправлениях ввода/вывода (I/O redirection) и однопоточных программах cat редко даёт выигрыш в скорости.
Часто cat используется для удобства форматирования длинных команд или для читабельности скрипта, а не для производительности. Пример, где cat применяют ради удобства записи:
cat /some/very/extremely/super/long/full/path.txt |
egrep '(some|very|extremley|super|long|regex)' |
...
Альтернативный подход — использовать переменные окружения для читаемости:
FILE=...
REGEX=...
grep "$REGEX" "$FILE"
Специальный случай: когда cat может ускорить выполнение
Особый сценарий связан с интерактивными программами, которые выполняют много коротких вызовов printf(), что приводит к множественным коротким системным вызовам write(). В этом случае накладные расходы на переходы в ядро становятся заметными.
Если программа пишет в терминал (PTY), libc обычно определяет, что stdout — это терминал (через isatty()), и flush может происходить чаще, чтобы пользователь видел результаты сразу. Но если вывод перенаправлен в pipe, поведение буферизации меняется.
Пример с простым неэффективным чтением
Рассмотрим случай с программой, которая по одному байту читает stdin и делает простую операцию. Нефтьный пример на C:
#include <unistd.h>
#include <stdint.h>
int main(void) {
uint8_t x = 0;
unsigned char c;
while (read(STDIN_FILENO, &c, 1) == 1) {
x ^= c;
}
return x;
}
При таком цикле read(STDIN_FILENO, &c, 1) каждая итерация делает отдельный системный вызов read(), что медленно. Если на вход подавать данные через cat (prog | cat), то параллельное чтение и запись может позволить libc накопить больше данных в пользовательском буфере и тем самым уменьшить число системных вызовов.
Роль isatty() и поведения буферизации
Если stdout не является TTY (isatty() возвращает 0), стандартные библиотеки чаще используют блочную (полную) буферизацию. Это значит, что многие короткие printf() будут накапливаться в пользовательском буфере и лишь при заполнении этого буфера выполнится единичный системный вызов write().
В ситуациях prog и prog | cat это даёт заметный выигрыш: prog | cat уменьшает число переходов в ядро, экономит CPU на context switch и может завершить пакетную задачу быстрее. Типичный размер буфера здесь — около 4 KiB, поэтому «короткие» записи — это записи заметно меньшего размера, чем 4 KiB.
Буферизация в конвейерах: команда buffer и сетевые сценарии
Иногда добавление промежуточной буферизации в pipeline действительно полезно. Команда buffer специально создана для таких случаев — её man-страница содержит подробности. Buffer помогает сгладить различия между быстрым этапом чтения и более медленными этапами в конвейере.
Пример реального сценария «disk — network — disk»: чтение с диска, передача по SSH на удалённый сервер и запись на удалённый диск.
read_from_disk.sh | ssh server 'store_to_disk.sh'
Если вставить buffer, получаем:
read_from_disk.sh | buffer | ssh server 'store_to_disk.sh'
Или можно запускать buffer на удалённой стороне. Идея в том, что buffer позволяет держать receive window открытым и сглаживать «малые» и «большие» I/O операции, уменьшая влияние задержек сети и диска на общую пропускную способность.
Почему это важно на диске и в сети
У традиционных вращающихся жёстких дисков (Winchester drive) время поиска (seek) велико — порядка ~10 мс. Даже на SSD случайные чтения имеют дополнительные задержки, хотя и меньшие. При передаче данных по TCP пауза, сопоставимая с RTT, может закрыть receive window, и для его повторного открытия потребуется несколько RTT.
Это приводит к back-pressure: чтение с диска может блокироваться в сетевом write(), ожидая освобождения окна приёма. Добавление буфера в конвейере снижает вероятность таких пауз и может улучшить общую производительность пакетной передачи.
Практические рекомендации
Удаляйте «лишний» cat по умолчанию, если цель — максимальная производительность: он добавляет процессы и обычно снижает скорость. Используйте cat ради удобочитаемости команд, если это помогает поддерживать код и не влияет критично на производительность.
Если у вас есть пакетная задача с множеством коротких записей или смешанными узкими местами («disk — network — disk»), замерьте время выполнения и протестируйте варианты с buffer или с дополнительным cat. В ограниченных обстоятельствах дополнительная буферизация действительно может сократить число системных вызовов и уменьшить общее время выполнения.
Короткая сводка
cat обычно ухудшает производительность pipeline, но в редких случаях его использование или специальная буферизация (buffer) может ускорить задачу за счёт уменьшения количества системных вызовов write() и сглаживания разницы между этапами конвейера. Основные факторы: поведение isatty(), размер буфера (~4 KiB), сетевые задержки (RTT, receive window) и время seek для дисков (~10 мс для HDD).