Linux While循环性能:非常慢

Linux While循环性能:非常慢,linux,bash,perl,Linux,Bash,Perl,我有input.txt和parts.txt文件,如下所示: input.txt CAR*BMW*X1*BUMBER*PLATE~ CAR*AUDI*A5*HOOD~ CAR*MAZDA*CX3*QNX*DIGITAL~ CAR*BMW*X5*SEAT~ SUV*FORD*EXPLORER*GLASS*SAFE~ CAR*FORD*FUSION*QNX~ CAR*GM*YUKON**~ 下面是用red hat Linux服务器编写的bash代码,需要很长时间。例如,我有一个10MB大小的输入文件

我有input.txt和parts.txt文件,如下所示:

input.txt
CAR*BMW*X1*BUMBER*PLATE~
CAR*AUDI*A5*HOOD~
CAR*MAZDA*CX3*QNX*DIGITAL~
CAR*BMW*X5*SEAT~
SUV*FORD*EXPLORER*GLASS*SAFE~
CAR*FORD*FUSION*QNX~
CAR*GM*YUKON**~
下面是用red hat Linux服务器编写的bash代码,需要很长时间。例如,我有一个10MB大小的输入文件,完成这个过程需要3个小时

#!/bin/bash
segment=CAR
position=3
a=0
b=0
while IFS='*' read -r -d'~' -a data; do
    if [ "${data[0]}" = "$segment" ]; then
        if [ ${#data[$position]} -gt 0 ]; then
           data[$position]=$(shuf -n1 "/tmp/parts.txt")
        b=$((b+1))
        fi
    a=$((a+1))
    fi
    # and output the data
     (IFS=*; printf "%s~" "${data[*]}";)  >> /tgt/output.txt
done < /src/input.txt 
代码说明: 对于input.txt文件中的所有“CAR”段,我试图使用shuf命令使用parts.txt文件中的随机数据更新该行中的第3个位置。 行(input.txt)中的每个字段都用*分隔,行分隔符为~

问题:我们能否改进上述while语句的性能? 我尝试在下面的代码中一次性编写output.txt,而不是在while循环中多次编写,但对于10MB的input.txt文件,这仍然需要时间

 (IFS=*; printf "%s~" "${data[*]}";)
done < input.txt > output.txt 
(IFS=*;printf“%s~”${data[*]})
完成output.txt

我在网上搜索,每个人都说pearl适合这种情况。我们可以使用pearl命令编写这个while循环吗?如何编写?

在进行优化时,第一步是计算读取输入文件所需的时间,而不进行任何处理。在我的系统上,一个10MB的文件只需要几百分之一秒

所以现在我们知道了最短的时间,我们需要看看优化策略。在示例代码中,您正在打开
parts.txt
,并从文件系统中读取输入文件中每个记录的文件。因此,您正在大量扩展所需的工作量。如果您可以将零件文件保存在内存中,并从中为输入文件中的每条记录获取一个随机元素,那就更好了

您可以进行的下一个优化是避免在每次需要零件时对零件列表进行洗牌。抓取一个随机元素比洗牌元素要好

您还可以跳过任何不以CAR开头的记录的处理,但这似乎是一个较小的优势

无论如何,以下各项实现了这些目标:

#!/usr/bin/env perl

use strict;
use warnings;
use Getopt::Long;
use Time::HiRes qw(time);

my ($parts_file, $input_file, $output_file) = ('parts.txt', 'input.txt', 'output.txt');

GetOptions(
    "parts=s",  \$parts_file,
    "input=s",  \$input_file,
    "output=s", \$output_file,
);

my $t0 = time;
chomp(
    my @parts = do {
        open my $fh, '<', $parts_file or die "Cannot open $parts_file: $!\n";
        <$fh>;
    }
);

open my $input_fh, '<', $input_file or die "Cannot open $input_file for input: $!\n";
local $/ = '~';

open my $out_fh,   '>', $output_file or die "Cannot open $output_file for output: $!\n";

my $rec_count = 0;
while (my $rec = <$input_fh>) {
    chomp $rec;
    $rec =~ s{^
        (CAR\*(?:[^*]+\*){2})
        [^*]+
    }{
        $1 . $parts[int(rand(@parts))]
    }xe;
    ++$rec_count;
    print $out_fh "$rec$/";
}

close $out_fh or die "Cannot close output file $output_file: $!\n";
printf "Elapsed time: %-.03f\nRecords: %d\n", time-$t0, $rec_count;
下面是它的工作原理:

-p
开关告诉Perl在命令行上指定的文件中的每一行上迭代,如果没有指定,则在STDIN上迭代。对于每一行,将该行的值放入
$\u
,然后在转到下一行之前,将
$\u
的内容打印到标准输出。这使我们有机会修改
$\uuu
,以便将更改写入标准输出。但是我们使用
-l
开关,它允许我们指定一个表示不同记录分隔符的八进制值。在本例中,我们将八进制值用于
~
字符。这会导致
-p
迭代由
~
分隔的记录,而不是
\n
。另外,
-l
开关条在输入时记录分隔符,并在输出时替换分隔符

但是,我们也使用
-a
-F
开关
-a
告诉Perl将输入自动拆分为
@F
数组,而
-F
让我们指定要自动拆分
*
字符。因为
-F
接受PCRE模式,并且
*
被认为是PCRE中的一个量词,所以我们用反斜杠将其转义

接下来,
-e
开关表示将以下字符串作为代码计算。最后,我们可以讨论代码字符串。首先是一个
BEGIN{…}
块,它将
@ARGV
中的一个值移位,并将其用作打开以读取明细表的文件名。一旦该文件名被关闭,脚本中稍后的
-p
开关就不会考虑读取它(BEGIN块发生在隐式
-p
循环之前)。因此,只需考虑<代码>中的代码开始{{}} /代码>块将记录分隔符临时设置为换行符,将部分文件读入数组,然后将记录分隔符再次返回到<代码> ~。< /P> 现在我们可以继续前进,越过开始区
@F
已成为保存给定记录中字段的容器。第四个字段(偏移量3)是您希望交换的字段。检查第一个字段(偏移量0)是否以
CAR
开头。如果是,则将第4个字段的内容设置为parts数组中的随机元素,但前提是该字段包含一个或多个字符

然后,我们将字段重新连接在一起,用星号分隔,并将结果分配回
$\uuuu
。我们的工作完成了。多亏了
-p
开关,Perl将
$的内容写入STDOUT,然后附加记录分隔符
~


最后,在命令行中,我们首先指定零件文件的路径,然后指定输入文件的路径,然后将STDOUT重定向到输出文件。

awk
这里是您的答案,我想:

awk 'BEGIN{while(getline<"parts.txt")r[++i]=$0;
           FS=OFS="*";
           RS=ORS="~";
           srand()}
     $1=="CAR"&&$4{$4=r[1+int(i*rand())]}
     1' input.txt >output.txt

awk'BEGIN{while(getline我完全同意,除了bash之外,还有其他语言更容易、更快

尽管如此,有时我还是无法抗拒挑战。让shell脚本快速运行的关键是尽可能少地在shell中运行;尝试找到一种使用外部实用程序批量工作而不是逐行工作的方法

下面的shell脚本是一个粗略的示例。它做了几件事以避免在shell中循环:

  • shuf
    的Gnu版本提供了
    -r
    标志,以生成从其输入中提取的(可能无限)随机行序列,而不是对输入进行混洗

  • paste
    命令会逐行连接两个输入流。(不幸的是,当最短的流结束时,它没有停止的方法,因此不能将其用于无限流。这会强制对输入文本进行笨拙的额外扫描,以计算行数。)

  • 可以将标准“第一个字段是
    CAR
    ,第四个字段不是空的”编码为单个常规表达式
    #!/usr/bin/env perl
    
    use strict;
    use warnings;
    use Getopt::Long;
    use Time::HiRes qw(time);
    
    my ($parts_file, $input_file, $output_file) = ('parts.txt', 'input.txt', 'output.txt');
    
    GetOptions(
        "parts=s",  \$parts_file,
        "input=s",  \$input_file,
        "output=s", \$output_file,
    );
    
    my $t0 = time;
    chomp(
        my @parts = do {
            open my $fh, '<', $parts_file or die "Cannot open $parts_file: $!\n";
            <$fh>;
        }
    );
    
    open my $input_fh, '<', $input_file or die "Cannot open $input_file for input: $!\n";
    local $/ = '~';
    
    open my $out_fh,   '>', $output_file or die "Cannot open $output_file for output: $!\n";
    
    my $rec_count = 0;
    while (my $rec = <$input_fh>) {
        chomp $rec;
        $rec =~ s{^
            (CAR\*(?:[^*]+\*){2})
            [^*]+
        }{
            $1 . $parts[int(rand(@parts))]
        }xe;
        ++$rec_count;
        print $out_fh "$rec$/";
    }
    
    close $out_fh or die "Cannot close output file $output_file: $!\n";
    printf "Elapsed time: %-.03f\nRecords: %d\n", time-$t0, $rec_count;
    
    perl -l0176  -apF'\*' -e '
        BEGIN{
            local $/ = "\n";
            chomp(@parts = do {open $fh, "<", shift(@ARGV); <$fh>})
        }
        $F[0] =~ m/^CAR/ && $F[3] =~ s/^\w+$/$parts[int(rand(@parts))]/e;
        $_ = join("*", @F);
    ' parts.txt input.txt >output.txt
    
    awk 'BEGIN{while(getline<"parts.txt")r[++i]=$0;
               FS=OFS="*";
               RS=ORS="~";
               srand()}
         $1=="CAR"&&$4{$4=r[1+int(i*rand())]}
         1' input.txt >output.txt
    
    # Count the number of "lines" in the input:
    count=$(tr '~' '\n' <input.txt | wc -l)
    # (paste) Paste together a column of random parts with the original input;
    # (sed)   then substitute  what is now the fifth column with the new first column
    #         if the criteria are met.
    # (cut)   Finally strip out the column of random parts and
    # (tr)    restore the record terminator ~ to return to the original format:
    paste -d '*' <(shuf -rn$count parts.txt) \
                 <(tr '~' '\n' <input.txt) |
    sed -E 's/^([^*]+)([*]CAR([*][^*]+){2}[*])[^*]+/\1\2\1/' |
    cut -f2- -d'*' |
    tr '\n' '~'
    
    # The input is 500,000 lines -- about 10MB -- created at random
    # from the short input data in the question
    $ tr '~' '\n' < input.txt | wc
    500000  500000 10498615
    $ tr '~' '\n' < input.txt | head
    CAR*BMW*X5*SEAT
    SUV*FORD*EXPLORER*GLASS*SAFE
    CAR*GM*YUKON**
    CAR*BMW*X1*BUMBER*PLATE
    SUV*FORD*EXPLORER*GLASS*SAFE
    SUV*FORD*EXPLORER*GLASS*SAFE
    CAR*AUDI*A5*HOOD
    CAR*AUDI*A5*HOOD
    CAR*AUDI*A5*HOOD
    CAR*FORD*FUSION*QNX
    
    # The script takes a couple of seconds
    $ time ./xform.sh > output.txt
    
    real    0m1.517s
    user    0m1.690s
    sys     0m0.121s
    
    # It seems to do the right thing:
    $ tr '~' '\n' < output.txt | head
    CAR*BMW*X5*NXP
    SUV*FORD*EXPLORER*GLASS*SAFE
    CAR*GM*YUKON**
    CAR*BMW*X1*GOOGLE*PLATE
    SUV*FORD*EXPLORER*GLASS*SAFE
    SUV*FORD*EXPLORER*GLASS*SAFE
    CAR*AUDI*A5*GOOGLE
    CAR*AUDI*A5*BLACKBERRY
    CAR*AUDI*A5*BLACKBERRY
    CAR*FORD*FUSION*NXP
    
    #!/bin/bash
    # $1 is the string to match in field 0. It must not contain / nor any regex
    # metacharacter.
    # $2 is the number of the field to substitute. It must be > 0.
    # Make the sed command:
    sedcmd='s/^([^*]*)([*]'$1'[*]([^*]*[*]){'$(($2-1))'})([^*]+)/\1\2\1/'
    # Count the number of "lines" in the input:
    count=$(tr '~' '\n' <input.txt | wc -l)
    # (paste) Paste together a column of random parts with the original input;
    # (sed)   then substitute  what is now the (position+1) column with the new first column
    #         if the criteria are met.
    # (cut)   Finally strip out the column of random parts and
    # (tr)    restore the record terminator ~ to return to the original format:
    paste -d '*' <(shuf -rn$count parts.txt) \
                 <(tr '~' '\n' <input.txt) |
    sed -E "$sedcmd" |
    cut -f2- -d'*' |
    tr '\n' '~'
    
    $ time ./xform.sh CAR 3 > output.txt
    
    real    0m1.519s
    user    0m1.712s
    sys     0m0.120s