Tại sao có sự khác biệt về thời gian thực hiện tiếng vang và tiếng mèo?


15

Trả lời câu hỏi này khiến tôi phải hỏi một câu hỏi khác:
Tôi nghĩ các tập lệnh sau làm điều tương tự và tập lệnh thứ hai sẽ nhanh hơn nhiều, vì tập đầu tiên sử dụng catđể mở tập tin nhiều lần nhưng tập thứ hai chỉ mở tập tin một lần và sau đó chỉ lặp lại một biến:

(Xem phần cập nhật cho mã chính xác.)

Đầu tiên:

#!/bin/sh
for j in seq 10; do
  cat input
done >> output

Thứ hai:

#!/bin/sh
i=`cat input`
for j in seq 10; do
  echo $i
done >> output

trong khi đầu vào khoảng 50 megabyte.

Nhưng khi tôi thử cái thứ hai thì cũng quá chậm vì lặp lại biến ilà một quá trình lớn. Tôi cũng gặp một số vấn đề với tập lệnh thứ hai, ví dụ kích thước của tệp đầu ra thấp hơn dự kiến.

Tôi cũng đã kiểm tra trang người đàn ông của echocatđể so sánh chúng:

echo - hiển thị một dòng văn bản

cat - nối các tập tin và in trên đầu ra tiêu chuẩn

Nhưng tôi đã không nhận được sự khác biệt.

Vì thế:

  • Tại sao con mèo quá nhanh và tiếng vang quá chậm trong kịch bản thứ hai?
  • Hay là vấn đề với biến i? (Vì nằm trong man page của echongười ta nói nó sẽ hiển thị "một dòng văn bản" và vì vậy tôi đoán nó được tối ưu hóa chỉ cho các biến ngắn, không cho các biến rất rất dài như i. Tuy nhiên, đó chỉ là một phỏng đoán.)
  • Và tại sao tôi gặp vấn đề khi tôi sử dụng echo?

CẬP NHẬT

Tôi đã sử dụng seq 10thay vì `seq 10`không chính xác. Đây là mã chỉnh sửa:

Đầu tiên:

#!/bin/sh
for j in `seq 10`; do
  cat input
done >> output

Thứ hai:

#!/bin/sh
i=`cat input`
for j in `seq 10`; do
  echo $i
done >> output

(Đặc biệt cảm ơn roaima .)

Tuy nhiên, nó không phải là vấn đề. Ngay cả khi vòng lặp chỉ xảy ra một lần, tôi cũng gặp vấn đề tương tự: cathoạt động nhanh hơn nhiều echo.


1
và những gì về cat $(for i in $(seq 1 10); do echo "input"; done) >> output? :)
netmonk

2
Các echonhanh hơn. Điều bạn đang thiếu là việc bạn tạo ra trình bao thực hiện quá nhiều công việc bằng cách không trích dẫn các biến khi bạn sử dụng chúng.
roaima

Trích dẫn các biến không phải là vấn đề; vấn đề là chính biến i (nghĩa là sử dụng nó như một bước trung gian giữa đầu vào và đầu ra).
Aleksander

`echo $ i` - đừng làm điều này. Sử dụng printf và trích dẫn đối số.
PSkocik

1
@PSkocik Điều tôi đang nói là bạn muốn printf '%s' "$i"chứ không phải echo $i. @cuonglm giải thích một số vấn đề về tiếng vang tốt trong câu trả lời của mình. Để biết tại sao thậm chí trích dẫn không đủ trong một số trường hợp có tiếng vang, hãy xem unix.stackexchange.com/questions/65804/ mẹo
PSkocik

Câu trả lời:


24

Có một số điều cần xem xét ở đây.

i=`cat input`

có thể đắt tiền và có rất nhiều biến thể giữa các vỏ.

Đó là một tính năng được gọi là thay thế lệnh. Ý tưởng là lưu trữ toàn bộ đầu ra của lệnh trừ đi các ký tự dòng mới theo sau vào ibiến trong bộ nhớ.

Để làm điều đó, shell shell lệnh trong một subshell và đọc đầu ra của nó thông qua một đường ống hoặc ổ cắm. Bạn thấy rất nhiều biến thể ở đây. Trên tệp 50MiB ở đây, tôi có thể thấy bash chậm hơn 6 lần so với ksh93 nhưng nhanh hơn một chút so với zsh và nhanh gấp đôi yash.

Lý do chính cho bashviệc chậm là nó đọc từ ống 128 byte tại một thời điểm (trong khi các shell khác đọc 4KiB hoặc 8KiB tại một thời điểm) và bị phạt bởi hệ thống gọi qua đầu.

zshcần thực hiện một số xử lý hậu kỳ để thoát các byte NUL (các shell khác phá vỡ các byte NUL) và yashthậm chí còn xử lý các nhiệm vụ nặng nề hơn bằng cách phân tích các ký tự nhiều byte.

Tất cả các shell cần phải loại bỏ các ký tự dòng mới mà chúng có thể thực hiện ít nhiều hiệu quả.

Một số có thể muốn xử lý các byte NUL duyên dáng hơn các byte khác và kiểm tra sự hiện diện của chúng.

Sau đó, khi bạn có biến lớn đó trong bộ nhớ, mọi thao tác trên nó thường liên quan đến việc phân bổ thêm bộ nhớ và đối phó dữ liệu.

Ở đây, bạn đang vượt qua (đang có ý định vượt qua) nội dung của biến echo.

May mắn thay, echođược tích hợp trong trình bao của bạn, nếu không việc thực thi có thể đã thất bại với một danh sách arg quá dài . Thậm chí sau đó, việc xây dựng mảng danh sách đối số sẽ có thể liên quan đến việc sao chép nội dung của biến.

Vấn đề chính khác trong cách tiếp cận thay thế lệnh của bạn là bạn đang gọi toán tử split + global (bằng cách quên trích dẫn biến).

Vì thế, các shell cần coi chuỗi là một chuỗi các ký tự (mặc dù một số shell không và có lỗi về vấn đề đó) nên trong các ngôn ngữ UTF-8, điều đó có nghĩa là phân tích các chuỗi UTF-8 (nếu chưa được thực hiện như thế yash) , tìm kiếm các $IFSký tự trong chuỗi. Nếu $IFSchứa không gian, tab hoặc dòng mới (theo trường hợp theo mặc định), thuật toán thậm chí còn phức tạp và đắt tiền hơn. Sau đó, các từ kết quả từ việc phân tách đó cần phải được phân bổ và sao chép.

Phần toàn cầu sẽ còn đắt hơn. Nếu bất kỳ của những lời nói chứa các ký tự glob ( *, ?, [), sau đó vỏ sẽ phải đọc nội dung của một số thư mục và làm một số mô hình kết hợp đắt tiền ( bash's thực hiện ví dụ nổi tiếng là rất xấu tại đó).

Nếu đầu vào chứa thứ gì đó tương tự /*/*/*/../../../*/*/*/../../../*/*/*, điều đó sẽ cực kỳ tốn kém vì điều đó có nghĩa là liệt kê hàng ngàn thư mục và có thể mở rộng đến vài trăm MiB.

Sau đó echothường sẽ làm một số xử lý thêm. Một số triển khai mở rộng \xtrình tự trong đối số mà nó nhận được, có nghĩa là phân tích nội dung và có thể là phân bổ và sao chép dữ liệu khác.

Mặt khác, OK, trong hầu hết các shell catkhông được tích hợp sẵn, vì vậy điều đó có nghĩa là hủy bỏ một quy trình và thực thi nó (để tải mã và các thư viện), nhưng sau lần gọi đầu tiên, mã đó và nội dung của tệp đầu vào sẽ được lưu trữ trong bộ nhớ. Mặt khác, sẽ không có trung gian. catsẽ đọc số lượng lớn tại một thời điểm và viết ngay lập tức mà không cần xử lý và không cần phân bổ số lượng lớn bộ nhớ, chỉ cần một bộ đệm mà nó sử dụng lại.

Điều đó cũng có nghĩa là nó đáng tin cậy hơn nhiều vì nó không bị nghẹt các byte NUL và không cắt các ký tự dòng mới (và không phân tách + global, mặc dù bạn có thể tránh điều đó bằng cách trích dẫn biến và không mở rộng trình tự thoát mặc dù bạn có thể tránh điều đó bằng cách sử dụng printfthay vì echo).

Nếu bạn muốn tối ưu hóa nó hơn nữa, thay vì gọi catnhiều lần, chỉ cần chuyển inputnhiều lần đến cat.

yes input | head -n 100 | xargs cat

Sẽ chạy 3 lệnh thay vì 100.

Để làm cho phiên bản biến trở nên đáng tin cậy hơn, bạn cần sử dụng zsh(các shell khác không thể đối phó với byte NUL) và thực hiện:

zmodload zsh/mapfile
var=$mapfile[input]
repeat 10 print -rn -- "$var"

Nếu bạn biết đầu vào không chứa NUL byte, thì bạn có thể thực hiện nó một cách đáng tin cậy POSIXly (mặc dù nó có thể không hoạt động khi printfkhông được dựng sẵn) với:

i=$(cat input && echo .) || exit # add an extra .\n to avoid trimming newlines
i=${i%.} # remove that trailing dot (the \n was removed by cmdsubst)
n=10
while [ "$n" -gt 10 ]; do
  printf %s "$i"
  n=$((n - 1))
done

Nhưng điều đó sẽ không bao giờ hiệu quả hơn việc sử dụng cattrong vòng lặp (trừ khi đầu vào rất nhỏ).


Điều đáng nói là trong trường hợp tranh cãi kéo dài, bạn có thể thoát khỏi bộ nhớ . Ví dụ/bin/echo $(perl -e 'print "A"x999999')
cuonglm

Bạn đang nhầm với giả định rằng kích thước đọc có ảnh hưởng đáng kể, vì vậy hãy đọc câu trả lời của tôi để hiểu lý do thực sự.
schily

@schily, thực hiện 409600 lần đọc 128 byte mất nhiều thời gian hơn (thời gian hệ thống) hơn 800 lần đọc là 64k. So sánh dd bs=128 < input > /dev/nullvới dd bs=64 < input > /dev/null. Trong số 0,6 cần phải bash để đọc tệp đó, 0,4 được dành cho các readcuộc gọi hệ thống đó trong các thử nghiệm của tôi, trong khi các shell khác tốn ít thời gian hơn ở đó.
Stéphane Chazelas

Chà, dường như bạn không chạy một phân tích hiệu suất thực sự. Ảnh hưởng của cuộc gọi đọc (khi so sánh các kích cỡ đọc khác nhau) là aprox. 1% toàn bộ thời gian trong khi các chức năng readwc()trim()trong Burne Shell chiếm 30% toàn bộ thời gian và điều này rất có thể bị đánh giá thấp vì không có libc với gprofchú thích cho mbtowc().
schily

Để được \xmở rộng?
Mohammad

11

Vấn đề không phải là về catecho, đó là về biến trích dẫn bị lãng quên $i.

Trong tập lệnh shell giống như Bourne (ngoại trừ zsh), việc để lại các biến không yêu cầu các glob+splittoán tử gây ra trên các biến.

$var

thực sự là:

glob(split($var))

Vì vậy, với mỗi lần lặp lại, toàn bộ nội dung của input(không bao gồm các dòng mới) sẽ được mở rộng, chia tách, tạo khối. Toàn bộ quá trình yêu cầu shell để phân bổ bộ nhớ, phân tích chuỗi nhiều lần. Đó là lý do bạn có hiệu suất kém.

Bạn có thể trích dẫn biến để ngăn chặn glob+splitnhưng nó sẽ không giúp bạn nhiều, vì khi shell vẫn cần xây dựng đối số chuỗi lớn và quét nội dung của nó cho echo(Thay thế nội dung echobằng bên ngoài /bin/echosẽ cung cấp cho bạn danh sách đối số quá dài hoặc hết bộ nhớ phụ thuộc vào $ikích thước). Hầu hết echoviệc triển khai không tuân thủ POSIX, nó sẽ mở rộng \xcác chuỗi dấu gạch chéo ngược trong các đối số mà nó nhận được.

Với cat, shell chỉ cần sinh ra một quá trình mỗi vòng lặp lặp và catsẽ thực hiện sao chép i / o. Hệ thống cũng có thể lưu trữ nội dung tệp để xử lý mèo nhanh hơn.


2
@roaima: Bạn đã không đề cập đến phần toàn cầu, đó có thể là một lý do lớn, hình ảnh một cái gì đó /*/*/*/*../../../../*/*/*/*/../../../../có thể có trong nội dung tập tin. Chỉ muốn chỉ ra các chi tiết .
cuonglm

Gotcha cảm ơn bạn. Ngay cả khi không có điều đó, thời gian tăng gấp đôi khi sử dụng một biến không được trích dẫn
roaima

1
time echo $( <xdditg106) >/dev/null real 0m0.125s user 0m0.085s sys 0m0.025s time echo "$( <xdditg106)" >/dev/null real 0m0.047s user 0m0.016s sys 0m0.022s
netmonk

Tôi không hiểu tại sao trích dẫn không thể giải quyết vấn đề. Tôi cần mô tả thêm.
Mohammad

1
@ mohammad.k: Như tôi đã viết trong câu trả lời của mình, trích dẫn biến ngăn chặn glob+splitmột phần và nó sẽ tăng tốc vòng lặp while. Và tôi cũng lưu ý rằng nó sẽ không giúp bạn nhiều. Vì khi hầu hết các echohành vi hệ vỏ không tuân thủ POSIX. printf '%s' "$i"tốt hơn.
cuonglm

2

Nếu bạn gọi

i=`cat input`

điều này cho phép quá trình shell của bạn tăng thêm 50MB lên tới 200 MB (tùy thuộc vào việc triển khai ký tự rộng bên trong). Điều này có thể làm cho vỏ của bạn chậm nhưng đây không phải là vấn đề chính.

Vấn đề chính là lệnh trên cần đọc toàn bộ tệp vào bộ nhớ shell và echo $inhu cầu thực hiện tách trường trên nội dung tệp đó trong $i. Để thực hiện phân tách trường, tất cả văn bản từ tệp cần được chuyển đổi thành các ký tự rộng và đây là nơi dành phần lớn thời gian.

Tôi đã làm một số xét nghiệm với trường hợp chậm và nhận được những kết quả này:

  • Nhanh nhất là ksh93
  • Tiếp theo là Bourne Shell của tôi (chậm hơn gấp đôi so với ksh93)
  • Tiếp theo là bash (chậm hơn 3x so với ksh93)
  • Cuối cùng là ksh88 (chậm hơn 7 lần so với ksh93)

Lý do tại sao ksh93 là nhanh nhất dường như là ksh93 không sử dụng mbtowc()từ libc mà là một triển khai riêng.

BTW: Stephane bị nhầm lẫn rằng kích thước đọc có một số ảnh hưởng, tôi đã biên dịch Bourne Shell để đọc trong các đoạn 4096 byte thay vì 128 byte và có cùng hiệu suất trong cả hai trường hợp.


Các i=`cat input`lệnh không làm tách lĩnh vực, đó là echo $iđiều đó không. Thời gian dành cho i=`cat input`sẽ không đáng kể so với echo $i, nhưng không so với cat inputmột mình, và trong trường hợp bash, sự khác biệt là phần tốt nhất do bashđọc nhỏ. Thay đổi từ 128 thành 4096 sẽ không ảnh hưởng đến hiệu suất echo $i, nhưng đó không phải là điểm tôi đang thực hiện.
Stéphane Chazelas

Cũng lưu ý rằng hiệu suất của echo $isẽ thay đổi đáng kể tùy thuộc vào nội dung của đầu vào và hệ thống tập tin (nếu nó chứa IFS hoặc ký tự toàn cầu), đó là lý do tại sao tôi không thực hiện bất kỳ so sánh nào về câu trả lời trong câu trả lời của mình. Ví dụ, ở đây trên đầu ra của yes | ghead -c50M, ksh93 là chậm nhất trong tất cả, nhưng trên yes | ghead -c50M | paste -sd: -, nó là nhanh nhất.
Stéphane Chazelas

Khi nói về tổng thời gian, tôi đã nói về toàn bộ việc thực hiện và vâng, tất nhiên, việc tách trường xảy ra với lệnh echo. và đây là nơi dành phần lớn thời gian
schily

Tất nhiên bạn đúng rằng hiệu suất phụ thuộc vào nội dung od $ i.
schily

1

Trong cả hai trường hợp, vòng lặp sẽ chỉ được chạy hai lần (một lần cho từ seqvà một lần cho từ 10).

Hơn nữa cả hai sẽ hợp nhất khoảng trắng liền kề và bỏ khoảng trắng hàng đầu / dấu, để đầu ra không nhất thiết phải là hai bản sao của đầu vào.

Đầu tiên

#!/bin/sh
for j in $(seq 10); do
    cat input
done >> output

Thứ hai

#!/bin/sh
i="$(cat input)"
for j in $(seq 10); do
    echo "$i"
done >> output

Một lý do tại sao echochậm hơn có thể là biến không trích dẫn của bạn đang được phân tách ở khoảng trắng thành các từ riêng biệt. Đối với 50 MB đó sẽ là rất nhiều công việc. Trích dẫn các biến!

Tôi đề nghị bạn sửa những lỗi này và sau đó đánh giá lại thời gian của bạn.


Tôi đã thử nghiệm điều này tại địa phương. Tôi đã tạo một tệp 50 MB bằng cách sử dụng đầu ra của tar cf - | dd bs=1M count=50. Tôi cũng đã mở rộng các vòng lặp để chạy theo hệ số x100 để thời gian được chia tỷ lệ thành giá trị hợp lý (Tôi đã thêm một vòng lặp nữa xung quanh toàn bộ mã của bạn: for k in $(seq 100); do... done). Dưới đây là thời gian:

time ./1.sh

real    0m5.948s
user    0m0.012s
sys     0m0.064s

time ./2.sh

real    0m5.639s
user    0m4.060s
sys     0m0.224s

Như bạn có thể thấy không có sự khác biệt thực sự, nhưng nếu bất cứ điều gì phiên bản chứa echokhông chạy nhanh hơn một chút. Nếu tôi loại bỏ các trích dẫn và chạy phiên bản 2 bị hỏng của bạn, thời gian sẽ tăng gấp đôi, cho thấy trình bao đang phải thực hiện nhiều công việc hơn dự kiến.

time ./2original.sh

real    0m12.498s
user    0m8.645s
sys     0m2.732s

Thực tế vòng lặp chạy 10 lần, không phải hai lần.
fpmurphy

Tôi đã làm như bạn nói, nhưng vấn đề chưa được giải quyết. catlà rất, rất nhanh hơn echo. Kịch bản đầu tiên chạy trong trung bình 3 giây, nhưng kịch bản thứ hai chạy trong trung bình 54 giây.
Mohammad

@ fpmurphy1: Không. Tôi đã thử mã của tôi. Vòng lặp chỉ chạy hai lần, không phải 10 lần.
Mohammad

@ mohammad.k lần thứ ba: nếu bạn trích dẫn các biến của mình, vấn đề sẽ biến mất.
roaima

@roaima: Lệnh này tar cf - | dd bs=1M count=50làm gì? Liệu nó tạo ra một tập tin thông thường với cùng các ký tự bên trong nó? Nếu vậy, trong trường hợp của tôi, tệp đầu vào là hoàn toàn bất thường với tất cả các loại ký tự và khoảng trắng. Và một lần nữa, tôi đã sử dụng timenhư bạn đã sử dụng, và kết quả là tôi đã nói: 54 giây so với 3 giây.
Mohammad

-1

read nhanh hơn nhiều cat

Tôi nghĩ mọi người đều có thể kiểm tra điều này:

$ cd /sys/devices/system/cpu/cpu0/cpufreq
───────────────────────────────────────────────────────────────────────────────────────────
$ time for ((i=0; i<10000; i++ )); do read p < scaling_cur_freq ; done

real    0m0.232s
user    0m0.139s
sys     0m0.088s
───────────────────────────────────────────────────────────────────────────────────────────
$ time for ((i=0; i<10000; i++ )); do cat scaling_cur_freq > /dev/null ; done

real    0m9.372s
user    0m7.518s
sys     0m2.435s
───────────────────────────────────────────────────────────────────────────────────────────
$ type -a read
read is a shell builtin
───────────────────────────────────────────────────────────────────────────────────────────
$ type -a cat
cat is /bin/cat

catmất 9.372 giây. echomất .232vài giây.

readnhanh hơn gấp 40 lần .

Thử nghiệm đầu tiên của tôi khi $pđược lặp lại trên màn hình cho thấy readnhanh hơn 48 lần so với cat.


-2

echonghĩa là để đặt 1 dòng trên màn hình. Những gì bạn làm trong ví dụ thứ hai là bạn đặt nội dung của tệp vào một biến và sau đó bạn in biến đó. Trong phần đầu tiên, bạn ngay lập tức đưa nội dung lên màn hình.

catđược tối ưu hóa cho việc sử dụng này. echokhông phải. Ngoài ra, đặt 50Mb vào một biến môi trường không phải là một ý tưởng tốt.


Tò mò. Tại sao sẽ không echođược tối ưu hóa để viết văn bản?
roaima

2
Không có gì trong tiêu chuẩn POSIX nói rằng echo có nghĩa là đặt một dòng trên màn hình.
sợ hãi

-2

Đó không phải là về tiếng vang nhanh hơn, mà là về những gì bạn đang làm:

Trong một trường hợp, bạn đang đọc từ đầu vào và viết đến đầu ra trực tiếp. Nói cách khác, bất cứ điều gì được đọc từ đầu vào thông qua con mèo, đi đến đầu ra thông qua thiết bị xuất chuẩn.

input -> output

Trong trường hợp khác, bạn đang đọc từ đầu vào vào một biến trong bộ nhớ và sau đó viết nội dung của biến đó vào đầu ra.

input -> variable
variable -> output

Cái sau sẽ chậm hơn nhiều, đặc biệt nếu đầu vào là 50MB.


Tôi nghĩ rằng bạn phải đề cập rằng con mèo phải mở tập tin ngoài việc sao chép từ stdin và viết nó vào thiết bị xuất chuẩn. Đây là sự xuất sắc của kịch bản thứ hai, nhưng kịch bản thứ nhất rất tốt hơn so với kịch bản thứ hai.
Mohammad

Không có sự xuất sắc trong kịch bản thứ hai; mèo cần phải mở tập tin đầu vào trong cả hai trường hợp. Trong trường hợp đầu tiên, thiết bị xuất chuẩn của mèo đi trực tiếp vào tệp. Trong trường hợp thứ hai, thiết bị xuất chuẩn của con mèo đi trước đến một biến và sau đó bạn in biến đó vào tệp đầu ra.
Aleksander

@ mohammad.k, rõ ràng không có "sự xuất sắc" trong kịch bản thứ hai.
tự đại diện
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.