如何使这个sed脚本更快?

时间:2009-12-01 19:13:48

标签: linux performance unix sed

我继承了这个尝试删除某些空格的sed脚本片段:

s/[\s\t]*|/|/g
s/|[\s\t]*/|/g
s/[\s] *$//g
s/^|/null|/g

对大约1Gb的文件进行操作。此脚本在我们的unix服务器上运行2个小时。任何想法如何加快它?

请注意\ s代表空格而\ t代表制表符,实际脚本使用实际空格和制表符而不是那些符号

输入文件是管道分隔文件,位于本地而非网络上。 4行位于使用sed -f

执行的文件中

11 个答案:

答案 0 :(得分:27)

我能用sed做的最好的就是这个剧本:

s/[\s\t]*|[\s\t]*/|/g
s/[\s\t]*$//
s/^|/null|/

在我的测试中,这比你的sed脚本快了大约30%。性能的提高来自前两个regexen的结合,省略了不需要的“g”标志。

然而,快30%只是一个温和的改进(在你的1GB数据文件上运行上述脚本仍然需要一个半小时)。我想看看能不能做得更好。

最后,我尝试过的其他方法(awk,perl和其他sed方法)没有更好的表现,除了 - 当然 - 一个普通的'C'实现。正如C所预期的那样,代码在这里发布有点冗长,但是如果你想要一个比其他方法更快的程序,你可能需要take a look at it

在我的测试中,C实现在你的sed脚本花费大约20%的时间内完成。因此,在Unix服务器上运行可能需要大约25分钟左右。

我没有花太多时间优化C实现。毫无疑问,有很多地方可以改进算法,但坦率地说,我不知道是否有可能削减超出它已经实现的时间。如果有的话,我认为它肯定会对你可以从其他方法(sed,awk,perl,python等)中获得什么样的性能设置一个上限。

编辑:原始版本有一个小错误导致它可能在输出结尾处打印错误的内容(例如,可能会打印出一个不应该存在的“null”)。今天我有一段时间看一看并解决了这个问题。我还优化了对strlen()的调用,这给它带来了另一个轻微的性能提升。

答案 1 :(得分:3)

我的测试表明sed可以很容易地在这样的东西上变成cpu绑定。如果您有一台多核机器,您可以尝试使用如下所示的脚本生成多个sed进程:

#!/bin/sh
INFILE=data.txt
OUTFILE=fixed.txt
SEDSCRIPT=script.sed
SPLITLIMIT=`wc -l $INFILE | awk '{print $1 / 20}'`

split -d -l $SPLITLIMT $INFILE x_

for chunk in ls x_??
do
  sed -f $SEDSCRIPT $chunk > $chunk.out &
done

wait 

cat x_??.out >> output.txt

rm -f x_??
rm -f x_??.out

答案 2 :(得分:2)

在您的示例中,我认为您正在清理文本文件中管道(|)分隔字段的开头和结尾处的空白区域。如果我这样做,我会将算法更改为以下内容:

for each line
    split the line into an array of fields
    remove the leading and trailing white space
    join the fields back back together as a pipe delimited line handling the empty first field correctly.

我也会使用其他语言,例如Perl或Ruby。

这种方法的优点是清理行的代码现在可以为每次调用处理更少的字符,并且即使需要更多的调用也应该执行得更快。

答案 3 :(得分:2)

尝试将前两行更改为:

s/[ \t]*|[ \t]*/|/g

答案 4 :(得分:1)

这个Perl脚本应该快得多

s/\s*|\s*/|/go;
s/\s *$//o;
s/^|/null|/o;

基本上,确保你的正则表达式被编译一次('o'标志),并且不需要在仅适用于行的结尾和开头的正则表达式上使用'g'。

此外,[\ s \ t] *相当于\ s *

答案 5 :(得分:1)

这可能有用。我只是测试了一下。

awk  'BEGIN {FS="|"; OFS="|"} {for (i=1; i<=NF; i++) gsub("[ \t]", "", $i); $1=$1; if ( $1 == "" ) $1 = "null"; print}'

答案 6 :(得分:1)

Perl怎么样:

#!/usr/bin/perl

while(<>) {
    s/\s*\|\s*/|/g;
    s/^\s*//;
    s/\s*$//;
    s/^\|/null|/;
    print;
}

编辑:显着改变了方法。在我的机器上,这几乎比你的sed脚本快3倍。

如果你真的需要尽可能最快的速度,可以编写专门的C程序来完成这项任务。

答案 7 :(得分:1)

使用gawk,而不是sed。

awk -vFS='|' '{for(i=1;i<=NF;i++) gsub(/ +|\t+/,"",$i)}1' OFS="|"  file

答案 8 :(得分:0)

尝试在一个命令中执行此操作:

sed 's/[^|]*(|.*|).*/\1/'

答案 9 :(得分:0)

你试过Perl吗?它可能会更快。

#!/usr/local/bin/perl -p

s#[\t ]+\|#|#g;
s#\|[\t ]+#|#g;
s#[\t ]*$##;
s#^\|#null|#;

编辑:实际上,它似乎比sed程序慢了三倍。奇怪...

答案 10 :(得分:0)

我认为与使用*相比,问题正则表达式中的+和大多数答案都可能是一个重大的减速。考虑问题中的第一个替换

s/[\s\t]*|/|/g

*匹配零个或多个项目后跟|,因此即使那些不需要替换的|也会被替换。将替换更改为

s/[\s\t]+|/|/g

只会更改前面有一个或多个空格和标签的|字符。

我没有sed可用,但我做了一个Perl实验。在我使用*的脚本的数据上花费的时间比使用+的脚本长7倍。

整个运行的时间是一致的。对于+,最小和最大时间之间的差异是平均值的4%,而*则是3.6%。 + :: *的平均时间比率为1:6.9。

实验详情

使用80mb文件进行测试,其中[st]\.的出现次数超过180000次,这些是小写字符st

该测试使用批处理命令文件,其中包含这两个命令中的30个,交替使用星号和加号。

perl -f TestPlus.pl input.ltrar > zz.oo
perl -f TestStar.pl input.ltrar > zz.oo

下面是一个脚本,另一个脚本仅将*更改为+,将star更改为plus

#! /bin/usr/perl
use strict;
use warnings;
use Time::HiRes qw( gettimeofday tv_interval );

my $t0 = [gettimeofday()];
while(<>)
{
    s/[st]*\././g;
}

my $elapsed = tv_interval ( $t0 );
print STDERR "Elapsed star $elapsed\n";

使用的Perl版本:

c:\test> perl -v
This is perl 5, version 16, subversion 3 (v5.16.3) built for MSWin32-x64-multi-thread
(with 1 registered patch, see perl -V for more detail)

Copyright 1987-2012, Larry Wall

Binary build 1603 [296746] provided by ActiveState http://www.ActiveState.com
Built Mar 13 2013 13:31:10