Làm thế nào để `yes` ghi vào tập tin nhanh như vậy?


58

Để tôi lấy một ví dụ:

$ 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

Ở đây bạn có thể thấy rằng lệnh yesviết 11504640các dòng trong một giây trong khi tôi chỉ có thể viết 1953các dòng trong 5 giây bằng cách sử dụng bash's forecho.

Như được đề xuất trong các bình luận, có nhiều thủ thuật khác nhau để làm cho nó hiệu quả hơn nhưng không có cách nào phù hợp với tốc độ của 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

Chúng có thể viết tới 20 nghìn dòng trong một giây. Và chúng có thể được cải thiện hơn nữa để:

$ 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

Những thứ này giúp chúng tôi có tới 40 nghìn dòng trong một giây. Tốt hơn, nhưng vẫn còn rất xa yesđể có thể viết khoảng 11 triệu dòng trong một giây!

Vì vậy, làm thế nào để yesviết vào tập tin nhanh như vậy?



9
Trong ví dụ thứ hai, bạn có hai lệnh gọi bên ngoài cho mỗi lần lặp của vòng lặp và datecó phần nặng, cộng với trình bao phải mở lại luồng đầu ra echocho mỗi lần lặp. Trong ví dụ đầu tiên, chỉ có một lệnh gọi duy nhất với một chuyển hướng đầu ra duy nhất và lệnh cực kỳ nhẹ. Hai là không có cách nào so sánh được.
CVn

@ MichaelKjorling bạn đúng datecó thể nặng, xem chỉnh sửa câu hỏi của tôi.
Pandya

1
timeout 1 $(while true; do echo "GNU">>file2; done;)là cách sử dụng sai timeouttimeoutlệnh sẽ chỉ bắt đầu sau khi thay thế lệnh kết thúc. Sử dụng timeout 1 sh -c 'while true; do echo "GNU">>file2; done'.
muru

1
tóm tắt các câu trả lời: bằng cách chỉ dành thời gian CPU cho write(2)các cuộc gọi hệ thống, không phải tải thuyền của các tòa nhà khác, trên đầu hoặc thậm chí tạo quy trình trong ví dụ đầu tiên của bạn (chạy và chờ datecho mỗi dòng được in vào tệp). Một giây viết chỉ đủ để nó bị nghẽn cổ chai trên đĩa I / O (chứ không phải CPU / bộ nhớ), trên một hệ thống hiện đại có nhiều RAM. Nếu được phép chạy lâu hơn, sự khác biệt sẽ nhỏ hơn. (Tùy thuộc vào mức độ thực thi bash mà bạn sử dụng và tốc độ tương đối của CPU và đĩa, bạn thậm chí có thể không bão hòa I / O đĩa với bash).
Peter Cordes

Câu trả lời:


65

tóm lại:

yesthể hiện hành vi tương tự với hầu hết các tiện ích tiêu chuẩn khác thường ghi vào FILE STREAM với đầu ra được bộ đệm bởi libC thông qua stdio . Chúng chỉ thực hiện các tòa nhà write()mỗi 4kb (16kb hoặc 64kb) hoặc bất cứ khối đầu ra BUFSIZ nào . echolà một write()mỗi GNU. Đó là một rất nhiều của chế độ chuyển đổi (mà không phải là, rõ ràng, như tốn kém như một bối cảnh chuyển đổi ) .

Và đó hoàn toàn không phải đề cập đến điều đó, ngoài vòng lặp tối ưu hóa ban đầu, yeslà một vòng lặp C được biên dịch rất đơn giản, nhỏ bé và vòng lặp shell của bạn không thể so sánh với chương trình tối ưu hóa trình biên dịch.


nhưng tôi đã sai:

Khi tôi nói trước đó yessử dụng stdio, tôi chỉ cho rằng nó đã làm bởi vì nó hoạt động rất giống như những gì đã làm. Điều này không chính xác - nó chỉ mô phỏng hành vi của họ theo cách này. Những gì nó thực sự làm rất giống với điều tôi đã làm bên dưới với trình bao: nó lần đầu tiên lặp lại để đưa ra các đối số của nó (hoặc ynếu không) cho đến khi chúng không thể phát triển hơn nữa mà không vượt quá BUFSIZ.

Một nhận xét từ nguồn ngay trước các fortrạng thái vòng lặp có liên quan :

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

yesNó không làm gì write()sau đó.


lạc đề:

(Như ban đầu được bao gồm trong câu hỏi và được giữ lại cho bối cảnh cho một lời giải thích có thể có thông tin đã được viết ở đây) :

Tôi đã thử timeout 1 $(while true; do echo "GNU">>file2; done;)nhưng không thể dừng vòng lặp.

Các timeoutvấn đề mà bạn có với sự thay thế lệnh - Tôi nghĩ rằng tôi nhận được nó bây giờ, và có thể giải thích lý do tại sao nó không dừng lại. timeoutkhông bắt đầu vì dòng lệnh của nó không bao giờ chạy. Vỏ của bạn tạo ra một vỏ con, mở một đường ống trên thiết bị xuất chuẩn của nó và đọc nó. Nó sẽ ngừng đọc khi đứa trẻ bỏ cuộc, và sau đó nó sẽ diễn giải tất cả những gì đứa trẻ viết cho $IFSviệc mở rộng và mở rộng toàn cầu, và với kết quả, nó sẽ thay thế mọi thứ từ $(khớp ).

Nhưng nếu đứa trẻ là một vòng lặp vô tận không bao giờ ghi vào đường ống, thì đứa trẻ không bao giờ ngừng lặp và timeoutdòng lệnh không bao giờ được hoàn thành trước đó (như tôi đoán) bạn làm CTRL-Cvà giết vòng lặp con. Vì vậy, không bao giờtimeout có thể giết vòng lặp cần hoàn thành trước khi nó có thể bắt đầu.


timeouts khác :

... chỉ đơn giản là không liên quan đến các vấn đề hiệu suất của bạn vì lượng thời gian mà chương trình shell của bạn phải dành để chuyển đổi giữa chế độ người dùng và chế độ kernel để xử lý đầu ra. timeouttuy nhiên, không linh hoạt như shell có thể dành cho mục đích này: trong đó shell excel có khả năng xử lý các đối số và quản lý các tiến trình khác.

Như đã lưu ý ở nơi khác, chỉ cần di chuyển chuyển [fd-num] >> named_filehướng của bạn đến mục tiêu đầu ra của vòng lặp thay vì chỉ hướng đầu ra ở đó cho lệnh được lặp đi lặp lại có thể cải thiện đáng kể hiệu suất bởi vì cách đó ít nhất open()chỉ cần thực hiện một lần. Điều này cũng được thực hiện dưới đây với |đường ống được nhắm mục tiêu là đầu ra cho các vòng bên trong.


so sánh trực tiếp:

Bạn có thể làm như:

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

Đó là loại giống như mối quan hệ phụ lệnh được mô tả trước đây, nhưng không có đường ống và đứa trẻ được backgrounded cho đến khi nó giết chết cha mẹ. Trong yestrường hợp cha mẹ thực sự đã được thay thế kể từ khi đứa trẻ được sinh ra, nhưng vỏ gọi yesbằng cách phủ lên quá trình của chính nó với cái mới và do đó, PID vẫn như cũ và con zombie của nó vẫn biết giết ai.


bộ đệm lớn hơn:

Bây giờ hãy xem về việc tăng write()bộ đệm của shell .

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

Tôi đã chọn số đó vì các chuỗi đầu ra dài hơn 1kb sẽ bị tách ra thành các phần riêng biệt write()cho tôi. Và đây là vòng lặp một lần nữa:

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

Đó là 300 lần lượng dữ liệu được viết bởi trình bao trong cùng một khoảng thời gian cho thử nghiệm này so với lần kiểm tra trước. Không quá xấu. Nhưng nó không phải là yes.


liên quan:

Theo yêu cầu, có một mô tả kỹ lưỡng hơn các nhận xét mã đơn thuần về những gì được thực hiện ở đây tại liên kết này .


@heemayl - có thể? Tôi không hoàn toàn chắc chắn tôi hiểu những gì bạn đang hỏi? khi một chương trình sử dụng stdio để ghi đầu ra, nó sẽ không có bộ đệm (như stderr theo mặc định) hoặc bộ đệm dòng (đến thiết bị đầu cuối theo mặc định) hoặc bộ đệm chặn (về cơ bản hầu hết các công cụ khác được đặt theo cách này theo mặc định) . Tôi không rõ ràng về những gì thiết lập kích thước của bộ đệm đầu ra - nhưng thường là khoảng 4kb. và vì vậy các hàm lib stdio sẽ thu thập đầu ra của chúng cho đến khi chúng có thể viết cả một khối. ddlà một công cụ tiêu chuẩn chắc chắn không sử dụng stdio chẳng hạn. hầu hết những người khác làm.
mikeerv

3
Phiên bản shell đang thực hiện open(hiện tại) writeAND close(mà tôi tin rằng vẫn đang chờ tuôn ra), VÀ tạo ra một quy trình mới và thực thi date, cho mỗi vòng lặp.
dave_thndry_085

@ dave_thndry_085 - truy cập / dev / trò chuyện . và những gì bạn nói không nhất thiết là đúng, như bạn có thể thấy ở đó. Ví dụ: thực hiện wc -lvòng lặp đó với bashtôi nhận được 1/5 sản lượng mà shvòng lặp thực hiện - bashquản lý hơn 100 nghìn writes()đến dash500 nghìn một chút .
mikeerv

Xin lỗi tôi đã mơ hồ; Tôi có nghĩa là phiên bản shell trong câu hỏi, tại thời điểm tôi đọc nó chỉ có phiên bản gốc với for((sec0=`date +%S`;...thời gian kiểm soát và chuyển hướng trong vòng lặp, không phải là những cải tiến tiếp theo.
dave_thndry_085

@ dave_thndry_085 - không sao đâu. câu trả lời đã sai dù sao về một số điểm cơ bản, và bây giờ sẽ khá chính xác, như tôi hy vọng.
mikeerv

20

Một câu hỏi tốt hơn sẽ là tại sao shell của bạn viết tệp quá chậm. Bất kỳ chương trình biên dịch độc lập nào sử dụng các tòa nhà viết tập tin có trách nhiệm (không xóa mọi ký tự tại một thời điểm) sẽ làm việc đó một cách hợp lý nhanh chóng. Những gì bạn đang làm, là viết các dòng bằng ngôn ngữ diễn giải (shell), và ngoài ra, bạn thực hiện rất nhiều thao tác đầu vào đầu vào không cần thiết. Có gì yeskhông:

  • mở một tập tin để viết
  • gọi các chức năng được tối ưu hóa và biên dịch để ghi vào luồng
  • luồng được đệm, do đó, một tòa nhà cao tầng (một công tắc đắt tiền sang chế độ kernel) rất hiếm khi xảy ra, trong các khối lớn
  • đóng một tập tin

Kịch bản của bạn làm gì:

  • đọc trong một dòng mã
  • diễn giải mã, thực hiện nhiều thao tác bổ sung để phân tích cú pháp đầu vào của bạn và tìm ra những việc cần làm
  • cho mỗi lần lặp của vòng lặp while (có lẽ không rẻ bằng ngôn ngữ diễn giải):
    • gọi datelệnh bên ngoài và lưu trữ đầu ra của nó (chỉ trong phiên bản gốc - trong phiên bản sửa đổi, bạn có được hệ số 10 bằng cách không làm điều này)
    • kiểm tra xem điều kiện kết thúc của vòng lặp có được đáp ứng không
    • mở một tập tin trong chế độ chắp thêm
    • echoLệnh parse , nhận ra nó (với một số mã khớp mẫu) dưới dạng shell dựng sẵn, gọi mở rộng tham số và mọi thứ khác trên đối số "GNU" và cuối cùng ghi dòng vào tệp đang mở
    • đóng tệp lại
    • lặp lại quá trình

Các phần đắt tiền: toàn bộ diễn giải là cực kỳ tốn kém (bash đang thực hiện rất nhiều quá trình tiền xử lý của tất cả đầu vào - chuỗi của bạn có khả năng chứa thay thế biến, thay thế quy trình, mở rộng dấu ngoặc, thoát ký tự và hơn thế nữa), mỗi lệnh gọi của nội dung có thể là một câu lệnh chuyển đổi với chuyển hướng đến một hàm liên quan đến nội dung, và rất quan trọng, bạn mở và đóng một tệp cho mỗi dòng đầu ra. Bạn có thể đặt >> filebên ngoài vòng lặp while để làm cho nó nhanh hơn rất nhiều , nhưng bạn vẫn đang ở trong một ngôn ngữ được diễn giải. Bạn khá may mắn đóecholà một shellin, không phải là một lệnh bên ngoài - nếu không, vòng lặp của bạn sẽ liên quan đến việc tạo ra một quy trình mới (fork & exec) trên mỗi lần lặp. Điều này sẽ khiến quá trình dừng lại - bạn đã thấy nó tốn kém như thế nào khi bạn có datelệnh trong vòng lặp.


11

Các câu trả lời khác đã giải quyết các điểm chính. Bên cạnh đó, bạn có thể tăng thông lượng của vòng lặp while bằng cách ghi vào tệp đầu ra khi kết thúc tính toán. So sánh:

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

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

với

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

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

Vâng, vấn đề này và tốc độ viết (ít nhất) tăng gấp đôi trong trường hợp của tôi
Pandya
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.