Tại sao lặp lại một tệp nhanh hơn hai lần so với đọc nó vào bộ nhớ và tính toán hai lần?


26

Tôi đang so sánh như sau

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

với những điều sau đây

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

và đáng ngạc nhiên là cái thứ hai dài hơn gần 3 lần so với cái thứ nhất. Nó sẽ nhanh hơn chứ?


Có thể là do giải pháp thứ hai, nội dung tệp được đọc 3 lần và chỉ hai lần trong ví dụ đầu tiên?
Laurent C.

4
Ít nhất là trong ví dụ thứ hai, bạn $( command substitution )không trực tiếp. Tất cả phần còn lại xảy ra thông qua các đường ống đồng thời, nhưng trong ví dụ thứ hai, bạn phải đợi cho log=đến khi hoàn thành. Hãy thử với << TẠI ĐÂY \ n $ {log = $ (lệnh)} \ nHERE - xem những gì bạn nhận được.
mikeerv

Trong trường hợp tệp cực lớn, máy bị hạn chế bộ nhớ hoặc nhiều mục hơn grep, bạn có thể thấy một số tốc độ sử dụng teeđể tệp chắc chắn chỉ được đọc một lần. cat stdout.log | tee >/dev/null >(grep -c 'true'>true.cnt) >(grep -c 'false'>false.cnt); cat true.cnt; cat false.cnt
Matt

@LaurentC., Không, nó chỉ được đọc một lần trong ví dụ thứ hai. Chỉ có một cuộc gọi để đuôi.
psusi

Bây giờ so sánh những điều này tail -n 10000 | fgrep -c '"success": true'và sai.
kojiro

Câu trả lời:


11

Một mặt, phương thức thứ nhất gọi tailhai lần, vì vậy nó phải thực hiện nhiều công việc hơn phương thức thứ hai chỉ thực hiện điều này một lần. Mặt khác, phương pháp thứ hai phải sao chép dữ liệu vào hệ vỏ rồi quay ra ngoài, do đó, nó phải thực hiện nhiều công việc hơn phiên bản đầu tiên tailđược dẫn trực tiếp vào grep. Phương thức đầu tiên có một lợi thế bổ sung trên máy đa bộ xử lý: grepcó thể hoạt động song song tail, trong khi phương thức thứ hai được tuần tự hóa nghiêm ngặt, trước tiên tail, sau đó grep.

Vì vậy, không có lý do rõ ràng tại sao một người nên nhanh hơn người khác.

Nếu bạn muốn xem những gì đang diễn ra, hãy nhìn vào hệ thống gọi là hệ vỏ. Hãy thử với các vỏ khác nhau, quá.

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'

Với phương pháp 1, các giai đoạn chính là:

  1. tail đọc và tìm kiếm điểm bắt đầu của nó.
  2. tailghi các đoạn 4096 byte grepđọc nhanh như chúng được tạo ra.
  3. Lặp lại bước trước cho chuỗi tìm kiếm thứ hai.

Với phương pháp 2, các giai đoạn chính là:

  1. tail đọc và tìm kiếm điểm bắt đầu của nó.
  2. tail ghi các đoạn 4096 byte mà bash đọc 128 byte mỗi lần và zsh đọc 4096 byte mỗi lần.
  3. Bash hoặc zsh viết các đoạn 4096 byte grepđọc nhanh như chúng được tạo ra.
  4. Lặp lại bước trước cho chuỗi tìm kiếm thứ hai.

Các đoạn 128 byte của Bash khi đọc đầu ra của lệnh thay thế làm chậm đáng kể; zsh xuất hiện nhanh như phương pháp 1 đối với tôi. Số dặm của bạn có thể thay đổi tùy thuộc vào loại và số CPU, cấu hình bộ lập lịch, phiên bản của các công cụ liên quan và kích thước của dữ liệu.


Là kích thước trang 4k con số phụ thuộc? Ý tôi là, cả đuôi và zsh đều chỉ là những tòa nhà chọc trời? (Có thể đó là thuật ngữ không chính xác, mặc dù tôi hy vọng là không ...) Bash là gì?
mikeerv

Đây là vị trí trên Gilles! Với zsh, phương thức thứ hai nhanh hơn một chút trên máy của tôi.
phunehehe

Công việc tuyệt vời Gilles, tks.
X Tian

@mikeerv Tôi chưa xem nguồn để xem các chương trình này chọn kích cỡ như thế nào. Các lý do có khả năng nhất để xem 4096 sẽ là hằng số tích hợp hoặc st_blksizegiá trị cho một đường ống, là 4096 trên máy này (và tôi không biết đó có phải là vì kích thước trang MMU không). 128 của Bash sẽ phải là một hằng số tích hợp.
Gilles 'SO- ngừng trở thành ác quỷ'

@Gilles, cảm ơn vì đã trả lời chu đáo. Ive chỉ tò mò về kích thước trang gần đây.
mikeerv

26

Tôi đã thực hiện bài kiểm tra sau và trên hệ thống của tôi, kết quả khác biệt dài hơn khoảng 100 lần cho tập lệnh thứ hai.

Tập tin của tôi là một đầu ra strace được gọi là bigfile

$ wc -l bigfile.log 
1617000 bigfile.log

Chữ viết

xtian@clafujiu:~/tmp$ cat p1.sh
tail -n 1000000 bigfile.log | grep '"success": true' | wc -l
tail -n 1000000 bigfile.log | grep '"success": false' | wc -l

xtian@clafujiu:~/tmp$ cat p2.sh
log=$(tail -n 1000000 bigfile.log)
echo "$log" | grep '"success": true' | wc -l
echo "$log" | grep '"success": true' | wc -l

Tôi thực sự không có bất kỳ trận đấu nào cho grep vì vậy không có gì được ghi vào đường ống cuối cùng thông qua wc -l

Dưới đây là thời gian:

xtian@clafujiu:~/tmp$ time bash p1.sh
0
0

real    0m0.381s
user    0m0.248s
sys 0m0.280s
xtian@clafujiu:~/tmp$ time bash p2.sh
0
0

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

Vì vậy, tôi đã chạy hai tập lệnh một lần nữa thông qua lệnh strace

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

Dưới đây là kết quả từ các dấu vết:

$ 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

Và 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

Phân tích

Không có gì đáng ngạc nhiên, trong cả hai trường hợp, phần lớn thời gian dành cho việc chờ đợi một quá trình hoàn thành, nhưng p2 chờ gấp 2,63 lần so với p1 và như những người khác đã đề cập, bạn đang bắt đầu muộn trong p2.sh.

Vì vậy, bây giờ hãy quên đi waitpid, bỏ qua %cột và nhìn vào cột giây trên cả hai dấu vết.

Thời gian lớn nhất p1 dành phần lớn thời gian để đọc có thể hiểu được, bởi vì có một tệp lớn để đọc, nhưng p2 dành thời gian đọc dài hơn 28,82 lần so với p1. - bashkhông mong đợi để đọc một tệp lớn như vậy thành một biến và có thể đang đọc bộ đệm tại một thời điểm, chia thành các dòng và sau đó lấy một tệp khác.

số lần đọc p2 là 705k so với 84k cho p1, mỗi lần đọc yêu cầu chuyển ngữ cảnh vào không gian kernel và ra ngoài một lần nữa. Gần 10 lần số lần đọc và chuyển đổi ngữ cảnh.

Thời gian viết p2 dành thời gian viết dài hơn 41,93 lần so với p1

số đếm ghi p1 không viết nhiều hơn p2, 42k so với 21k, tuy nhiên chúng nhanh hơn nhiều.

Có lẽ là do các echodòng vào greptrái ngược với bộ đệm viết đuôi.

Hơn nữa , p2 dành nhiều thời gian hơn cho việc viết hơn là đọc, p1 là cách khác!

Yếu tố khác Nhìn vào số lượng brkcuộc gọi hệ thống: p2 dành thời gian ngắt gấp 2,42 lần so với số lần đọc! Trong p1 (nó thậm chí không đăng ký). brklà khi chương trình cần mở rộng không gian địa chỉ của nó vì ban đầu không được phân bổ, điều này có thể là do bash phải đọc tệp đó vào biến và không mong đợi nó lớn như vậy, và như @scai đã đề cập, nếu tập tin trở nên quá lớn, thậm chí điều đó sẽ không hoạt động.

tailcó lẽ là một trình đọc tệp khá hiệu quả, bởi vì đây là những gì nó được thiết kế để làm, nó có thể ghi lại tệp và quét để ngắt dòng, do đó cho phép kernel tối ưu hóa i / o. Bash không tốt cả về thời gian đọc và viết.

p2 dành 44ms và 41ms cloneexecvđó không phải là số tiền có thể đo được cho p1. Có lẽ bash đọc và tạo biến từ đuôi.

Cuối cùng, Totals p1 thực hiện các cuộc gọi hệ thống ~ 150k so với p2 740k (gấp 4,93 lần).

Loại bỏ Waitpid, p1 dành 0,014416 giây thực hiện các cuộc gọi hệ thống, p2 0,439132 giây (lâu hơn 30 lần).

Vì vậy, có vẻ như p2 dành phần lớn thời gian trong không gian người dùng, không làm gì ngoài việc chờ đợi các cuộc gọi hệ thống hoàn thành và nhân sắp xếp lại bộ nhớ, p1 thực hiện nhiều thao tác ghi hơn, nhưng hiệu quả hơn và giảm tải hệ thống đáng kể và do đó nhanh hơn.

Phần kết luận

Tôi sẽ không bao giờ cố gắng lo lắng về việc mã hóa thông qua bộ nhớ khi viết tập lệnh bash, điều đó không có nghĩa là bạn không cố gắng làm việc hiệu quả.

tailđược thiết kế để làm những gì nó làm, nó có thể memory mapslà tập tin sao cho hiệu quả để đọc và cho phép kernel tối ưu hóa i / o.

Cách tốt hơn để tối ưu hóa vấn đề của bạn có thể là trước tiên grepcho các dòng '"thành công":' và sau đó đếm các dấu vết và sai lệch, grepcó một tùy chọn đếm một lần nữa để tránh wc -l, hoặc thậm chí tốt hơn, chuyển đuôi qua awkvà đếm các dấu vết và sai lệch đồng thời. p2 không chỉ mất nhiều thời gian mà còn tăng tải cho hệ thống trong khi bộ nhớ bị xáo trộn với các brks.


2
TL; DR: malloc (); nếu bạn có thể nói với $ log mức độ lớn cần thiết và có thể viết nó nhanh chóng trong một op mà không cần phân bổ lại, thì nó có thể sẽ nhanh như vậy.
Chris K

5

Trên thực tế, giải pháp đầu tiên cũng đọc tập tin vào bộ nhớ! Điều này được gọi là bộ nhớ đệm và được hệ điều hành tự động thực hiện.

Và như đã được giải thích chính xác bởi mikeerv , giải pháp đầu tiên sẽ xuất hiện grep trong khi tệp đang được đọc trong khi giải pháp thứ hai thực thi nó sau khi tệp đã được đọc bởi tail.

Vì vậy, giải pháp đầu tiên là nhanh hơn vì tối ưu hóa khác nhau. Nhưng điều này không phải lúc nào cũng đúng. Đối với các tệp thực sự lớn mà HĐH quyết định không lưu bộ đệm, giải pháp thứ hai có thể trở nên nhanh hơn. Nhưng lưu ý rằng đối với các tệp lớn hơn không phù hợp với bộ nhớ của bạn, giải pháp thứ hai sẽ không hoạt động.


3

Tôi nghĩ rằng sự khác biệt chính là rất đơn giản echolà chậm. Xem xét điều này:

$ 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

Như bạn có thể thấy ở trên, bước tốn thời gian là in dữ liệu. Nếu bạn chỉ cần chuyển hướng đến một tệp mới và grep thông qua đó sẽ nhanh hơn nhiều khi chỉ đọc tệp một lần.


Và theo yêu cầu, với một chuỗi ở đây:

 $ 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

Cái này thậm chí còn chậm hơn, có lẽ là do chuỗi ở đây đang nối tất cả dữ liệu trên một dòng dài và điều đó sẽ làm chậm 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

Nếu biến được trích dẫn để không xảy ra sự chia tách, mọi thứ sẽ nhanh hơn một chút:

 $ 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

Nhưng vẫn chậm vì bước giới hạn tốc độ là in dữ liệu.


Tại sao bạn không thử <<<nó sẽ rất thú vị để xem nếu điều đó làm cho một sự khác biệt.
Graeme

3

Tôi cũng đã từng làm điều này ... Đầu tiên, tôi xây dựng tệp:

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

Nếu bạn chạy trên chính mình, bạn nên đưa ra 1,5 triệu dòng trong /tmp/logvới tỷ lệ 2: 1 trong tổng số "success": "true"dòng để "success": "false"dòng.

Điều tiếp theo tôi đã làm là chạy một số thử nghiệm. Tôi đã chạy tất cả các bài kiểm tra thông qua một proxy shnên timesẽ chỉ phải xem một quy trình duy nhất - và do đó có thể hiển thị một kết quả duy nhất cho toàn bộ công việc.

Đây có vẻ là nhanh nhất, mặc dù nó thêm một mô tả tệp thứ hai và tee,mặc dù tôi nghĩ rằng tôi có thể giải thích tại sao:

    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

Đây là lần đầu tiên của bạn:

    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

Và thứ hai của bạn:

    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

Bạn có thể thấy rằng trong các thử nghiệm của tôi có sự khác biệt lớn hơn 3 * về tốc độ khi đọc nó thành một biến như bạn đã làm.

Tôi nghĩ một phần của điều đó là một biến shell phải được phân tách và xử lý bởi shell khi nó được đọc - nó không phải là một tệp.

Một here-documentmặt khác, đối với tất cả các tính năng, là một file- mộtfile descriptor, anyway. Và như tất cả chúng ta đều biết - Unix hoạt động với các tệp.

Điều thú vị nhất với tôi here-docslà bạn có thể điều khiển chúng file-descriptors- như một cách thẳng thắn |pipe- và thực hiện chúng. Điều này rất tiện dụng vì nó cho phép bạn tự do hơn một chút trong việc chỉ ra |pipenơi bạn muốn.

Tôi phải teecác tailvì là người đầu tiên grepăn here-doc |pipevà không có gì để lại cho phần thứ hai để đọc. Nhưng kể từ khi tôi |pipedvào /dev/fd/3và nhặt nó lên một lần nữa để vượt qua >&1 stdout,nó không thành vấn đề. Nếu bạn sử dụng grep -cnhư nhiều người khác khuyên dùng:

    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

Nó thậm chí còn nhanh hơn.

Nhưng khi tôi chạy nó mà không cần . sourcingsự heredoctôi không thể nền thành công quá trình đầu tiên để chạy chúng hoàn toàn đồng thời. Đây là nó mà không có nền tảng đầy đủ:

    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

Nhưng khi tôi thêm &:

    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 `|'

Tuy nhiên, sự khác biệt dường như chỉ là một phần trăm giây, ít nhất là đối với tôi, vì vậy hãy xem nó như bạn muốn.

Dù sao, lý do nó chạy nhanh hơn teelà vì cả hai đều grepschạy cùng một lúc chỉ với một lần gọi tail. teesao chép tệp cho chúng tôi và tách nó ra grepquy trình thứ hai tất cả trong luồng - mọi thứ chạy cùng một lúc từ đầu đến cuối, vì vậy chúng tất cả kết thúc cùng một lúc, quá.

Vì vậy, quay trở lại ví dụ đầu tiên của bạn:

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

Và thứ hai của bạn:

    var=$( tail ) ; #wait til finished
    echo | grep | wc #wait til finished
    echo | grep | wc #now we're done

Nhưng khi chúng tôi phân chia đầu vào của chúng tôi và chạy các quy trình của chúng tôi đồng thời:

          3>&1  | grep #now we're done
              /        
    tail | tee  #both process together
              \  
          >&1   | grep #now we're done

1
+1 nhưng bài kiểm tra cuối cùng của bạn đã chết vì lỗi cú pháp, tôi không nghĩ rằng thời gian là chính xác ở đó :)
terdon

@terdon Họ có thể sai - Tôi đã chỉ ra rằng nó đã chết. Tôi đã chỉ ra sự khác biệt giữa & và không & - khi bạn thêm nó, trình bao bị đảo lộn. Nhưng tôi đã thực hiện rất nhiều bản sao / dán để tôi có thể làm hỏng một hoặc hai, nhưng tôi nghĩ rằng họ ổn cả ...
mikeerv

sh: dòng 2: lỗi cú pháp gần mã thông báo không mong đợi `| '
terdon

@terdon Vâng - "Tôi không thể thành công trong quá trình đầu tiên để chạy chúng hoàn toàn đồng thời. Thấy không?" Cái đầu tiên không được làm nền, nhưng khi tôi thêm & trong một nỗ lực để làm như vậy "mã thông báo bất ngờ". Khi tôi . nguồn di truyền tôi có thể sử dụng &.
mikeerv
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.