Як `так` записати у файл так швидко?


58

Наведу приклад:

$ timeout 1 yes "GNU" > file1
$ wc -l file1
11504640 file1

$ for ((sec0=`date +%S`;sec<=$(($sec0+5));sec=`date +%S`)); do echo "GNU" >> file2; done
$ wc -l file2
1953 file2

Тут ви бачите, що команда yesзаписує 11504640рядки за секунду, тоді як я можу записати лише 1953рядки за 5 секунд, використовуючи bash forта echo.

Як запропоновано в коментарях, існують різні хитрощі, щоб зробити його більш ефективним, але жоден не підходить до відповідності швидкості yes:

$ ( while :; do echo "GNU" >> file3; done) & pid=$! ; sleep 1 ; kill $pid
[1] 3054
$ wc -l file3
19596 file3

$ timeout 1 bash -c 'while true; do echo "GNU" >> file4; done'
$ wc -l file4
18912 file4

Вони можуть записати до 20 тисяч рядків за секунду. І їх можна вдосконалити:

$ timeout 1 bash -c 'while true; do echo "GNU"; done >> file5' 
$ wc -l file5
34517 file5

$ ( while :; do echo "GNU"; done >> file6 ) & pid=$! ; sleep 1 ; kill $pid
[1] 5690
$ wc -l file6
40961 file6

Вони отримують нас до 40 тисяч рядків за секунду. Краще, але все-таки далекий крик, від yesякого можна написати близько 11 мільйонів рядків за секунду!

Отже, як yesзаписати у файл так швидко?



9
У другому прикладі у вас є два зовнішніх виклику команд для кожної ітерації циклу, і dateце дещо велика вага, плюс оболонка повинна повторно відкрити вихідний потік echoдля кожної ітерації циклу. У першому прикладі є лише одне виклик команди з одним перенаправленням виходу, і команда надзвичайно легка. Двоє жодним чином не порівнянні.
CVn

@ MichaelKjörling Ви маєте рацію, dateможе бути важкою, див. Редагування мого питання.
Пандія

1
timeout 1 $(while true; do echo "GNU">>file2; done;)це неправильний спосіб використання, timeout оскільки timeoutкоманда запуститься лише після завершення заміни команди. Використовуйте timeout 1 sh -c 'while true; do echo "GNU">>file2; done'.
муру

1
підсумок відповідей: витрачаючи час процесора лише на write(2)системні дзвінки, а не на завантаження суден інших системних дзвінків, накладних оболонок або навіть створення процесу у вашому першому прикладі (який працює і чекає dateкожного друку рядка у файл). Однієї секунди написання ледве вистачає для того, щоб вузьке місце на диску вводу / виводу (а не процесорі / пам'яті), в сучасній системі з великою кількістю оперативної пам'яті. Якщо дозволяти працювати довше, різниця була б меншою. (Залежно від того, наскільки погана реалізація bash ви використовуєте, та відносна швидкість процесора та диска, ви навіть не можете наситити дискові введення / виведення bash).
Пітер Кордес

Відповіді:


65

слова:

yesдемонструє подібну поведінку до більшості інших стандартних утиліт, які, як правило, записують у FILE STREAM з вихідним сигналом, завантаженим libC через stdio . Вони виконують syscall write()кожні 4 кб (16 кб або 64 кб ) або будь-який вихідний блок BUFSIZ . echoє write()пер GNU. Це багато в режимі комутації (який не є, по- видимому, настільки ж дорогим , як контекстно-перемикач ) .

І це зовсім не згадати, що, крім початкового циклу оптимізації, yesце дуже простий, крихітний, складений цикл C, і ваш цикл оболонок аж ніяк не порівнянний з програмою, оптимізованою компілятором.


але я помилявся:

Коли я говорив, що раніше yesвикористовував stdio, я лише припускав, що він це робить, тому що він дуже схожий на тих, що роблять. Це було неправильно - це лише імітує їх поведінку таким чином. Те, що він насправді робить, дуже схоже на аналог того, що я зробив нижче з оболонкою: спочатку він циклічно конфігурує свої аргументи (або yякщо такі немає), поки вони не можуть більше зростати, не перевищуючи їх BUFSIZ.

Коментар від джерела, що безпосередньо передує відповідному forциклу, констатує:

/* Buffer data locally once, rather than having the
large overhead of stdio buffering each item.  */

yesпісля цього робить своє write().


відступ:

(Як спочатку було включено у запитання та збережено для контексту до можливого інформаційного пояснення, яке вже написано тут) :

Я намагався, timeout 1 $(while true; do echo "GNU">>file2; done;)але не можу зупинити цикл.

timeoutПроблема у вас є з заміною команди - Я думаю , що я отримую зараз, і може пояснити , чому він не зупиняється. timeoutне запускається, оскільки його командний рядок ніколи не запускається. Ваша оболонка роздвоює дочірню оболонку, відкриває трубу на її штриху і читає її. Він перестане читати, коли дитина вийде, а потім інтерпретуватиме все, що дитина написав, для $IFSрозширення та глобальних розширень, а з результатами замінить усе - від $(відповідного ).

Але якщо дитина - це нескінченний цикл, який ніколи не записує на трубу, то дитина ніколи не припиняє циклічно, і timeoutкомандний рядок 'ніколи не завершується раніше (як я здогадуюсь) ви робите CTRL-Cі вбиваєте дочірній цикл. Тому ніколи неtimeout можна вбивати цикл, який потрібно виконати, перш ніж він може запуститися.


інші timeouts:

... просто не настільки актуальні для ваших проблем з продуктивністю, як кількість часу, яку ваша програма оболонки повинна витратити на перемикання між режимом користувача та ядра для обробки результатів. timeout, однак, не настільки гнучка, як оболонка може бути для цієї мети: там, де оболонки мають відмінний вміння керувати аргументами та керувати іншими процесами.

Як зазначається в іншому місці, просто переміщення вашого [fd-num] >> named_fileперенаправлення на вихідну ціль циклу, а не лише спрямування виводу туди на цикл команд, що перекинулися, може істотно підвищити продуктивність, оскільки таким чином принаймні open()систематичний виклик потрібно виконати лише один раз. Це також робиться внизу з |націленою трубою як вихід для внутрішніх петель.


пряме порівняння:

Вам може подобатися:

for cmd in  exec\ yes 'while echo y; do :; done'
do      set +m
        sh  -c '{ sleep 1; kill "$$"; }&'"$cmd" | wc -l
        set -m
done

256659456
505401

Який вид з як відносини команди до південь від описаних вище, але немає труби і дитина , поки фоновий не вбиває батько. У yesвипадку, коли батька фактично були замінені з моменту появи породження дитини, але оболонка викликає yes, накладаючи власний процес на новий, і таким чином PID залишається тим самим, і його дитина-зомбі все ще знає, кого вбити.


більший буфер:

Тепер давайте ознайомимось із збільшенням write()буфера оболонки .

IFS="
";    set y ""              ### sets up the macro expansion       
until [ "${512+1}" ]        ### gather at least 512 args
do    set "$@$@";done       ### exponentially expands "$@"
printf %s "$*"| wc -c       ### 1 write of 512 concatenated "y\n"'s  

1024

Я вибрав це число, тому що вихідні рядки довжиною більше 1 кб розбивалися на окремі write()для мене. І ось ось знову цикл:

for cmd in 'exec  yes' \
           'until [ "${512+:}" ]; do set "$@$@"; done
            while printf %s "$*"; do :; done'
do      set +m
        sh  -c $'IFS="\n"; { sleep 1; kill "$$"; }&'"$cmd" shyes y ""| wc -l
        set -m
done

268627968
15850496

Це в 300 разів перевищує кількість даних, записаних оболонкою за той самий проміжок часу для цього тесту, ніж останній. Не надто пошарпаний. Але це не так yes.


пов'язані:

Як вимагається, є більш ретельний опис, ніж прості коментарі до коду щодо того, що робиться тут, за цим посиланням .


@heemayl - можливо? я не зовсім впевнений, я розумію, що ти просиш? коли програма використовує stdio для запису виводу, вона робить або не буферизацію (наприклад, stderr за замовчуванням), або буферизацію ліній (до терміналів за замовчуванням) або блокування буферизацією (в основному більшість інших матеріалів встановлюється таким чином за замовчуванням) . мені трохи незрозуміло, що встановлює розмір вихідного буфера - але зазвичай це близько 4 кб. і тому функції libida stdio збиратимуть свої результати, поки вони не зможуть записати цілий блок. ddце один стандартний інструмент, який, безумовно, не використовує stdio, наприклад. більшість інших.
mikeserv

3
Версія оболонки робить open(існуючу) writeAND close(яку, на мою думку, все ще чекає флеш), і створює новий процес та виконує dateдля кожного циклу.
dave_thompson_085

@ dave_thompson_085 - перейдіть до / dev / chat . і те, що ви говорите, не обов'язково відповідає дійсності, як ви це бачите. Наприклад, за допомогою цього wc -lциклу bashдля мене виходить 1/5 виводу, який shробить цикл - bashуправляє трохи більше 100 к writes()до dash500 к.
mikeserv

Вибачте, що я був неоднозначним; Я мав на увазі версію оболонки у запитанні, яка на момент її читання мала лише оригінальну версію з for((sec0=`date +%S`;...контрольним часом та перенаправленням у циклі, а не наступні вдосконалення.
dave_thompson_085

@ dave_thompson_085 - його штраф. відповідь все-таки була помилковою щодо деяких фундаментальних моментів, і зараз, як я сподіваюся, повинна бути майже правильною.
mikeserv

20

Краще питання - чому ваша оболонка записує файл так повільно. Будь-яка автономна складена програма, яка використовує систематичні виклики файлів відповідально (не промиваючи кожного символу одночасно), зробить це досить швидко. Те, що ви робите, - це написання рядків на інтерпретованій мові (оболонці), а крім того, ви робите багато непотрібних операцій з введення виводу. Що yesробить:

  • відкриває файл для запису
  • виклики, оптимізовані та складені функції для запису в потік
  • потік буферизований, тому syscall (дорогий перехід у режим ядра) трапляється дуже рідко, великими шматками
  • закриває файл

Що робить ваш сценарій:

  • читається в рядку коду
  • інтерпретує код, роблячи безліч зайвих операцій, щоб фактично проаналізувати ваш вклад і з'ясувати, що робити
  • для кожної ітерації циклу while (що, мабуть, не дешево в інтерпретованій мові):
    • зателефонуйте до dateзовнішньої команди та зберігайте її вихід (лише в оригінальній версії - у переглянутій версії ви отримуєте коефіцієнт 10, не роблячи цього)
    • перевірити, чи виконується умова припинення циклу
    • відкрити файл у режимі додавання
    • echoкоманда розбору , розпізнайте її (з деяким кодом відповідності шаблону) як вбудовану оболонку, розширення параметрів виклику та все інше в аргументі "GNU" та нарешті запишіть рядок у відкритий файл
    • знову закрийте файл
    • повторити процес

Дорогі частини: вся інтерпретація надзвичайно дорога (bash робить дуже багато попередньої обробки всіх вхідних даних - ваш рядок потенційно може містити змінну заміну, підстановку процесу, розширення дужок, символи виходу і багато іншого), кожен виклик вбудованого Можливо, оператор перемикання з перенаправленням на функцію, яка стосується вбудованого, і що дуже важливо, ви відкриваєте та закриваєте файл для кожного рядка виводу. Ви можете поставити >> fileзовні цикл while, щоб зробити його набагато швидшим , але ви все ще знаєте мову, що тлумачиться. Вам цілком пощастилоechoце вбудована оболонка, а не зовнішня команда - інакше ваш цикл передбачає створення нового процесу (fork & exec) на кожній ітерації. Що призведе до зупинки процесу - ви побачили, наскільки це дорого, коли ви мали dateкоманду в циклі.


11

Інші відповіді стосуються основних питань. Зі сторони, ви можете збільшити пропускну здатність циклу while, записавши у вихідний файл наприкінці обчислення. Порівняйте:

$ i=0;time while  [ $i -le 1000 ]; do ((++i)); echo "GNU" >>/tmp/f; done;

real    0m0.080s
user    0m0.032s
sys     0m0.037s

з

$ i=0;time while  [ $i -le 1000 ]; do ((++i)); echo "GNU"; done>>/tmp/f;

real    0m0.030s
user    0m0.019s
sys     0m0.011s

Так, це має значення, а швидкість запису (принаймні) у моєму випадку подвоюється
Пандія,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.