一个 Shell 程序的性能优化

发表于:2007-05-26来源:作者:点击数: 标签:
编写 Linux Shell 脚本程序不要仅限于完成基本的程序功能,认真的分析 Shell 脚本并找出优化的方法对个人能力的提高以及对脚本程序的 质量 改善都有重要的意义,希望读者能从本文中获得许多实用的 Shell 程序方法。 本文 Shell 程序运行环境: 程序运行环境
编写 Linux Shell 脚本程序不要仅限于完成基本的程序功能,认真的分析 Shell 脚本并找出优化的方法对个人能力的提高以及对脚本程序的质量改善都有重要的意义,希望读者能从本文中获得许多实用的 Shell 程序方法。

本文 Shell 程序运行环境:

  • 程序运行环境 Redhat Linux As3
  • GNU bash, version 2.05b.0(1)-release (i386-redhat-linux-gnu)
  • 代码清单:shellcode.txt

问题描述:有一个普通的通话话单文件(包括"计费号码","主叫号码","被叫号码","开始时间","结束时间","时长","费用"等其它字段),要求根据另外一个号段配置文件(由"号段下限"和"号段上限"两个字段组成)将此话单文件进行分拣过虑。

分拣规则:如果通话话单文件中的"计费号码"位于号段文件的某个号段内,则将此条记录计入结果文件 1,否则计入结果文件 2。

通话话单文件样例:


9013320003|9013320003|9918128025|20060814163420|20060814163450|30|20|00|01|005
            9926645208|9926645208|9918188065|20060814163415|20060814163545|90|30|00|01|005
            9934877207|9934877207|9936972003|20060814163620|20060814163930|190|50|00|01|005
            ......
            ......
            

号段配置文件样例:


9013305000,9013327999
            9013767000,9013768999
            9923670000,9923679999
            9928998000,9928999999
            9932310000,9932319999
            9932333400,9932333599
            9936034000,9936036999
            9936084000,9936084999
            9998537000,9998537999
            9998620000,9998629999
            9998690000,9998699999
            

例如:

对于通话话单文件的第一条记录中的"计费号码"为 9013320000,此号码正好属于号段配置文件的第一个号段 9013305000,9013327999中,即:条件 9013305000<= 9013320000 <=9013327999 成立,所以应该将通话话单文件的第一条记录计入结果文件 1 中;对于通话话单文件中的第二条记录的"计费号码"为 9926645208 它不属于号段文件中的任何一个段,所以应该将通话话单的第二条记录计入结果文件 2 中。

对于这样一个简单的问题首先想到的解决方法为:

解决方法1:

写一个双重循环,外层循环为逐条读取"通话话单文件"并获取每条记录的第一个字段的值"计费号码",内层循环:根据外层循环获得的"计费号码"在"号段文件"中循环比较,判断此号码是否属于相应号段。

程序代码如下(省略了文件存在性判断等语句):


while read f
            do
            org="$(expr substr ${f} 1 10)"   #取得"计费号码"存入变量org中
            while read numseg
            do
            nglow="$(expr substr ${numseg} 1 10 )"   #将号段下限存入变量nglow
            ngtop="$(expr substr ${numseg} 12 10 )"  #将号段上限存入变量ngtop
            if [ "$org" \> "$nglow"  -a "$org" \< $ngtop ]
            #判断"计费号码"是否在此号段内
            then
            echo "${f}" >> ./resultfile1.cdr #如果在此号段内,将此记录计入结果文件1中
            else
            echo "${f}" >> ./resultfile2.cdr #如果不在此号段内,将此记录计入结果文件2中
            fi
            done < ./numseg.txt
            done < ./rttest.txt
            

解决方法1 对于号段文件和通话话单的记录数都比较少的情况下基本可以完成工作,但是当两个文件的记录数较多(例如号段文件>50条,话单文件> 10000条)的时候,这种方法就会花费几个小时甚至几天的时间才能得出处理结果。此脚本程序执行慢的原因是对第二个循环内的比较运算只用了最简单的顺序比较方法,所以当号段文件的记录增多的时候,脚本的执行速度会急剧下降。

解决方法2:

将内层循环的逐个比较的方法改为二分查找法进行判断,程序代码如下:


#!/bin/bash
            #Author Xsh  date:08-15-2006
            #程序中使用了二分查找法进行号码的范围判断
            #为突出重点,省略了文件存在性判断和异常捕获以及帮助提示等程序语句
            #程序的工作目录为当前目录
            echo "Time:$(date)==>Strat to processing........." #显示程序开始运行时间
            while read numseg
            do
            tmplow="${tmplow} $(expr substr ${numseg} 1 10 & >/dev/null ) "
            tmptop="${tmptop} $(expr substr ${numseg} 12 10 & >/dev/null ) "
            done < ./numseg.txt
            #读取号段文件,下限号段存入变量tmplow,上限号段存入变量tmptop
            arr_lownug=(${tmplow}) #将下限号段存入数组arr_lownug
            arr_topnug=(${tmptop})	#将上限号段存入数组arr_topnug
            #定义函数checknum(),输入参数为需要检查的"计费号码",输出参数为0或者1
            #若checknum()输出为0 表示"计费号码" 不在号段文件的所有号段范围内
            #若checknum()输出为1 表示"计费号码" 在号段文件的所有号段范围内
            # checknum()函数中用二分搜索法进行号码的判断
            checknum(){
            thisnum=$1
            ckresult=0
            lowflag=0
            topflag=$(expr ${#arr_lownug[*]} - 1 )  #标注1
            MaxIndex=$(expr ${#arr_topnug[*]} - 1 ) #标注2
            midflag=0
            midflag=$(expr ${topflag} / 2 )		#标注3
            if [ "${thisnum}" \< "${arr_lownug[0]}" -o "${thisnum}" \>
            "${arr_topnug[${MaxIndex}]}"  ]
            then
            return 0
            else
            while [ "$lowflag" != "$midflag" ]
            do
            if[ "$thisnum" \> "${arr_lownug[${midflag}]}" -o "$thisnum" ==             "${arr_lownug[${midflag}]}" ]
            then
            lowflag=${midflag}
            midflag=$(expr `expr ${topflag} + ${lowflag}` / 2 ) #标注4
            elif["$thisnum"\<"${arr_lownug[${midflag}]}" -o "$thisnum" ==             "${arr_lownug[${midflag}]}" ]
            then
            topflag=${midflag}
            midflag=$(expr `expr ${topflag} + ${lowflag}` / 2 ) #标注5
            else
            echo "Error!"
            fi
            done
            if [ "$thisnum" \< "${arr_topnug[${lowflag}]}" -o "$thisnum" ==             "${arr_topnug[${lowflag}]}" ]
            then
            return 1
            else
            return 0
            fi
            fi
            }#函数定义完毕
            while read f
            do
            org="$(expr substr ${f} 1 10)" #标注6
            checknum ${org}
            returnval=$?
            if [ "$returnval" == "1"  ]
            then
            echo "${f}" >> ./Match_result.cdr  #将匹配的记录存入结果文件1
            else
            echo "${f}" >> ./NoMatch_result.cdr #将不匹配的记录存入结果文件2
            fi
            done < ./rttest.txt
            echo "Time:$(date) ==> Proclearcase/" target="_blank" >ccess is end! "
            exit 0;
            

将以上程序投入运行,号段文件有 71行记录,需要分拣的通话话单文件17 万条左右通过观察发现此方法的执行效率确实比解决方法1 提高了很多,但是仍需要数小时的时间才能执行完毕。我另外写了一个同样功能的C语言程序,执行同样的测试数据得出结果仅需要10~15秒的时间!shell 程序确实要比同样功能的C程序慢一些,但是目前的情况是用C程序处理只需要几秒钟时间, 而Shell程序确需要数小时!在用此Shell程序处理的时候我用Top命令看了一些系统资源的消耗发现CPU、内存以及IO的占用都极小,说明系统资源很充足,不是系统的问题造成了程序处理速度慢。问题出在哪了呢?

我用命名ps -ef 观察系统进程的运行情况,发现每过几秒种就会有一个expr进程产生,这个进程运行很短的时间就消失了(不仔细观察可能都看不到有expr这个进程产生)。这时候我觉得我好像发现程序运行慢的原因了:程序的几个循环里面都有用expr进行计算的语句,expr属于Shell外部命令,所以每次运算都要产生一个 expr进程,而程序的运行种最消耗时间的除了IO操作外就数产生新进程操作了,于是我决定把用expr进行计算的地方都尽量修改为用shell的内部命令进行计算。程序变成了下面的样子(标注1~标注6为修改过的地方):

程序代码如下:


―――――――――――――――――――――――――――――――――――――――
            #!/bin/bash
            #Author Xsh  date:08-15-2006
            echo "Time:$(date)==>Strat to processing........." #显示程序开始运行时间
            while read numseg
            do
            tmplow="${tmplow} $(expr substr ${numseg} 1 10 & >/dev/null ) "
            tmptop="${tmptop} $(expr substr ${numseg} 12 10 & >/dev/null ) "
            done < ./numseg.txt	#循环读取号段文件下限号段存入变量tmplow,上限号段存入变量tmptop
            arr_lownug=(${tmplow}) #将下限号段存入数组arr_lownug
            arr_topnug=(${tmptop})	#将上限号段存入数组arr_topnug
            #定义函数checknum(),输入参数为需要检查的"计费号码",输出参数为0或者1
            #函数checknum()输出为0 表示"计费号码" 不在号段文件的所有号段范围内
            #函数checknum()输出为1 表示"计费号码" 在号段文件的所有号段范围内
            #函数checknum()中用二分搜索法进行号码的判断
            checknum(){ #函数定义开始
            thisnum=$1
            ckresult=0
            lowflag=0
            topflag=$((${#arr_lownug[*]} - 1 ))   #标注1
            MaxIndex=$((${#arr_topnug[*]} - 1 ))  #标注2
            midflag=0
            midflag=$((${topflag} / 2 ))	      #标注3
            if [ "${thisnum}" \< "${arr_lownug[0]}" -o "${thisnum}" \> "${arr_topnug[${MaxIndex}]}" ]
            then
            return 0
            else
            while [ "$lowflag" != "$midflag" ]
            do
            if ["$thisnum"\> "${arr_lownug[${midflag}]}" -o "$thisnum" ==             "${arr_lownug[${midflag}]}"]
            then
            lowflag=${midflag}
            midflag=$(( $((${topflag} + ${lowflag})) / 2 )) #标注4
            elif["$thisnum"\<"${arr_lownug[${midflag}]}" -o "$thisnum" ==             "${arr_lownug[${midflag}]}"]
            then
            topflag=${midflag}
            midflag=$(($((${topflag} + ${lowflag})) / 2 )) #标注5
            else
            echo "Error!"
            fi
            done
            if ["$thisnum" \< "${arr_topnug[${lowflag}]}" -o "$thisnum" ==             "${arr_topnug[${lowflag}]}"]
            then
            return 1
            else
            return 0
            fi
            fi
            }#函数定义结束
            while read f
            do
            org="${f:0:10}"  #标注6
            checknum ${org}
            returnval=$?
            if [ "$returnval" == "1"  ]
            then
            echo "${f}" >> ./Match_result.cdr  #将匹配的记录存入结果文件1
            else
            echo "${f}" >> ./NoMatch_result.cdr #将不匹配的记录存入结果文件2
            fi
            done < ./rttest.txt
            echo "Time:$(date) ==> Proccess is end! "
            exit 0;
            

将修改过的程序进行运行,很快就得出了结果,总的运行时间没有超过8分钟。此时这个程序的运行效率已经基本可以让人接受了。同样的一个问题由于改进了程序算法和充分利用了LinuxShell的内置运算符,将程序的运行时间大大的缩短。当然此程序还有可以改进的地方,对此文感兴趣的读者可以对此程序做进一步优化。

原文转自:http://www.ltesting.net