【问题标题】:How to make this sed script faster?如何使这个 sed 脚本更快?
【发布时间】:2009-12-01 19:13:47
【问题描述】:

我继承了这个 sed 脚本 sn-p,它试图删除某些空格:

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

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

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

输入文件是一个管道分隔文件,位于本地而不是网络上。这 4 行在使用 sed -f 执行的文件中

【问题讨论】:

  • 你是如何调用 sed 的?该文件是否确实在您的本地磁盘上,而不是在 NFS 挂载上?
  • 本地磁盘上的文件。我正在使用 sed -f 调用 sed
  • 请给出您正在使用的整个命令行。普通的sed -f 从标准输入读取并写入标准输出,这显然不是你在做什么。
  • 我不认为这真的有必要....假设sed -f < input.txt > output.txt
  • 我刚刚在一个填充了示例数据的 1.2GiB 数据文件上尝试了我的 C 实现。在一台 10 岁的 800 MHz Pentium III 上花费了 4 分 17 秒。虽然诚然它可能会根据实际输入而有所不同,但我认为它很有可能会显着优于您的 sed 脚本;)

标签: linux performance unix sed


【解决方案1】:

我能用 sed 做的最好的事情就是这个脚本:

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

在我的测试中,它的运行速度比您的 sed 脚本快 30%。性能的提升来自于结合前两个正则表达式并在不需要的地方省略了“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 获得了令人印象深刻的大量工作,以实现更快的解决方案并正确测试它,而不是仅仅假设它会更快。
【解决方案2】:

尝试将前两行更改为:

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

【讨论】:

  • 我的测试发现这和分开做没有区别。
  • 我的测试发现这可以减少解析 250MB 文件所需的时间。我还发现通过消除不需要的g 选项可以进一步减少。
【解决方案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

【讨论】:

  • ls 的无用使用。改为这样做:for chunk in x_?? 并且通配符已排序,因此此处不需要循环:cat x_??.out > output.txt
  • 已编辑以包括丹尼斯的 cmets。
【解决方案4】:

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

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。

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

【讨论】:

  • 您能建议一个 awk 命令来执行此操作吗?抱歉不是 awk 专家
  • 我使用 awk 测试了这个算法(与 D. Williamson 和 levislevis85 建议的 awk 程序相同),它比 OP 的 sed 脚本慢一点,比sed 脚本的优化版本。所以我不相信这种通用方法(在模式替换之前将记录拆分为字段)可能会导致任何加速(无论语言如何)。
【解决方案5】:

这个 Perl 脚本应该快得多

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

基本上,确保您的正则表达式编译一次('o' 标志),并且无需在仅适用于行尾和行首的正则表达式上使用 'g'。

另外,[\s\t]* 等价于 \s*

【讨论】:

  • 在这种情况下,“o”标志是多余的。 Perl 总是编译一个正则表达式,除非它里面有一个变量。
【解决方案6】:

这可能有效。我只测试了一点。

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

【讨论】:

  • 初步测试表明它的执行速度与sed 版本大致相同。
【解决方案7】:

Perl 怎么样:

#!/usr/bin/perl

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

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

如果您确实需要尽可能快的速度,请编写一个专门的 C 程序来完成这项任务。

【讨论】:

  • 在我的机器上,这比 OP 的 sed 脚本慢一点,并且比 OP 的 sed 脚本更优化的版本花费的时间要长两倍。
【解决方案8】:

使用 gawk,而不是 sed。

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

【讨论】:

    【解决方案9】:

    尝试在一个命令中完成:

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

    【讨论】:

    • 能否提供一些测试数据?没有测试就很难正确编写正则表达式:)
    【解决方案10】:

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

    #!/usr/local/bin/perl -p
    
    s#[\t ]+\|#|#g;
    s#\|[\t ]+#|#g;
    s#[\t ]*$##;
    s#^\|#null|#;
    

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

    【讨论】:

      【解决方案11】:

      我认为问题中正则表达式中的* 与使用+ 相比,大多数答案可能会显着放缓。考虑问题中的第一个替换

      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
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-11-06
        • 2018-05-19
        • 2021-10-18
        相关资源
        最近更新 更多