Là đường ống, dịch chuyển, hoặc mở rộng tham số hiệu quả hơn?


26

Tôi đang cố gắng tìm cách hiệu quả nhất để lặp qua các giá trị nhất định là một số lượng giá trị nhất quán cách xa nhau trong danh sách các từ được phân tách bằng dấu cách (tôi không muốn sử dụng một mảng). Ví dụ,

list="1 ant bat 5 cat dingo 6 emu fish 9 gecko hare 15 i j"

Vì vậy, tôi muốn có thể chỉ lặp qua danh sách và chỉ truy cập 1,5,6,9 và 15.

EDIT: Tôi nên nói rõ rằng các giá trị tôi đang cố gắng nhận được từ danh sách không phải khác về định dạng so với phần còn lại của danh sách. Điều khiến họ đặc biệt chỉ là vị trí của họ trong danh sách (Trong trường hợp này, vị trí 1,4,7 ...). Vì vậy, danh sách có thể1 2 3 5 9 8 6 90 84 9 3 2 15 75 55nhưng tôi vẫn muốn những con số tương tự. Ngoài ra, tôi muốn có thể làm điều đó với giả sử tôi không biết độ dài của danh sách.

Các phương pháp tôi đã nghĩ đến cho đến nay là:

Phương pháp 1

set $list
found=false
find=9
count=1
while [ $count -lt $# ]; do
    if [ "${@:count:1}" -eq $find ]; then
    found=true
    break
    fi
    count=`expr $count + 3`
done

Phương pháp 2

set list
found=false
find=9
while [ $# ne 0 ]; do
    if [ $1 -eq $find ]; then
    found=true
    break
    fi
    shift 3
done

Phương pháp 3 Tôi khá chắc chắn rằng đường ống làm cho điều này trở thành lựa chọn tồi tệ nhất, nhưng tôi đã cố gắng tìm một phương pháp không sử dụng được thiết lập, vì tò mò.

found=false
find=9
count=1
num=`echo $list | cut -d ' ' -f$count`
while [ -n "$num" ]; do
    if [ $num -eq $find ]; then
    found=true
    break
    fi
    count=`expr $count + 3`
    num=`echo $list | cut -d ' ' -f$count`
done

Vì vậy, những gì sẽ được hiệu quả nhất, hoặc tôi đang thiếu một phương pháp đơn giản hơn?


10
Tôi sẽ không sử dụng tập lệnh shell ở vị trí đầu tiên nếu hiệu quả là mối quan tâm quan trọng. Làm thế nào lớn là danh sách của bạn mà nó làm cho một sự khác biệt?
Barmar


2
Không làm thống kê về các trường hợp thực tế của vấn đề của bạn, bạn sẽ không biết gì. Điều này bao gồm so sánh với "lập trình trong awk", vv Nếu số liệu thống kê quá đắt, thì việc tìm kiếm hiệu quả có lẽ không đáng.
David Tonhofer

2
Levi, chính xác thì cách "hiệu quả" trong định nghĩa của bạn là gì? Bạn muốn tìm một cách nhanh hơn để lặp đi lặp lại?
Sergiy Kolodyazhnyy

Câu trả lời:


18

Khá đơn giản với awk. Điều này sẽ giúp bạn có được giá trị của mọi trường thứ tư cho đầu vào có độ dài bất kỳ:

$ awk -F' ' '{for( i=1;i<=NF;i+=3) { printf( "%s%s", $i, OFS ) }; printf( "\n" ) }' <<< $list
1 5 6 9 15

Công việc này là tận dụng awkcác biến tích hợp như NF(số lượng trường trong bản ghi) và thực hiện một số forvòng lặp đơn giản để lặp lại dọc theo các trường để cung cấp cho bạn những biến bạn muốn mà không cần biết trước sẽ có bao nhiêu.

Hoặc, nếu bạn thực sự chỉ muốn những trường cụ thể như được chỉ định trong ví dụ của bạn:

$ awk -F' ' '{ print $1, $4, $7, $10, $13 }' <<< $list
1 5 6 9 15

Đối với câu hỏi về hiệu quả, cách đơn giản nhất sẽ là kiểm tra phương pháp này hoặc từng phương pháp khác của bạn và sử dụng timeđể hiển thị thời gian cần thiết; bạn cũng có thể sử dụng các công cụ như straceđể xem cách hệ thống gọi luồng. Cách sử dụng timetrông như:

$ time ./script.sh

real    0m0.025s
user    0m0.004s
sys     0m0.008s

Bạn có thể so sánh đầu ra giữa các phương pháp khác nhau để xem phương pháp nào hiệu quả nhất về thời gian; các công cụ khác có thể được sử dụng cho các số liệu hiệu quả khác.


1
Điểm tốt, @MichaelHomer; Tôi đã thêm một bên giải quyết câu hỏi "làm thế nào tôi có thể xác định phương pháp nàohiệu quả nhất ".
DopeGhoti

2
@LeviUzodike Về echovs <<<, "giống hệt" là một từ quá mạnh. Bạn có thể nói rằng stuff <<< "$list"nó gần giống với printf "%s\n" "$list" | stuff. Về echovs printf, tôi hướng bạn đến câu trả lời này
JoL

5
@DopeGhoti Thật ra nó có. <<<thêm một dòng mới ở cuối Điều này tương tự như cách $()loại bỏ một dòng mới từ cuối. Điều này là do các dòng được chấm dứt bởi dòng mới. <<<cung cấp một biểu thức dưới dạng một dòng, vì vậy nó phải được chấm dứt bởi một dòng mới. "$()"lấy các dòng và cung cấp chúng làm đối số, vì vậy sẽ hợp lý khi chuyển đổi bằng cách xóa dòng mới kết thúc.
JoL

3
@LeviUzodike awk là một công cụ được đánh giá thấp. Nó sẽ làm cho tất cả các loại vấn đề có vẻ phức tạp dễ dàng giải quyết. Đặc biệt là khi bạn đang cố gắng viết một regex phức tạp cho một cái gì đó như sed, bạn thường có thể tiết kiệm hàng giờ bằng cách thay vì viết nó theo thủ tục trong awk. Học nó sẽ chia cổ tức lớn.
Joe

1
@LeviUzodike: Có awklà một nhị phân độc lập phải khởi động. Không giống như perl hay đặc biệt là Python, trình thông dịch awk khởi động nhanh chóng (vẫn là tất cả các chi phí liên kết động thông thường khi thực hiện khá nhiều cuộc gọi hệ thống, nhưng awk chỉ sử dụng libc / libm và libdl. Ví dụ: sử dụng straceđể kiểm tra các cuộc gọi hệ thống của khởi động awk) . Nhiều shell (như bash) khá chậm, do đó, việc kích hoạt một tiến trình awk có thể nhanh hơn so với việc lặp qua các token trong danh sách với các shell được tích hợp ngay cả đối với các kích thước danh sách nhỏ. Và đôi khi bạn có thể viết một #!/usr/bin/awkkịch bản thay vì một #!/bin/shkịch bản.
Peter Cordes

35
  • Nguyên tắc đầu tiên của tối ưu hóa phần mềm: Đừng .

    Cho đến khi bạn biết tốc độ của chương trình là một vấn đề, không cần phải suy nghĩ về tốc độ của nó. Nếu danh sách của bạn dài khoảng đó hoặc chỉ ~ 100-1000 mặt hàng, bạn có thể thậm chí sẽ không nhận ra nó mất bao lâu. Có một cơ hội bạn dành nhiều thời gian suy nghĩ về việc tối ưu hóa hơn sự khác biệt sẽ là gì.

  • Quy tắc thứ hai: Biện pháp .

    Đó là cách chắc chắn để tìm hiểu và là cách đưa ra câu trả lời cho hệ thống của bạn. Đặc biệt là với vỏ sò, có rất nhiều, và chúng không giống nhau. Một câu trả lời cho một vỏ có thể không áp dụng cho bạn.

    Trong các chương trình lớn hơn, hồ sơ cũng ở đây. Phần chậm nhất có thể không phải là phần bạn nghĩ.

  • Thứ ba, quy tắc tối ưu hóa shell script đầu tiên: Không sử dụng shell .

    Thật đấy. Nhiều shell không được thực hiện nhanh chóng (vì việc khởi chạy các chương trình bên ngoài không cần phải có) và thậm chí chúng có thể phân tích lại các dòng mã nguồn mỗi lần.

    Sử dụng một cái gì đó như awk hoặc Perl thay thế. Trong một tiêu chuẩn vi mô tầm thường tôi đã làm, awknhanh hơn hàng chục lần so với bất kỳ hệ vỏ thông thường nào khi chạy một vòng lặp đơn giản (không có I / O).

    Tuy nhiên, nếu bạn sử dụng shell, hãy sử dụng các hàm dựng sẵn của shell thay vì các lệnh bên ngoài. Ở đây, bạn đang sử dụng exprkhông tích hợp trong bất kỳ hệ vỏ nào tôi tìm thấy trên hệ thống của mình, nhưng có thể được thay thế bằng mở rộng số học tiêu chuẩn. Ví dụ i=$((i+1))thay vì i=$(expr $i + 1)tăng i. Việc bạn sử dụng cuttrong ví dụ trước cũng có thể thay thế bằng các mở rộng tham số tiêu chuẩn.

    Xem thêm: Tại sao sử dụng vòng lặp shell để xử lý văn bản được coi là thực tiễn xấu?

Bước 1 và # 2 nên áp dụng cho câu hỏi của bạn.


12
# 0, trích dẫn mở rộng của bạn :-)
Kusalananda

8
Không phải là awkcác vòng lặp nhất thiết phải tốt hơn hay tệ hơn các vòng lặp vỏ. Đó là shell thực sự tốt trong việc chạy các lệnh và chỉ đạo đầu vào và đầu ra đến và từ các quá trình, và thẳng thắn thay vì cồng kềnh ở mọi thứ khác; trong khi các công cụ như awktuyệt vời trong việc xử lý dữ liệu văn bản, bởi vì đó là những gì vỏ và công cụ như awkđược tạo ra (tương ứng) ở nơi đầu tiên.
DopeGhoti

2
@DopeGhoti, mặc dù vỏ có vẻ khách quan chậm hơn. Một số rất đơn giản trong khi các vòng lặp dường như chậm hơn 25 lần dashso với gawkdashlà lớp vỏ nhanh nhất tôi đã thử nghiệm ...
ilkkachu

1
@Joe, đó là :) dashbusyboxkhông hỗ trợ (( .. ))- Tôi nghĩ đó là một phần mở rộng không chuẩn. ++cũng được đề cập rõ ràng là không bắt buộc, theo như tôi có thể nói, i=$((i+1))hoặc : $(( i += 1))là những người an toàn.
ilkkachu

1
Re "thêm thời gian suy nghĩ" : điều này bỏ qua một yếu tố quan trọng. Nó có thường xuyên chạy không, và cho bao nhiêu người dùng? Nếu một chương trình lãng phí 1 giây, có thể được sửa chữa bởi lập trình viên nghĩ về nó trong 30 phút, thì có thể sẽ lãng phí thời gian nếu chỉ có một người dùng sẽ chạy nó một lần. Mặt khác, nếu có một triệu người dùng, đó là một triệu giây hoặc 11 ngày thời gian của người dùng. Nếu mã lãng phí một phút của một triệu người dùng, đó là khoảng 2 năm thời gian của người dùng.
agc

13

Tôi sẽ chỉ đưa ra một số lời khuyên chung trong câu trả lời này, chứ không phải điểm chuẩn. Điểm chuẩn là cách duy nhất để trả lời các câu hỏi về hiệu suất một cách đáng tin cậy. Nhưng vì bạn không nói bạn đang thao tác bao nhiêu dữ liệu và tần suất bạn thực hiện thao tác này, nên không có cách nào để thực hiện một điểm chuẩn hữu ích. Những gì hiệu quả hơn cho 10 mặt hàng và những gì hiệu quả hơn cho 1000000 mặt hàng thường không giống nhau.

Như một quy tắc chung, việc gọi các lệnh bên ngoài sẽ tốn kém hơn so với thực hiện một cái gì đó với các cấu trúc shell thuần, miễn là mã shell thuần không liên quan đến một vòng lặp. Mặt khác, một vòng lặp shell lặp lại trên một chuỗi lớn hoặc một lượng lớn chuỗi có thể sẽ chậm hơn một lần gọi của một công cụ có mục đích đặc biệt. Ví dụ, thực hiện vòng lặp của bạn cutcó thể chậm đáng chú ý trong thực tế, nhưng nếu bạn tìm ra cách để làm toàn bộ với một điều duy nhấtcut gọi có khả năng nhanh hơn thực hiện cùng một thao tác với thao tác chuỗi trong vỏ.

Xin lưu ý rằng điểm cắt có thể thay đổi rất nhiều giữa các hệ thống. Nó có thể phụ thuộc vào kernel, vào cách cấu hình bộ lập lịch của kernel, trên hệ thống tập tin chứa các tệp thực thi bên ngoài, vào mức độ áp lực của CPU so với bộ nhớ hiện tại và nhiều yếu tố khác.

Đừng gọi exprđể thực hiện số học nếu bạn hoàn toàn lo lắng về hiệu suất. Thực tế, đừng gọi exprđể thực hiện số học. Shell có số học tích hợp, rõ ràng và nhanh hơn gọi expr.

Bạn dường như đang sử dụng bash, vì bạn đang sử dụng các cấu trúc bash không tồn tại trong sh. Vậy tại sao bạn không sử dụng một mảng? Một mảng là giải pháp tự nhiên nhất và cũng có khả năng là nhanh nhất. Lưu ý rằng các chỉ số mảng bắt đầu từ 0.

list=(1 2 3 5 9 8 6 90 84 9 3 2 15 75 55)
for ((count = 0; count += 3; count < ${#list[@]})); do
  echo "${list[$count]}"
done

Kịch bản của bạn có thể nhanh hơn nếu bạn sử dụng sh, nếu hệ thống của bạn có dấu gạch ngang hoặc ksh shthay vì bash. Nếu bạn sử dụng sh, bạn không nhận được các mảng được đặt tên, nhưng bạn vẫn nhận được mảng một trong các tham số vị trí mà bạn có thể đặt set. Để truy cập một phần tử tại một vị trí không được biết cho đến khi chạy, bạn cần sử dụng eval(chăm sóc trích dẫn mọi thứ đúng cách!).

# List elements must not contain whitespace or ?*\[
list='1 2 3 5 9 8 6 90 84 9 3 2 15 75 55'
set $list
count=1
while [ $count -le $# ]; do
  eval "value=\${$count}"
  echo "$value"
  count=$((count+1))
done

Nếu bạn chỉ muốn truy cập mảng một lần và đi từ trái sang phải (bỏ qua một số giá trị), bạn có thể sử dụng shiftthay vì chỉ số biến.

# List elements must not contain whitespace or ?*\[
list='1 2 3 5 9 8 6 90 84 9 3 2 15 75 55'
set $list
while [ $# -ge 1 ]; do
  echo "$1"
  shift && shift && shift
done

Cách tiếp cận nào nhanh hơn phụ thuộc vào vỏ và vào số lượng phần tử.

Một khả năng khác là sử dụng xử lý chuỗi. Nó có lợi thế là không sử dụng các tham số vị trí, vì vậy bạn có thể sử dụng chúng cho mục đích khác. Sẽ chậm hơn đối với số lượng lớn dữ liệu, nhưng điều đó khó có thể tạo ra sự khác biệt đáng chú ý đối với lượng nhỏ dữ liệu.

# List elements must be separated by a single space (not arbitrary whitespace)
list='1 2 3 5 9 8 6 90 84 9 3 2 15 75 55'
while [ -n "$list" ]; do
  echo "${list% *}"
  case "$list" in *\ *\ *\ *) :;; *) break;; esac
  list="${list#* * * }"
done

" Mặt khác, một vòng lặp shell lặp lại trên một chuỗi lớn hoặc một lượng lớn chuỗi có khả năng chậm hơn một lần gọi một công cụ có mục đích đặc biệt " nhưng nếu công cụ đó có các vòng lặp trong đó như awk thì sao? @ikkachu cho biết các vòng lặp awk nhanh hơn, nhưng bạn có nói rằng với <1000 trường lặp lại, lợi ích của các vòng lặp nhanh hơn sẽ không vượt quá chi phí gọi awk vì đó là lệnh bên ngoài (giả sử tôi có thể thực hiện cùng một nhiệm vụ trong shell vòng lặp với việc sử dụng chỉ được xây dựng trong các lệnh)?
Levi Uzodike

@LeviUzodike Vui lòng đọc lại đoạn đầu tiên trong câu trả lời của tôi.
Gilles 'SO- ngừng trở nên xấu xa'

Bạn cũng có thể thay thế shift && shift && shiftbằng shift 3ví dụ thứ ba của mình - trừ khi trình bao bạn đang sử dụng không hỗ trợ nó.
Joe

2
@Joe Thật ra, không. shift 3sẽ thất bại nếu có quá ít đối số còn lại. Bạn sẽ cần một cái gì đó nhưif [ $# -gt 3 ]; then shift 3; else set --; fi
Gilles 'SO- ngừng trở nên xấu xa'

3

awklà một lựa chọn tuyệt vời, nếu bạn có thể thực hiện tất cả quá trình xử lý của mình bên trong tập lệnh Awk. Mặt khác, bạn chỉ cần kết thúc đường ống đầu ra Awk đến các tiện ích khác, phá hủy mức tăng hiệu suất của awk.

bashLặp đi lặp lại trên một mảng cũng rất tuyệt, nếu bạn có thể phù hợp với toàn bộ danh sách của mình bên trong mảng (đối với hệ vỏ hiện đại có lẽ là một sự đảm bảo) bạn không bận tâm đến thể dục cú pháp mảng.

Tuy nhiên, một cách tiếp cận đường ống:

xargs -n3 <<< "$list" | while read -ra a; do echo $a; done | grep 9

Ở đâu:

  • xargs nhóm danh sách được phân tách khoảng trắng thành các lô gồm ba, mỗi dòng mới được phân tách
  • while read tiêu thụ danh sách đó và xuất ra cột đầu tiên của mỗi nhóm
  • grep lọc cột đầu tiên (tương ứng với mọi vị trí thứ ba trong danh sách gốc)

Cải thiện sự hiểu biết, theo ý kiến ​​của tôi. Mọi người đã biết những công cụ này làm gì, vì vậy thật dễ dàng để đọc từ trái sang phải và lý do về những gì sẽ xảy ra. Cách tiếp cận này cũng ghi lại rõ ràng độ dài sải chân ( -n3) và mẫu bộ lọc ( 9), vì vậy thật dễ dàng để biến đổi:

count=3
find=9
xargs -n "$count" <<< "$list" | while read -ra a; do echo $a; done | grep "$find"

Khi chúng tôi đặt câu hỏi về "hiệu quả", hãy chắc chắn suy nghĩ về "tổng hiệu quả trọn đời". Tính toán đó bao gồm nỗ lực của các nhà bảo trì để giữ cho mã hoạt động và chúng tôi túi thịt là những máy kém hiệu quả nhất trong toàn bộ hoạt động.


2

Có lẽ đây?

cut -d' ' -f1,4,7,10,13 <<<$list
1 5 6 9 15

Xin lỗi tôi đã không rõ ràng trước đây, nhưng tôi muốn có thể có được các số tại các vị trí đó mà không biết độ dài của danh sách. Nhưng cảm ơn, tôi quên cắt có thể làm điều đó.
Levi Uzodike

1

Đừng sử dụng các lệnh shell nếu bạn muốn có hiệu quả. Giới hạn bản thân vào đường ống, chuyển hướng, thay thế, vv và các chương trình. Đó là lý do tại sao xargsparallelcác tiện ích tồn tại - bởi vì bash trong khi các vòng lặp không hiệu quả và rất chậm. Chỉ sử dụng các vòng lặp bash như là giải pháp cuối cùng.

list="1 ant bat 5 cat dingo 6 emu fish 9 gecko hare 15 i j"
if 
    <<<"$list" tr -d -s '[0-9 ]' | 
    tr -s ' ' | tr ' ' '\n' | 
    grep -q -x '9'
then
    found=true
else 
    found=false
fi
echo ${found} 

Nhưng bạn có thể nhận được phần nào nhanh hơn với tốt awk.


Xin lỗi tôi không rõ ràng trước đây, nhưng tôi đang tìm một giải pháp có thể trích xuất các giá trị chỉ dựa trên vị trí của chúng trong danh sách. Tôi chỉ lập danh sách ban đầu như thế bởi vì tôi muốn nó rõ ràng là những giá trị tôi muốn.
Levi Uzodike

1

Theo tôi, giải pháp rõ ràng nhất (và có lẽ cũng hiệu quả nhất) là sử dụng các biến awk RS và ORS:

awk -v RS=' ' -v ORS=' ' 'NR % 3 == 1' <<< "$list"

1
  1. Sử dụng tập lệnh shell GNU sedPOSIX :

    echo $(printf '%s\n' $list | sed -n '1~3p')
  2. Hoặc với bashsự thay thế tham số của :

    echo $(sed -n '1~3p' <<< ${list// /$'\n'})
  3. Non- GNU ( tức là POSIX ) sedbash:

    sed 's/\([^ ]* \)[^ ]* *[^ ]* */\1/g' <<< "$list"

    Hoặc hơn nữa, sử dụng cả POSIX sed và shell script:

    echo "$list" | sed 's/\([^ ]* \)[^ ]* *[^ ]* */\1/g'

Đầu ra của bất kỳ trong số này:

1 5 6 9 15
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.