【问题标题】:AWK/GAWK performanceAWK/GAWK 性能
【发布时间】:2017-09-16 18:14:40
【问题描述】:

我有一个 8400 万行的 XML,我正在 Red Hat Linux 中使用 'gawk' 处理它。 (好吧,有些人会建议使用其他工具而不是 GAWK,但我的 XML 没有多行标签或任何其他使 GAWK 不是该工作的好选择的特性。)

我关心的是性能。

我最初的 AWK 脚本是这样的:

# Test_1.awk
BEGIN {FS = "<|:|=";}
{
if ($3 == "SubNetwork id")
    {
    # do something
    }
}
END {
# print something
}

这会进行 8400 万次字符串比较,每行一次。

我注意到“SubNetwork id”仅在行中有 4 个字段时出现(NF=4),因此我更改了脚本以减少字符串比较:

# Test_2.awk
BEGIN {FS = "<|:|=";}
{
if (NF == 4)
    {
    if ($3 == "SubNetwork id")
        {
        # do something
        }
    }
}
END {
# print something
}

我运行它,发现我检查了 'NF == 4' 8400 万次(很明显)和 '$3 == "SubNetwork id"' 只有 300 万次。太好了,我减少了字符串比较的次数,我一直认为这比简单的整数比较更耗时(NF 是整数,对吗?)。

当我测试这两个脚本的性能时,我感到很惊讶。大多数时候 Test_1 比 Test_2 快。我多次运行它们以考虑可能正在使用 CPU 时间的其他进程,但总的来说,我的测试是在 CPU 或多或少“空闲”时运行的。

我的大脑告诉我,8400 万个整数比较加上 300 万个字符串比较肯定比 8400 万个字符串比较快,但显然我的推理有问题。

我的 XML 如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<ConfigDataFile xmlns:un="specific.xsd" xmlns:xn="generic.xsd">
    <configData dnPrefix="Undefined">
        <xn:SubNetwork id="ROOT_1">
            <xn:SubNetwork id="ROOT_2">
                <xn:attributes>
                ...
                </xn:attributes>
            </xn:SubNetwork>
            <xn:SubNetwork id="ID_1">
            ....
            </xn:SubNetwork>
            <xn:SubNetwork id="ID_2">
            .....
            </xn:SubNetwork>
        </xn:SubNetwork>
    </configData>
</ConfigDataFile>

任何帮助理解这个性能问题将不胜感激。

提前致谢。

【问题讨论】:

    标签: performance awk gawk


    【解决方案1】:

    我做了更多测试:

    1- 生成包含一些数据的大文件

    yes 'SomeSampleText SomeOtherText 33 1970 YetAnotherText 777 abc 1 AndSomeMore' | head -12000000 > SomeData.txt
    

    分隔符是空格!

    2- 多次运行这 6 个测试,并计算每个测试的平均时间。我是在 3 台不同的机器上完成的(使用 Red Hat Linux Enterprise 4)

    time gawk 'BEGIN {a = 0;} {if ($5 == "YetAnotherText") a ++;} END {print "a: " a;}' SomeData.txt
    time gawk 'BEGIN {a = 0;} {if ($0 ~ /YetAnotherText/) a ++;} END {print "a: " a;}' SomeData.txt
    time gawk 'BEGIN {a = 0;} /YetAnotherText/ {a ++;} END {print "a: " a;}' SomeData.txt
    time gawk 'BEGIN {a = 0;} {if (NF == 9) a ++;} END {print "a: " a;}' SomeData.txt
    time gawk 'BEGIN {a = 0;} {if ($1 == "SomeSampleText") a ++;} END {print "a: " a;}' SomeData.txt
    time gawk 'BEGIN {a = 0;} {if ($9 == "AndSomeMore") a ++;} END {print "a: " a;}' SomeData.txt
    

    3- 我得到了这些结果(数字是秒)

    -- Machine 1
    10.35
    39.39
    38.87
    10.40
    7.72
    12.26
    
    -- Machine 2
    8.50
    32.43
    31.83
    9.10
    6.54
    9.91
    
    -- Machine 3
    12.35
    13.55
    12.90
    14.40
    9.43
    14.93
    

    看起来在测试 2 和 3 中搜索模式 /YetAnotherText/ 非常慢。机器 3 除外...

    4- 生成另一个带有不同分隔符的数据的大文件

    yes "<SomeSampleText:SomeOtherText=33>1970<YetAnotherText:777=abc>1<AndSomeMore>" | head -12000000 > SomeData2.txt
    

    5- 运行 6 次测试,更改 FS

    time gawk 'BEGIN {FS = "<|:|=";} {if ($5 == "YetAnotherText") a ++;} END {print "a: " a;}' SomeData2.txt
    time gawk 'BEGIN {FS = "<|:|=";} {if ($0 ~ /YetAnotherText/) a ++;} END {print "a: " a;}' SomeData2.txt
    time gawk 'BEGIN {FS = "<|:|=";} /YetAnotherText/ {a ++;} END {print "a: " a;}' SomeData2.txt
    time gawk 'BEGIN {FS = "<|:|=";} {if (NF == 8) a ++;} END {print "a: " a;}' SomeData2.txt
    time gawk 'BEGIN {FS = "<|:|=";} {if ($2 == "SomeSampleText") a ++;} END {print "a: " a;}' SomeData2.txt
    time gawk 'BEGIN {FS = "<|:|=";} {if ($8 == "AndSomeMore>") a ++;} END {print "a: " a;}' SomeData2.txt
    

    6- 我得到了这些结果(我只为机器 3 做过,抱歉)

    66.17
    33.11
    32.16
    76.77
    37.17
    77.20
    

    我的结论(另见 @user31264 的 cmets):

    • 当有一个简单的分隔符而不是多个分隔符时,解析和拆分为字段似乎更快。
    • 通常获得 $N 比获得 $M 更快,其中 N
    • 在某些情况下,在整行中搜索 /pattern/ 比比较 $N == "pattern" 更快,尤其是当 N 不是该行的第一个字段时
    • 获取 NF 可能会很慢,因为必须解析行并计算字段,如果有多个分隔符则更是如此

    【讨论】:

      【解决方案2】:

      另一个简单的测试

      文件是整个样本的 3.000.000 行副本。结果是 3 次运行后的代表性时间(用于缓存和其他操作系统影响)

      # time awk 'BEGIN{FS="[<:=]"}NF>=4{a++}END{print a+0}' YourFile
      780100
      real    0m1.89s user    0m1.74s sys     0m0.01s
      
      # time awk 'BEGIN{FS="<|:|="}NF>=4{a++}END{print a+0}' YourFile
      780100
      real    0m2.00s user    0m1.91s sys     0m0.02s
      
      # time awk 'BEGIN{FS="<|:|="}NF>=4&&/:SubNetwork/{a++}END{print a+0}' YourFile
      780100
      real    0m3.09s user    0m2.93s sys     0m0.02s
      
      # time awk 'BEGIN{FS=":SubNetwork"}NF>=2{a++}END{print a+0}' YourFile
      1560200
      real    0m1.32s user    0m1.27s sys     0m0.02s
      
      # time awk '/:SubNetwork/{a++}END{print a}' YourFile
      1560200
      real    0m3.23s user    0m3.06s sys     0m0.02s
      

      表明如果您使用 :SubNetwork 作为字段分隔符,它是最快的。

      现在对于后续操作,您可能需要通过 FS="&lt;|:|=";$1=$1"";$0=$0""; ... your action ...; FS=":SubNetwork" 之类的方式拆分或重新分隔字段

      作为预过滤器的额外测试

      # time awk '$1 == "<xn:SubNetwork" || $1 == "<xn:Attributes" {a++}END{print a+0}' YourFile
      780100
      real    0m1.29s user    0m1.20s sys     0m0.03s
      

      【讨论】:

      • 我无法更改字段分隔符,因为在我的真实脚本中我需要找到更多字符串,而不仅仅是“子网络 ID”。我将我的真实脚本简化为两个较小的脚本(我原来的帖子中的脚本)以测试性能,令我惊讶的是比较 $3 == "SubNetwork id" 比比较 NF == 4 慢
      • 我使用:SubNetwork 作为仅通过 NF 捕获线路的测试样本。想法是像你一样工作,1)预过滤器快速 2)细节过滤器重。所以看看你的所有约束中是否有一个共同的部分可以轻松分隔线。
      • 添加了一个额外的测试,可能对您的最后一条评论有所帮助
      • 很明显,选择合适的 FS 可以产生重大影响@NeronLeVelu
      【解决方案3】:

      下面是一个简单的测试。第一行将 10,000,000 行“a b c d”输出到文件 a 中。 awk 是 GNU awk 4.1.3

      [~] yes 'a b c d' | h -10000000 > a
      [~] time awk '{if(NF==5)print("a")}' a
      2.344u 0.012s 0:02.36 99.5%     0+0k 0+0io 0pf+0w
      [~] time awk '{if(NF==5)print("a")}' a
      2.364u 0.008s 0:02.37 99.5%     0+0k 0+0io 0pf+0w
      [~] time awk '{if($4=="Hahaha")print("a")}' a
      2.876u 0.024s 0:02.90 99.6%     0+0k 0+0io 0pf+0w
      [~] time awk '{if($4=="Hahaha")print("a")}' a
      2.880u 0.020s 0:02.90 100.0%    0+0k 0+0io 0pf+0w
      [~] time awk '{if($1=="Hahaha")print("a")}' a
      2.540u 0.020s 0:02.56 100.0%    0+0k 0+0io 0pf+0w
      [~] time awk '{if($1=="Hahaha")print("a")}' a
      2.404u 0.004s 0:02.41 99.5%     0+0k 0+0io 0pf+0w
      

      如您所见,检查 $1 比检查 $4 更快,因为在前一种情况下,AWK 只需要解析到第一个单词的行。如果您只检查 NF,AWK 只计算单词,这在我的情况下甚至更快,但在您的情况下,计算单词可能比解析输入行直到第 3 个单词要慢。

      最后,我们可以这样加速 AWK:

      [~] time awk '/Hahaha/{if($4=="Hahaha")print("a")}' a
      1.376u 0.020s 0:01.40 99.2%     0+0k 0+0io 0pf+0w
      [~] time awk '/Hahaha/{if($4=="Hahaha")print("a")}' a
      1.372u 0.028s 0:01.40 99.2%     0+0k 0+0io 0pf+0w
      

      因为/Hahaha/ 不需要任何解析。

      如果在{ 之前添加/SubNetwork id/,可能会加快速度。

      如果您只处理带有“SuNetwork id”的行而忽略所有其他行,您可能想要这样做

      grep 'SubNetwork id' your_input_file | awk -f prog.awk
      

      这会大大加快速度,因为 grep 比 awk 快得多。

      最后,另一种加速 awk 的方法是使用 mawk,它比 gawk 快得多。不幸的是,有时它会产生与 gawk 不同的结果,因此应始终对其进行测试。

      【讨论】:

      • 根据手册,(gnu.org/software/gawk/manual/gawk.html#Fields),“当 awk 读取输入记录时,awk 实用程序会自动将记录解析或分离成称为字段的块。” 不是这样解析的 [$1 -> (blank)], [$2 -> xn], [$3 -> SubNetwork id], [$4 -> "ID_1">], [NF 设置为 4] 在您开始对这些字段进行任何操作之前?从您的回答中,我了解到只有在脚本中引用了 $3 时才分配/评估 $3,对吧?
      • 还有一点:/SubNetwork id/ 不需要解析,但还是整行里面的字符串比较,肯定比较费时间。 gawk 解析(将行拆分为字段)真的对性能如此不利,甚至比与整行进行字符串比较还要糟糕?
      • @MikeDuke - “当 awk 读取输入记录时,awk 实用程序会自动将记录解析或分离成称为字段的块。” - 这描述了语义,而不是内部行为。 “ gawk 解析(将行拆分为字段)真的对性能如此不利,甚至比与整行进行字符串比较还要糟糕?” - 你看到测试结果了吗?是的,它很慢。顺便说一句,模式搜索在字符串长度上是线性的,而且速度非常快(如果你做得正确的话)。 grep,在我的测试中,用时不到 0.1 秒。
      • 你为什么不自己测试一切?
      • 我在几台机器上测试它并等待收集所有数据。请看我的回复,你的回答很有帮助。谢谢@user31264
      猜你喜欢
      • 1970-01-01
      • 2013-02-03
      • 1970-01-01
      • 1970-01-01
      • 2016-12-08
      • 1970-01-01
      • 1970-01-01
      • 2014-05-05
      • 2013-10-22
      相关资源
      最近更新 更多