Почему итерация по файлу в два раза быстрее, чем чтение в память и вычисление в два раза?

Я сравниваю следующие

tail -n 1000000 stdout.log | grep -c '"success": true'
tail -n 1000000 stdout.log | grep -c '"success": false'

со следующим

log=$(tail -n 1000000 stdout.log)
echo "$log" | grep -c '"success": true'
echo "$log" | grep -c '"success": false'

и удивительно, что вторая занимает почти в 3 раза дольше, чем первая. Это должно быть быстрее, не так ли?

26 голосов | спросил phunehehe 19 MaramWed, 19 Mar 2014 11:17:25 +04002014-03-19T11:17:25+04:0011 2014, 11:17:25

5 ответов


11

С одной стороны, первый метод дважды вызывает вызов tail, поэтому он должен выполнять больше работы, чем второй метод, который только делает это один раз. С другой стороны, второй метод должен скопировать данные в оболочку, а затем вернуться, поэтому он должен выполнять больше работы, чем первая версия, где tail напрямую передается в grep. Первый способ имеет дополнительное преимущество на многопроцессорной машине: grep может работать параллельно с tail, тогда как второй метод строго сериализуется, сначала tail, затем grep

Таким образом, нет очевидной причины, почему нужно быть быстрее, чем другой.

Если вы хотите посмотреть, что происходит, посмотрите, какие системы вызывает оболочка. Попробуйте также с различными оболочками.

strace -t -f -o 1.strace sh -c '
  tail -n 1000000 stdout.log | grep "\"success\": true" | wc -l;
  tail -n 1000000 stdout.log | grep "\"success\": false" | wc -l'

strace -t -f -o 2-bash.strace bash -c '
  log=$(tail -n 1000000 stdout.log);
  echo "$log" | grep "\"success\": true" | wc -l;
  echo "$log" | grep "\"success\": true" | wc -l'

strace -t -f -o 2-zsh.strace zsh -c '
  log=$(tail -n 1000000 stdout.log);
  echo "$log" | grep "\"success\": true" | wc -l;
  echo "$log" | grep "\"success\": true" | wc -l'

С помощью метода 1 основными этапами являются:

  1. tail читает и ищет свою отправную точку.
  2. tail записывает 4096-байтовые фрагменты, которые grep читается так же быстро, как и они.
  3. Повторите предыдущий шаг для второй строки поиска.

С помощью метода 2 основными этапами являются:

  1. tail читает и ищет свою отправную точку.
  2. tail записывает 4096-байтовые фрагменты, которые bash читает 128 байтов за раз, а zsh читает 4096 байт за раз.
  3. Bash или zsh записывает 4096-байтовые фрагменты, которые grep читаются так же быстро, как и они.
  4. Повторите предыдущий шаг для второй строки поиска.

128-байтовые фрагменты Bash при чтении вывода подстановки команд значительно замедляют его; zsh выходит примерно так же быстро, как метод 1 для меня. Ваш пробег может варьироваться в зависимости от типа и количества CPU, конфигурации планировщика, версий задействованных инструментов и размера данных.

ответил Gilles 20 MaramThu, 20 Mar 2014 06:37:40 +04002014-03-20T06:37:40+04:0006 2014, 06:37:40
26

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

Мой файл - это вывод strace, называемый bigfile

$ wc -l bigfile.log 
1617000 bigfile.log

Сценарии

[email protected]:~/tmp$ cat p1.sh
tail -n 1000000 bigfile.log | grep '"success": true' | wc -l
tail -n 1000000 bigfile.log | grep '"success": false' | wc -l

[email protected]:~/tmp$ cat p2.sh
log=$(tail -n 1000000 bigfile.log)
echo "$log" | grep '"success": true' | wc -l
echo "$log" | grep '"success": true' | wc -l

У меня на самом деле нет совпадений для grep, поэтому ничто не записывается в последний канал через wc -l

Ниже приведены тайминги:

[email protected]:~/tmp$ time bash p1.sh
0
0

real    0m0.381s
user    0m0.248s
sys 0m0.280s
[email protected]:~/tmp$ time bash p2.sh
0
0

real    0m46.060s
user    0m43.903s
sys 0m2.176s

Итак, я снова выполнил два сценария с помощью команды strace

strace -cfo p1.strace bash p1.sh
strace -cfo p2.strace bash p2.sh

Вот результаты от трасс:

$ cat p1.strace 
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 97.24    0.508109       63514         8         2 waitpid
  1.61    0.008388           0     84569           read
  1.08    0.005659           0     42448           write
  0.06    0.000328           0     21233           _llseek
  0.00    0.000024           0       204       146 stat64
  0.00    0.000017           0       137           fstat64
  0.00    0.000000           0       283       149 open
  0.00    0.000000           0       180         8 close
...
  0.00    0.000000           0       162           mmap2
  0.00    0.000000           0        29           getuid32
  0.00    0.000000           0        29           getgid32
  0.00    0.000000           0        29           geteuid32
  0.00    0.000000           0        29           getegid32
  0.00    0.000000           0         3         1 fcntl64
  0.00    0.000000           0         7           set_thread_area
------ ----------- ----------- --------- --------- ----------------
100.00    0.522525                149618       332 total

И p2.strace

$ cat p2.strace 
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 75.27    1.336886      133689        10         3 waitpid
 13.36    0.237266          11     21231           write
  4.65    0.082527        1115        74           brk
  2.48    0.044000        7333         6           execve
  2.31    0.040998        5857         7           clone
  1.91    0.033965           0    705681           read
  0.02    0.000376           0     10619           _llseek
  0.00    0.000000           0       248       132 open
...
  0.00    0.000000           0       141           mmap2
  0.00    0.000000           0       176       126 stat64
  0.00    0.000000           0       118           fstat64
  0.00    0.000000           0        25           getuid32
  0.00    0.000000           0        25           getgid32
  0.00    0.000000           0        25           geteuid32
  0.00    0.000000           0        25           getegid32
  0.00    0.000000           0         3         1 fcntl64
  0.00    0.000000           0         6           set_thread_area
------ ----------- ----------- --------- --------- ----------------
100.00    1.776018                738827       293 total

Анализ

Неудивительно, что в обоих случаях большую часть времени тратится на ожидание завершения процесса, но p2 ждет в 2,63 раза дольше, чем p1, и, как уже отмечали другие, вы начинаете с конца страницы p2.sh.

Итак, забудьте о waitpid, проигнорируйте % и посмотрите на столбцы секунд на обеих трассах.

Самое большое время p1 тратит большую часть своего времени на чтение, вероятно, понятно, потому что есть большой файл для чтения, но p2 тратит 28,82 раза больше времени на чтение, чем p1. - bash не ожидает чтения такого большого файла в переменную и, вероятно, будет считывать буфер за раз, разбивая на строки, а затем получение другого.

количество просмотров p2 составляет 705k против 84k для p1, каждое чтение требует переключения контекста в пространство ядра и снова. Почти в 10 раз больше числа чтения и контекстных переключателей.

Время записи p2 тратит в 41,93 раза больше времени на запись, чем p1

число записи p1 делает больше записей, чем p2, 42k против 21k, однако они намного быстрее.

Вероятно, из-за echo строк в grep в отличие от хвостовых буферов.

Далее , p2 тратит больше времени на запись, чем в чтении, p1 - наоборот!

Другой фактор Посмотрите на количество системных вызовов brk: p2 тратит в 2,42 раза больше, чем на него делает чтение! В p1 (он даже не регистрируется). brk - это когда программа должна расширять свое адресное пространство, потому что изначально недостаточно было выделено, это, вероятно, связано с тем, что bash должен читать это файл в переменную и не ожидая, что он будет таким большим, и, как упоминал @scai, если файл становится слишком большим, даже это не сработает.

tail, вероятно, довольно эффективный файловый читатель, потому что это то, что он предназначен для создания, вероятно, memmaps файл и сканирование для разрывов строк, что позволяет ядру оптимизировать i /o. bash не так хорош как во время чтения, так и чтения.

p2 тратит 44 мс и 41 мс в clone и execv это не измеримая сумма для p1. Вероятно, чтение bash и создание переменной из хвоста.

Наконец, Totals p1 выполняет ~ 150k системных вызовов vs p2 740k (в 4,9 раза больше).

Устранение waitpid, p1 тратит 0.014416 секунд на выполнение системных вызовов, p2 0.439132 секунд (в 30 раз дольше).

Итак, p2 тратит большую часть времени в пространстве пользователя, ничего не делая, кроме ожидания завершения системных вызовов и ядра для реорганизации памяти, p1 выполняет больше операций записи, но более эффективен и значительно снижает нагрузку на систему и поэтому быстрее.

Заключение

Я бы никогда не стал беспокоиться о кодировании в памяти при написании сценария bash, что не означает, что вы не пытаетесь быть эффективным.

tail предназначен для того, чтобы делать то, что он делает, вероятно, memory maps файл, чтобы онэффективный для чтения и позволяет ядру оптимизировать i /o.

Лучшим способом оптимизации вашей проблемы может быть первый grep для «успеха»: «строки», а затем подсчет прав и falses, grep имеет параметр count, который снова избегает wc -l, или еще лучше, проведите хвост до awk и одновременно подсчитайте истины и фальши. p2 не только занимает много времени, но и добавляет нагрузку на систему, в то время как память перетасовывается с помощью brks.

ответил X Tian 19 MarpmWed, 19 Mar 2014 13:07:48 +04002014-03-19T13:07:48+04:0001 2014, 13:07:48
5

На самом деле первое решение также считывает файл в память! Это называется кэшированием и автоматически выполняется операционной системой.

И как уже правильно объяснено mikeserv первое решение exectutes grep while файл читается, а второе решение выполняет его после , файл читается tail.

Итак, первое решение быстрее из-за различных оптимизаций. Но это не всегда должно быть правдой. Для действительно больших файлов, которые ОС решает не кэшировать, второе решение может ускориться. Но обратите внимание, что для еще больших файлов, которые не будут вписываться в вашу память, второе решение не будет работать вообще.

ответил scai 19 MarpmWed, 19 Mar 2014 12:25:32 +04002014-03-19T12:25:32+04:0012 2014, 12:25:32
3

Я думаю, что основное отличие очень просто в том, что echo работает медленно. Рассмотрим это:

$ time (tail -n 1000000 foo | grep 'true' | wc -l; 
        tail -n 1000000 foo | grep 'false' | wc -l;)
666666
333333

real    0m0.999s
user    0m1.056s
sys     0m0.136s

$ time (log=$(tail -n 1000000 foo); echo "$log" | grep 'true' | wc -l; 
                                    echo "$log" | grep 'false' | wc -l)
666666
333333

real    0m4.132s
user    0m3.876s
sys     0m0.468s

$ time (tail -n 1000000 foo > bb;  grep 'true' bb | wc -l; 
                                   grep 'false' bb | wc -l)
666666
333333

real    0m0.568s
user    0m0.512s
sys     0m0.092s

Как вы можете видеть выше, шаг по времени - это печать данных. Если вы просто перенаправляете новый файл и grep через much быстрее, когда только один раз читаете файл.


И как запрошено, с строкой:

 $ time (log=$(tail -n 1000000 foo); grep 'true' <<< $log | wc -l; 
                                     grep 'false' <<< $log | wc -l  )
1
1

real    0m7.574s
user    0m7.092s
sys     0m0.516s

Это еще медленнее, предположительно потому, что здесь строка объединяет все данные на одну длинную строку и замедляет grep

$ tail -n 1000000 foo | (time grep -c 'true')
666666

real    0m0.500s
user    0m0.472s
sys     0m0.000s

$ tail -n 1000000 foo | perl -pe 's/\n/ /' | (time grep -c 'true')
1

real    0m1.053s
user    0m0.048s
sys     0m0.068s

Если переменная цитируется так, что не происходит никакого расщепления, все происходит немного быстрее:

 $ time (log=$(tail -n 1000000 foo); grep 'true' <<< "$log" | wc -l; 
                                     grep 'false' <<< "$log" | wc -l  )
666666
333333

real    0m6.545s
user    0m6.060s
sys     0m0.548s

Но все еще медленно, потому что шаг ограничения скорости печатает данные.

ответил terdon 19 MarpmWed, 19 Mar 2014 17:31:17 +04002014-03-19T17:31:17+04:0005 2014, 17:31:17
3

У меня тоже было такое ... Во-первых, я создал файл:

printf '"success": "true"
        "success": "true"
        "success": "false"
        %.0b' `seq 1 500000` >|/tmp/log

Если вы запустите выше, вы должны придумать 1,5 миллиона строк в /tmp/log с помощью 2 : 1 для строк "success": "true" для "success": "false" .

Следующее, что я сделал, - это запустить некоторые тесты. Я провел все тесты через прокси sh , поэтому time нужно будет смотреть только один процесс - и поэтому может показать единственный результат для всего задания.

Это кажется самым быстрым, хотя он добавляет второй файловый дескриптор и tee, , хотя я думаю, я могу объяснить, почему:

    time sh <<-\CMD
        . <<HD /dev/stdin | grep '"success": "true"' | wc -l
            tail -n 1000000 /tmp/log | { tee /dev/fd/3 |\
                grep '"success": "false"' |\
                    wc -l 1>&2 & } 3>&1 &
        HD
    CMD
666666
333334
sh <<<''  0.11s user 0.08s system 84% cpu 0.224 total

Вот ваш первый:

    time sh <<\CMD
        tail -n 1000000 /tmp/log | grep '"success": "true"' | wc -l
        tail -n 1000000 /tmp/log | grep '"success": "false"' | wc -l
    CMD

666666
333334
sh <<<''  0.31s user 0.17s system 148% cpu 0.323 total

И ваш второй:

    time sh <<\CMD
        log=$(tail -n 1000000 /tmp/log)
        echo "$log" | grep '"success": "true"' | wc -l
        echo "$log" | grep '"success": "false"' | wc -l
    CMD
666666
333334
sh <<<''  2.12s user 0.46s system 108% cpu 2.381 total

Вы можете видеть, что в моих тестах было больше разницы в скорости 3 * при чтении его в переменную, как и вы.

Я думаю, что часть этого состоит в том, что переменная оболочки должна быть разделена и обработана оболочкой при ее чтении - это не файл.

A here-document , с другой стороны, для всех целей и целей является ---- +: = 11 =: + ---- - a file в любом случае , И, как мы все знаем, Unix работает с файлами.

Самое интересное для меня о file descriptor, заключается в том, что вы можете манипулировать их here-docs - как прямой file-descriptors - и выполнить их. Это очень удобно, так как позволяет вам немного больше свободы указывать ваш |pipe , где вы хотите.

Мне пришлось |pipe tee , потому что первый tail em> grep , и второе чтение не осталось. Но так как I here-doc |pipe в |piped и снова перетащил его, чтобы перейти к /dev/fd/3 это не имело большого значения. Если вы используете >&1 stdout, , как многие другие рекомендуют:

grep -c

Это еще быстрее.

Но когда я запускаю его без time sh <<-\CMD . <<HD /dev/stdin | grep -c '"success": "true"' tail -n 1000000 /tmp/log | { tee /dev/fd/3 |\ grep -c '"success": "false"' 1>&2 & } 3>&1 & HD CMD 666666 333334 sh <<<'' 0.07s user 0.04s system 62% cpu 0.175 total . sourcing Я не могу успешно выполнить первый процесс, чтобы запустить их полностью одновременно. Здесь он полностью не опирается на него:

heredoc

Но когда я добавляю time sh <<\CMD tail -n 1000000 /tmp/log | { tee /dev/fd/3 |\ grep -c '"success": "true"' 1>&2 & } 3>&1 |\ grep -c '"success": "false"' CMD 666666 333334 sh <<<'' 0.10s user 0.08s system 109% cpu 0.165 total

&:

Тем не менее, разница составляет всего несколько сотых доли секунды, по крайней мере для меня, поэтому принимайте ее так, как вы.

В любом случае причина, по которой он работает быстрее с time sh <<\CMD tail -n 1000000 /tmp/log | { tee /dev/fd/3 |\ grep -c '"success": "true"' 1>&2 & } 3>&1 & |\ grep -c '"success": "false"' CMD sh: line 2: syntax error near unexpected token `|' , состоит в том, что оба tee запускается одновременно с одним вызовом greps дублирует файл для нас и разделяет его на второй tail. tee обрабатывать все в потоке - все работает сразу от начала до конца, поэтому все они заканчиваются примерно в одно и то же время.

Итак, вернемся к вашему первому примеру:

grep

И ваш второй:

    tail | grep | wc #wait til finished
    tail | grep | wc #now we're done

Но когда мы разделяем наш вход и запускаем наши процессы одновременно:

    var=$( tail ) ; #wait til finished
    echo | grep | wc #wait til finished
    echo | grep | wc #now we're done
ответил terdon 19 MarpmWed, 19 Mar 2014 17:31:17 +04002014-03-19T17:31:17+04:0005 2014, 17:31:17

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132