Бесполезное использование кошки?
это, вероятно, во многих часто задаваемых вопросах-вместо использования:
cat file | command
(что называется бесполезное использование кошки), правильный способ должен быть:
command < file
во втором, "правильном" способе - ОС не должна порождать дополнительный процесс.
Несмотря на это, я продолжал использовать бесполезную кошку по двум причинам.
более эстетично-мне нравится, когда данные перемещаются равномерно только слева направо. И его легче заменить
cat
С чем-то еще (gzcat
,echo
, ...), добавить 2-й файл или вставить новый фильтр (pv
,mbuffer
,grep
...).Я "чувствовал", что в некоторых случаях это может быть быстрее. Быстрее, потому что есть 2 процесса, 1-й (
cat
) делает чтение, а второй делает все, что угодно. И они могут работать параллельно, что означает иногда более быстрое выполнение.
является ли моя логика правильной (по 2-й причине)?
8 ответов:
я не знал до сегодня, когда какой-то новичок попытался подколоть UUOC на меня за один из моих ответов. Это было
cat file.txt | grep foo | cut ... | cut ...
. Я дал ему часть своего ума, и только после этого посетил ссылку, которую он дал мне, ссылаясь на происхождение награды и практику этого. Дальнейшие поиски привели меня к этому вопросу. К сожалению, несмотря на сознательное рассмотрение, ни один из ответов не включал мое обоснование.я не хотел быть защищаясь в ответ на него. В конце концов, в мои молодые годы я бы написал команду
grep foo file.txt | cut ... | cut ...
потому что всякий раз, когда вы делаете часто одинgrep
s вы изучаете размещение аргумента файла, и это готовое знание, что первый-это шаблон, а последующие-имена файлов.это был сознательный выбор, чтобы использовать
cat
когда я ответил на вопрос, отчасти из-за причины "хорошего вкуса" (по словам Линуса Торвальдса), но главным образом для убедительного причина функции.последняя причина более важна, поэтому я сначала потушу ее. Когда я предлагаю трубопровод в качестве решения, я ожидаю, что он будет многоразовым. Вполне вероятно, что трубопровод будет добавлен в конце или соединен с другим трубопроводом. В этом случае наличие аргумента файла для grep завинчивает возможность повторного использования, и вполне возможно сделать это молча без сообщения об ошибке, если аргумент файл существует. То есть
grep foo xyz | grep bar xyz | wc
даст вам, сколько строк вxyz
содержатьbar
в то время как вы ожидаете количество строк, которые содержат обаfoo
иbar
. Необходимость изменения аргументов для команды в конвейере перед ее использованием может привести к ошибкам. Добавьте к этому возможность молчаливых неудач, и это станет особенно коварной практикой.первая причина тоже не маловажна, так как много "хорошего вкуса" просто является интуитивным подсознательным обоснованием таких вещей, как тихие неудачи выше, которые вы не можете думать правильно в тот момент, когда какой-то человек, нуждающийся в образовании, говорит: "но разве эта кошка не бесполезна".
тем не менее, я постараюсь также осознать прежнюю причину "хорошего вкуса", о которой я упоминал. Эта причина связана с ортогональным духом дизайна Unix.
grep
неcut
иls
неgrep
. Поэтому по крайней мереgrep foo file1 file2 file3
идет вразрез с духом конструкции. Ортогональный способ сделать этоcat file1 file2 file3 | grep foo
. Теперь,grep foo file1
это просто частный случайgrep foo file1 file2 file3
, а если вы не относитесь к нему так же, как вы, по крайней мере, используете циклы мозговых часов, пытаясь избежать бесполезной награды cat.это приводит нас к аргументу, что
grep foo file1 file2 file3
это сцепление, иcat
сцепляется так, что это правильноcat file1 file2 file3
а потому чтоcat
не сцепляется вcat file1 | grep foo
поэтому мы нарушаем дух обоихcat
и всемогущий Unix. Ну, если бы это было так, то Unix понадобилась бы другая команда, чтобы прочитать вывод одного файла и выплюнуть его stdout (не разбивайте его на страницы или что-то еще просто чистый плевок в stdout). Так что у вас будет ситуация, когда вы говоритеcat file1 file2
илиdog file1
и добросовестно помните, чтобы избежатьcat file1
чтобы избежать получения награды, а также избежатьdog file1 file2
С тех пор, надеюсь, дизайнdog
выдаст ошибку, если указано несколько файлов.надеюсь, в этот момент Вы сочувствуете дизайнерам Unix за то, что не включили отдельную команду, чтобы выплюнуть файл в stdout, а также именование
cat
для конкатенации вместо того, чтобы дать ему какое-то другое имя.<edit>
удалены некорректные комментарии<
, фактически<
является эффективным средством без копирования, чтобы выплюнуть файл в stdout, который вы можете разместить в начале конвейера, поэтому разработчики unix включили что-то специально для этого</edit>
следующий вопрос: почему важно иметь команды, которые просто плюют файл или конкатенацию нескольких файлы в stdout, без дальнейшей обработки? Одна из причин заключается в том, чтобы не иметь каждую команду Unix, которая работает на стандартном входе, чтобы знать, как проанализировать хотя бы один аргумент файла командной строки и использовать его в качестве ввода, если он существует. Вторая причина заключается в том, чтобы пользователи не помнили: (a) где идут аргументы имени файла; и (b) избегайте ошибки беззвучного конвейера, как упоминалось выше.
это подводит нас к тому, почему
grep
имеет дополнительную логику. Обоснование заключается в том, чтобы позволить пользователю беглость для команд, которые используются часто и на автономной основе (а не как конвейер). Это небольшой компромисс ортогональности для значительного повышения удобства использования. Не все команды должны быть разработаны таким образом, и команды, которые не часто используются, должны полностью избегать дополнительной логики аргументов файла (помните, что дополнительная логика приводит к ненужной хрупкости (возможность ошибки)). Исключение-разрешить аргументы файла, как в случаеgrep
. (кстати Кстати обратите внимание, чтоls
имеет совершенно другую причину не просто принять, но в значительной степени требуют аргументов файла)наконец, что можно было бы сделать лучше, если бы такие исключительные команды, как
grep
(но не обязательноls
) генерирует ошибку, если стандартный ввод также доступен при указании аргументов файла. Это разумно, потому что команды включают логику, которая нарушает ортогональный дух Unix для удобства пользователя. Для дальнейшего удобства пользователя, то есть для предотвращения страданий, вызванных молчаливым отказом, такие команды не должны колебаться нарушать свое собственное нарушение, имея дополнительную логику, чтобы предупредить пользователя, если есть возможность молчаливой ошибки.
Неа!
прежде всего, не имеет значения, где в команде происходит перенаправление. Поэтому, если вам нравится перенаправление слева от вашей команды, это нормально:
< somefile command
это то же самое, что
command < somefile
во-вторых, есть n + 1 процессы и подоболочка происходит, когда вы используете трубу. Это определенно медленнее. В некоторых случаях n было бы равно нулю (например, когда вы перенаправляете на встроенную оболочку), поэтому используя
cat
вы добавляете новый процесс совершенно излишне.как обобщение, всякий раз, когда вы обнаружите, что используете трубу, стоит потратить 30 секунд, чтобы увидеть, можете ли вы ее устранить. (Но, вероятно, не стоит брать намного больше, чем 30 секунд.) Вот несколько примеров, когда трубы и процессы часто используются без необходимости:
for word in $(cat somefile); … # for word in $(<somefile); … (or better yet, while read < somefile) grep something | awk stuff; # awk '/something/ stuff' (similar for sed) echo something | command; # command <<< something (although echo would be necessary for pure POSIX)
не стесняйтесь редактировать, чтобы добавить больше примеров.
С версией UUoC,
cat
должен прочитать файл в память, затем записать его в канал, и команда должна прочитать данные из канала, поэтому ядро должно скопировать весь файл три раз, тогда как в перенаправленном случае ядро должно скопировать файл только один раз. Быстрее сделать что-то один раз, чем сделать это три раза.использование:
cat "$@" | command
это совершенно другое и не обязательно бесполезное использование
cat
. Это по-прежнему бесполезно, если команда является стандартным фильтром, который принимает ноль или более аргументов имени файла и обрабатывает их по очереди. Рассмотримtr
command: это чистый фильтр, который игнорирует или отклоняет аргументы имени файла. Чтобы передать ему несколько файлов, вы должны использоватьcat
как показано на рисунке. (Конечно, есть отдельное обсуждение, что конструкцияtr
не очень хорошо; нет никакой реальной причины, по которой он не мог быть разработан как стандартный фильтр.) Это также может быть действительным, если вы хотите команда для обработки всех входных данных как один файл, а не как несколько отдельных файлов, даже если команда будет принимать несколько отдельных файлов: например,wc
такая команда.это
cat single-file
случай, который безусловно бесполезен.
Я не согласен с большинством случаев чрезмерно самодовольной награды UUOC, потому что, когда вы учите кого-то другого,
cat
является удобным местом-держателем для любой команды или жесткого сложного конвейера команд, которые производят вывод, подходящий для обсуждаемой проблемы или задачи.это особенно верно на таких сайтах, как Stack Overflow, ServerFault, Unix & Linux или любой из сайтов SE.
если кто-то специально спрашивает об оптимизации, или если вы хотите добавить дополнительная информация об этом тогда, отлично, расскажет о том, как использовать cat неэффективно. Но не ругайте людей, потому что они решили стремиться к простоте и легкости понимания в своих примерах, а не смотреть на меня, как я крут! сложность.
короче говоря, потому что кошка не всегда кошка.
кроме того, потому что большинство людей, которым нравится ходить вокруг награждения UUOCs, делают это, потому что они больше озабочены тем, чтобы показать, насколько они "умны", чем они помогают или обучение людей. На самом деле, они демонстрируют, что они, вероятно, просто еще один новичок, который нашел крошечную палку, чтобы бить своих сверстников.
обновление
вот еще один UUOC, который я опубликовал в ответе на https://unix.stackexchange.com/a/301194/7696:
sqlq() { local filter filter='cat' # very primitive, use getopts for real option handling. if [ "" == "--delete-blank-lines" ] ; then filter='grep -v "^$"' shift fi # each arg is piped into sqlplus as a separate command printf "%s\n" "$@" | sqlplus -S sss/eee@sid | $filter }
педанты UUOC сказали бы, что это UUOC, потому что это легко можно сделать
$filter
по умолчанию пустая строка и естьif
заявлениеfilter='| grep -v "^$"'
но Имо, не вставляя символ трубы в$filter
этот "бесполезный"cat
служит чрезвычайно полезной цели самодокументирования того факта, что$filter
наprintf
line-это не просто еще один аргумент дляsqlplus
, это дополнительный выбираемый пользователем выходной фильтр.если есть необходимость иметь несколько дополнительных выходных фильтров, обработка параметров может просто добавить
| whatever
до$filter
так часто, как это необходимо - один дополнительныйcat
в конвейере нет это может повредить что-либо или вызвать заметную потерю производительности.
дополнительная проблема заключается в том, что труба может молча маскировать подоболочку. Для этого примера я заменю
cat
Сecho
, но та же проблема существует.echo "foo" | while read line; do x=$line done echo "$x"
вы могли бы ожидать
x
содержатfoo
, но это не так.x
вы установили был в подрешетке породил для выполненияwhile
петли.x
в оболочке, которая запустила конвейер, имеет несвязанное значение или вообще не задано.в bash4, вы можете настроить некоторые параметры оболочки так что последняя команда конвейера выполняется в той же оболочке, что и та, которая запускает конвейер, но тогда вы можете попробовать это
echo "foo" | while read line; do x=$line done | awk '...'
и
x
снова локально дляwhile
's-подуровне.
сценарий оболочки очень похож на язык копирования/вставки. Для большинства людей, которые пишут скрипты, они не в нем, выучить язык, это просто препятствие, они должны преодолеть, чтобы продолжать делать вещи на языке(языках) они вообще-то знакомы.
в этом контексте я рассматриваю его как разрушительный и потенциально даже разрушительно распространять различные анти-шаблоны сценариев оболочки. Код, который кто-то находит при переполнении стека, в идеале должен быть возможен для копирования/вставки в их среду с минимальными изменениями и неполным пониманием.
редактирование кода может быть проблематично потому что легко внести изменения, которые не были предназначены автором кода. Поэтому мы склонны оставлять комментарии, чтобы предложить изменения в коде.комментарии UUCA и связанные с ними антипаттерны предназначены не только для авторов кода, который мы комментируем; они так же a предостережение emptor помочь читателей на сайте становится известно о проблемах в коде, который они находят здесь.
мы не можем надеяться достичь ситуации, когда нет ответы на переполнение стека рекомендуют бесполезно
cat
s (или некотируемые переменные, илиchmod 777
, или большое разнообразие других антипаттернов), но мы можем, по крайней мере, помочь обучить пользователя, который собирается скопировать/вставить этот код в самый внутренний жесткий цикл своего скрипта, который выполняется миллионы раз.
на обороны КПП:
да
< input process > output
или
process < input > output
является более эффективным, но многие вызовы не имеют проблем с производительностью, так что вам все равно.
эргономическим причинам:
мы привыкли читать слева направо, так что команда, как
cat infile | process1 | process2 > outfile
это тривиально, чтобы понять.
process1 < infile | process2 > outfile
должен перепрыгнуть через process1, а затем читать слева направо. Это может быть исцелен:
< infile process1 | process2 > outfile
выглядит как-то, как будто есть стрелка, указывающая налево, где ничего нет. Более запутанным и похожим на причудливое цитирование является:
process1 > outfile < infile
и генерация скриптов часто является итерационным процессом,
cat file cat file | process1 cat file | process1 | process2 cat file | process1 | process2 > outfile
где вы видите свой прогресс шаг за шагом, в то время как
< file
не работает даже. Простые способы менее подвержены ошибкам, а эргономичная командная катенация проста с cat.
еще одна тема, что большинство людей подвергались воздействию > и
и сравнение двух операндов С является коммутативным, противопоказания, что означает
(a > b) == (b < a)
я помню, как в первый раз использовал
a.sh < file
может означать то же самое, что
file > a.sh
и как-то переписать мой a.sh сценарий. Возможно, это проблема для многих новичков.
редкие различия
wc -c journal.txt 15666 journal.txt cat journal.txt | wc -c 15666
последний может быть использован в расчетах непосредственно.
factor $(cat journal.txt | wc -c)
конечно
< journal.txt wc -c 15666 wc -c < journal.txt 15666
но кого это волнует-15k?
если бы я иногда сталкивался с проблемами, конечно, я бы изменил свою привычку вызывать кошку.
при использовании очень больших или многих, многих файлов, избегая cat штраф. Для большинства вопросов использование cat является ортогональным, вне темы, а не проблемой.
начиная эти бесполезные бесполезное использование обсуждения кошки на каждую вторую тему оболочки только раздражает и скучно. Получить жизнь и ждать Минуты славы, когда дело с вопросами производительности.
Я думаю ,что (традиционный способ) с помощью трубы немного быстрее; на моем поле я использовал чтобы увидеть, что происходит:
без трубы:
toc@UnixServer:~$ strace wc -l < wrong_output.c execve("/usr/bin/wc", ["wc", "-l"], [/* 18 vars */]) = 0 brk(0) = 0x8b50000 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb77ad000 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) open("/etc/ld.so.cache", O_RDONLY) = 3 fstat64(3, {st_mode=S_IFREG|0644, st_size=29107, ...}) = 0 mmap2(NULL, 29107, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb77a5000 close(3) = 0 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) open("/lib/i386-linux-gnu/libc.so.6", O_RDONLY) = 3 read(3, "7ELFp204"..., 512) = 512 fstat64(3, {st_mode=S_IFREG|0755, st_size=1552584, ...}) = 0 mmap2(NULL, 1563160, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0xb7627000 mmap2(0xb779f000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x178) = 0xb779f000 mmap2(0xb77a2000, 10776, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xb77a2000 close(3) = 0 mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7626000 set_thread_area({entry_number:-1 -> 6, base_addr:0xb76268d0, limit:1048575, seg_32bit:1, contents:0, read_exec_only:0, limit_in_pages:1, seg_not_present:0, useable:1}) = 0 mprotect(0xb779f000, 8192, PROT_READ) = 0 mprotect(0x804f000, 4096, PROT_READ) = 0 mprotect(0xb77ce000, 4096, PROT_READ) = 0 munmap(0xb77a5000, 29107) = 0 brk(0) = 0x8b50000 brk(0x8b71000) = 0x8b71000 open("/usr/lib/locale/locale-archive", O_RDONLY|O_LARGEFILE) = 3 fstat64(3, {st_mode=S_IFREG|0644, st_size=5540198, ...}) = 0 mmap2(NULL, 2097152, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb7426000 mmap2(NULL, 1507328, PROT_READ, MAP_PRIVATE, 3, 0x2a8) = 0xb72b6000 close(3) = 0 open("/usr/share/locale/locale.alias", O_RDONLY) = 3 fstat64(3, {st_mode=S_IFREG|0644, st_size=2570, ...}) = 0 mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb77ac000 read(3, "# Locale name alias data base.\n#"..., 4096) = 2570 read(3, "", 4096) = 0 close(3) = 0 munmap(0xb77ac000, 4096) = 0 open("/usr/share/locale/fr_FR.UTF-8/LC_MESSAGES/coreutils.mo", O_RDONLY) = -1 ENOENT (No such file or directory) open("/usr/share/locale/fr_FR.utf8/LC_MESSAGES/coreutils.mo", O_RDONLY) = -1 ENOENT (No such file or directory) open("/usr/share/locale/fr_FR/LC_MESSAGES/coreutils.mo", O_RDONLY) = -1 ENOENT (No such file or directory) open("/usr/share/locale/fr.UTF-8/LC_MESSAGES/coreutils.mo", O_RDONLY) = -1 ENOENT (No such file or directory) open("/usr/share/locale/fr.utf8/LC_MESSAGES/coreutils.mo", O_RDONLY) = -1 ENOENT (No such file or directory) open("/usr/share/locale/fr/LC_MESSAGES/coreutils.mo", O_RDONLY) = -1 ENOENT (No such file or directory) open("/usr/share/locale-langpack/fr_FR.UTF-8/LC_MESSAGES/coreutils.mo", O_RDONLY) = -1 ENOENT (No such file or directory) open("/usr/share/locale-langpack/fr_FR.utf8/LC_MESSAGES/coreutils.mo", O_RDONLY) = -1 ENOENT (No such file or directory) open("/usr/share/locale-langpack/fr_FR/LC_MESSAGES/coreutils.mo", O_RDONLY) = -1 ENOENT (No such file or directory) open("/usr/share/locale-langpack/fr.UTF-8/LC_MESSAGES/coreutils.mo", O_RDONLY) = -1 ENOENT (No such file or directory) open("/usr/share/locale-langpack/fr.utf8/LC_MESSAGES/coreutils.mo", O_RDONLY) = -1 ENOENT (No such file or directory) open("/usr/share/locale-langpack/fr/LC_MESSAGES/coreutils.mo", O_RDONLY) = 3 fstat64(3, {st_mode=S_IFREG|0644, st_size=316721, ...}) = 0 mmap2(NULL, 316721, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb7268000 close(3) = 0 open("/usr/lib/i386-linux-gnu/gconv/gconv-modules.cache", O_RDONLY) = 3 fstat64(3, {st_mode=S_IFREG|0644, st_size=26064, ...}) = 0 mmap2(NULL, 26064, PROT_READ, MAP_SHARED, 3, 0) = 0xb7261000 close(3) = 0 read(0, "#include<stdio.h>\n\nint main(int "..., 16384) = 180 read(0, "", 16384) = 0 fstat64(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 2), ...}) = 0 mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7260000 write(1, "13\n", 313 ) = 3 close(0) = 0 close(1) = 0 munmap(0xb7260000, 4096) = 0 close(2) = 0 exit_group(0) = ?
и с трубой:
toc@UnixServer:~$ strace cat wrong_output.c | wc -l execve("/bin/cat", ["cat", "wrong_output.c"], [/* 18 vars */]) = 0 brk(0) = 0xa017000 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb774b000 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) open("/etc/ld.so.cache", O_RDONLY) = 3 fstat64(3, {st_mode=S_IFREG|0644, st_size=29107, ...}) = 0 mmap2(NULL, 29107, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb7743000 close(3) = 0 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) open("/lib/i386-linux-gnu/libc.so.6", O_RDONLY) = 3 read(3, "7ELFp204"..., 512) = 512 fstat64(3, {st_mode=S_IFREG|0755, st_size=1552584, ...}) = 0 mmap2(NULL, 1563160, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0xb75c5000 mmap2(0xb773d000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x178) = 0xb773d000 mmap2(0xb7740000, 10776, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xb7740000 close(3) = 0 mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb75c4000 set_thread_area({entry_number:-1 -> 6, base_addr:0xb75c48d0, limit:1048575, seg_32bit:1, contents:0, read_exec_only:0, limit_in_pages:1, seg_not_present:0, useable:1}) = 0 mprotect(0xb773d000, 8192, PROT_READ) = 0 mprotect(0x8051000, 4096, PROT_READ) = 0 mprotect(0xb776c000, 4096, PROT_READ) = 0 munmap(0xb7743000, 29107) = 0 brk(0) = 0xa017000 brk(0xa038000) = 0xa038000 open("/usr/lib/locale/locale-archive", O_RDONLY|O_LARGEFILE) = 3 fstat64(3, {st_mode=S_IFREG|0644, st_size=5540198, ...}) = 0 mmap2(NULL, 2097152, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb73c4000 mmap2(NULL, 1507328, PROT_READ, MAP_PRIVATE, 3, 0x2a8) = 0xb7254000 close(3) = 0 fstat64(1, {st_mode=S_IFIFO|0600, st_size=0, ...}) = 0 open("wrong_output.c", O_RDONLY|O_LARGEFILE) = 3 fstat64(3, {st_mode=S_IFREG|0664, st_size=180, ...}) = 0 read(3, "#include<stdio.h>\n\nint main(int "..., 32768) = 180 write(1, "#include<stdio.h>\n\nint main(int "..., 180) = 180 read(3, "", 32768) = 0 close(3) = 0 close(1) = 0 close(2) = 0 exit_group(0) = ? 13
вы можете сделать некоторые испытания с
strace
иtime
команда с большим количеством и более длинными командами для хорошего бенчмаркинга.