大文件下使用 bash 的 'while read line' 语句的效率问题

7
我使用了while循环来处理任务,它从一个大约有1000万行的文件中读取记录。 我发现随着时间的推移,处理速度变得越来越慢。 我做了一个含有100万行的模拟脚本,发现了问题所在。 但我仍不知道为什么,read命令是如何工作的?
seq 1000000 > seq.dat
while read s;
do
    if [ `expr $s % 50000` -eq 0 ];then
        echo -n $( expr `date +%s` - $A) ' ';
        A=`date +%s`;
    fi
done < seq.dat

终端输出时间间隔:

98 98 98 98 98 97 98 97 98 101 106 112 121 121 127 132 135 134

大约在50000行时,处理速度明显变慢。

1
awk 在这个任务中会更快。 - jordanm
非常好的测试案例!通过在输出中添加值$s可以帮助可视化问题。看起来你对测试很了解,但我还是要问一下显而易见的问题:1. 是否有其他进程可能会争夺资源?2. 你是否多次运行并得到相同的结果?我会在我的环境中尝试一下,如果我得到不同的结果,我会提供我的输入。祝你好运。 - shellter
2个回答

5
使用您的代码,我看到了相同的递增时间模式(从一开始就是这样!)。如果您想要更快的处理速度,您应该使用shell内部功能进行重写。这是我的bash版本:
tabChar="   "  # put a real tab char here, of course
seq 1000000 > seq.dat
while read s;
do
    if (( ! ( s % 50000 ) )) ;then
        echo $s "${tabChar}" $( expr `date +%s` - $A) 
        A=$(date +%s);
    fi
done < seq.dat

编辑 修复了错误,输出显示每一行都在被处理,现在只有每 50000 行才进行计时。唉!

过去的

  if ((  s % 50000 )) ;then

固定到
  if (( ! ( s % 50000 ) )) ;then

当前输出为 echo ${.sh.version} = 版本号为 JM 93t+ 2010-05-24

50000
100000   1
150000   0
200000   1
250000   0
300000   1
350000   0
400000   1
450000   0
500000   1
550000   0
600000   1
650000   0
700000   1
750000   0

输出bash

50000    480
100000   3
150000   2
200000   3
250000   3
300000   2
350000   3
400000   3
450000   2
500000   2
550000   3
600000   2
650000   2
700000   3
750000   3
800000   2
850000   2
900000   3
950000   2
800000   1
850000   0
900000   1
950000   0
1e+06    1

关于为什么原始测试用例需要这么长时间...不确定。我很惊讶看到每个测试周期的时间和时间增加。如果你真的需要理解这个问题,可能需要花些时间来更详细地检查测试内容。也许你可以运行trussstrace(取决于你的基本操作系统)来查看是否有什么问题。
希望这可以帮到你。

可能是因为使用 expr 意味着每次进行数学运算都要启动一个单独的进程。具体来说,它会为每个数字的 if 语句启动一个进程。正如你所展示的那样,使用内置评估速度要快得多。虽然这并不能解释时间增加的原因。+1 是因为你找到了问题的根源,而不是像我一样假设是 read 的问题。 - Tim Pote
谢谢,我发现这不是一个“读取”问题。使用 (( )) 而不是 'expr' 比我的原始脚本更快,并且我添加了进程标识打印。 - leemzoon
谢谢,我发现这不是一个“读取”问题。使用(( ))而不是'expr'比我的原始脚本快得多,并且我在我的进程中添加了一个pid打印,发现pid号码在增加。这可能是脚本变慢的原因吗?当操作系统用完其pid时会发生什么? - leemzoon
被创建的进程寿命非常短。不同的操作系统以不同的方式处理pid号码。有些增加1 + 1,有些则从可用的pid池中随机选择。我认为pids不是你的问题,但是回答你的问题“当操作系统用完pids会发生什么?”,我的猜测是你会收到一个操作系统级别的错误消息,如“无法创建进程”。祝好运。 - shellter
@shellter:您可以使用$'\t'来表示真正的制表符。 - l0b0

4

阅读是一个相对较慢的过程,正如《学习Korn Shell》的作者指出*(就在第7.2.2.1节上方)。还有其他程序,比如awksed已经高度优化,以执行本质上相同的操作:逐行从文件中读取并使用输入执行一些操作。

更不用说,每次进行减法或取模时,您都会调用外部进程,这可能很昂贵。awk具有这两个功能。

如下面的测试所示,awk要快得多:

#!/usr/bin/env bash

seq 1000000 | 
awk '
  BEGIN {
    command = "date +%s"
    prevTime = 0
  }
  $1 % 50000 == 0 {
    command | getline currentTime
    close(command)

    print currentTime - prevTime
    prevTime = currentTime
  }
'

输出:

1335629268
0   
0   
0   
0   
0   
0   
0   
0   
0   
0   
0   
0   
0   
0   
1   
0   
0   
0   
0

请注意,第一个数字相当于date +%s。就像在您的测试案例中一样,我让第一次匹配成功。
注意:是的,作者谈论的是Korn Shell,而不是OP标记的bash,但是在许多方面,bash和ksh非常相似。 ksh实际上是bash的超集。所以我认为,在一个shell中,read命令与另一个shell并没有太大的区别。

1
那本书中的注释适用于旧版Korn Shell。Ksh仍在不断发展,我认为这并不适用于新版kornshell(至少不太适用)。使用echo $ { .sh.version }查找您的版本。当然,OP已将其标记为“bash”问题。但是,是的,read较慢,测试有很多额外的子进程,但是每次迭代运行的是相同的一组进程。根据实际问题,awk可能更适合处理大型文件。祝大家好运。 - shellter
@shellter 添加了一些关于ksh和bash的简短说明。感谢您指出。 - Tim Pote
1
我的观点是,在旧版ksh中,read命令非常慢。虽然它已经得到了大部分修复,但是由于我现在在新版ksh和bash 3.2之间运行基本相同的代码,所以bash稍微快一些;-(我的ksh偏见现在显露出来了。 - shellter
@shellter 啊,我不知道。我只有一年半的严肃Shell脚本编写经验,而且我一直使用最新版本,所以我不知道旧版本的ksh是什么样子的。 - Tim Pote
正如您所指出的,read不是问题!我今天要出门了,晚上再回来看。祝大家好运。 - shellter

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接