Введение
Совет «не использовать лишний cat» знаком многим системным администраторам и разработчикам: обычно вызов cat в начале pipeline с одним аргументом снижает производительность. Тем не менее встречается вопрос — существует ли спецслучай, когда именно из‑за параллельного чтения и записи cat действительно ускорит выполнение программы?
В этой статье мы рассмотрим возможные сценарии: от очевидных ошибок в чтении по одному байту до эффектов буферизации вывода (isatty) и использования утилиты buffer. Сохраним примеры и цифры, чтобы понять, когда cat может помочь, а когда — лишь усложнить конвейер.
Почему обычно «cat» лишний
Часто удаление «лишнего» cat оправдано — это простой способ сократить ненужные процессы в Bourne-скриптах. Часто cat используется лишь по удобству форматирования длинных строк конвейера, например:
cat /some/very/extremely/super/long/full/path.txt |
egrep '(some|very|extremley|super|long|regex)' |
...
Альтернативой может быть использование переменных для читаемости:
FILE=...
REGEX=...
grep "$REGEX" "$FILE"
Таким образом cat чаще применяется для удобства человека, а не для улучшения производительности.
Случай с неэффективным чтением по одному байту
Рассмотрим пример программы с «неэффективной» функцией чтения, которая делает read() по одному байту в цикле. Такой код всегда будет тратить лишние CPU‑циклы, независимо от того, стоит ли перед ним cat в pipeline:
#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() на 1 байт. При этом драйвер и слой ОС обычно эффективно работают: underlying read() и драйвер как минимум передают 4 KiB за один раз и могут выполнять read‑ahead, чтобы поддерживать потребителя данных.
Таким образом, наличие cat в pipeline на самом деле не изменит фундаментальную неэффективность такого алгоритма; правильнее увеличить длину буфера чтения, чтобы уменьшить количество sys call и CPU‑переключений.
Когда cat может экономить ресурсы: эффект isatty и буферизация вывода
Есть реальный сценарий, в котором cat может уменьшить время выполнения: когда интерактивная программа делает много коротких printf(), каждый из которых вызывает write() в ядро. При выводе в pty (терминал) libc часто сбрасывает буфер при каждой новой строке, чтобы пользователь сразу видел результаты.
Если же запустить prog | cat, то isatty() для stdout вернёт «не TTY», и библиотечная буферизация приведёт к накоплению нескольких коротких записей в пользовательском буфере. В результате совокупное число системных вызовов write() уменьшается, падает число контекст‑переключений и программа может завершиться быстрее.
Эквивалентный эффект даёт перенаправление вывода в файл:
prog > log.txt
Часто речь идёт о буферах порядка 4 KiB, поэтому «короткие» записи — это записи заметно меньше 4 KiB. В таких случаях вставка cat в pipeline может принести выигрыш по CPU‑циклам и времени выполнения.
Буферизация с помощью утилиты buffer и сценарии «диск — сеть — диск»
Для пакетных задач, где несколько звеньев конвейера могут быть узким местом, полезна явная буферизация с помощью утилиты buffer. Пример реального сценария: чтение с диска, передача по SSH и запись на удалённый диск:
read_from_disk.sh | ssh server 'store_to_disk.sh'
Вставляя buffer, можно сгладить дискретность операций и уменьшить взаимную блокировку между чтением и записью:
read_from_disk.sh | buffer | ssh server 'store_to_disk.sh'
Или запускать buffer на удалённой стороне. Это особенно важно при смешении «малых» и «больших» I/O операций: seek на вращающемся (Winchester) диске стоит примерно ~10 ms, а при передаче TCP через WAN паузы на одну RTT могут закрывать окно приёма и создавать обратное давление на чтение с диска.
Даже при SSD остаётся штраф за случайные перемещения, хоть и меньший. Правильная буферизация помогает поддерживать приёмное окно TCP открытым и уменьшает блокировки в write() при записи через сеть.
Практические рекомендации
Не используйте лишний cat ради оптимизации, если нет явной причины: чаще это ухудшит ясность и добавит процесс. Однако cat может помочь в двух реальных случаях: при интерактивных программах с множеством коротких printf(), где isatty() переключает режим буферизации, и при выравнивании потоков данных в сложных pipeline, где явная буферизация снижает влияние сетевых или дисковых задержек.
Если есть сомнения, измеряйте время выполнения и нагрузку: определите узкое место, сделайте профилирование количества системных вызовов и используйте buffer или увеличьте размер буфера в программе (например, читать не по 1 байту, а крупнее). Эти меры сохранят производительность и уменьшат ненужные CPU‑циклы.