在文件中打印单行的最快方法

Posted

技术标签:

【中文标题】在文件中打印单行的最快方法【英文标题】:Fastest way to print a single line in a file 【发布时间】:2013-03-15 23:45:32 【问题描述】:

我必须从一个大文件中提取一个特定的行(1500000 行),在多个文件的循环中多次,我在问自己最好的选择是什么 (在性能方面)。 有很多方法可以做到这一点,我主要使用这 2 个

cat $file | head -1

cat $file | sed -n '1p'

我找不到答案,他们是否都只获取第一行或两个(或两者)之一首先打开整个文件,然后获取第 1 行?

【问题讨论】:

使用time 测量命令。 为什么将cat 输入到工具中?他们都可以自己打开文件,如果您担心效率,他们可能会做得更好。但是,是的,管道应该只“流式传输”文件的前几个块(然后注意到消费者不再关心)。 顺便说一句,对于大文件中的特定行,使用提前编译语言的程序很可能比head "-$pos" "$file" | tail -1 运行得更快。 (像 C 一样,特别是使用 SIMD 内在函数来优化大内存块上换行符的计数,直到您接近正确的起始位置。如果在页面缓存中已经很热,它应该只在映射文件后受内存带宽的限制.) 【参考方案1】:

我进行了广泛的测试,发现如果您想要文件的每一行

while IFS=$'\n' read LINE; do
  echo "$LINE"
done < your_input.txt

比任何其他(基于 Bash 的)方法快得多。所有其他方法(如sed)每次都读取文件,至少到匹配的行。如果文件有 4 行长,您将得到:1 -&gt; 1,2 -&gt; 1,2,3 -&gt; 1,2,3,4 = 10 读取,而 while 循环只维护一个位置光标(基于IFS),因此总共只会读取4

在具有 ~15k 行的文件上,差异是惊人的:~25-28 秒(基于sed,每次提取特定行)与 ~0-1 秒(基于while...read,通读归档一次)

上面的示例还展示了如何以更好的方式将IFS 设置为换行符(感谢下面 cmets 的 Peter),这有望解决有时在 Bash 中使用 while... read ... 时出现的一些其他问题.

【讨论】:

echo $line 应该是 echo "$line" 以避免分词。或者更好的是,printf "%s" "$line" 即使使用像 -e 这样的行也是安全的。是的,我认为你想要(IFS=$'\n'; read line; printf "%s" "$line"),尽管它分叉了一个子shell,所以如果IFS=$'\n' read line &lt; file 工作而无需保存/恢复IFS shell 变量,你可能只对read 使用覆盖IFS。 感谢彼得的输入!这让我进一步测试,我发现了一些非常有趣的东西,这在逻辑上也是有道理的。参考上面。 现在你正在打印整个文件(除了像 "-e" 这样的行,它会回显会吃掉或抛出错误),所以你的循环可以用 cat "$file" 替换,这又是很多比bash 读取循环快。这个问题是关于提取 single 行的,这意味着您希望它在循环中按顺序重复每一行。如果你只是想为输入文件或流的每一行运行一些 bash 命令(即不同的循环体),是的,你当然会这样做。 但这不太可能是从大文件中获取just第 100k 行的最快方法,而这是其他答案试图有效地做到的。 是的,我就是这么说的。这处理每一行行的最快方法,但这与问题所问的问题(以及其他答案所回答的问题)不同。他们只是在 sed 或 head|tail 上使用重复循环来获得足够长的时间来测量,而不是因为他们实际上想要一系列行。你的答案属于Looping through the content of a file in Bash,除了它已经用while read 循环回答。 (并使用安全的 printf 而不是不安全的 echo 作为正文)。【参考方案2】:

如果您只想从一个大文件中打印 1 行(比如第 20 行),您也可以这样做:

head -20 filename | tail -1

我用 bash 做了一个“基本”测试,它似乎比上面的 sed -n '1p;q 解决方案表现得更好。

测试获取一个大文件并从中间某处打印一行(在10000000 行),重复 100 次,每次选择下一行。所以它选择行10000000,10000001,10000002, ...等等直到10000099

$wc -l english
36374448 english

$time for i in 0..99; do j=$((i+10000000));  sed -n $j'p;q' english >/dev/null; done;

real    1m27.207s
user    1m20.712s
sys     0m6.284s

对比

$time for i in 0..99; do j=$((i+10000000));  head -$j english | tail -1 >/dev/null; done;

real    1m3.796s
user    0m59.356s
sys     0m32.376s

用于从多个文件中打印一行

$wc -l english*
  36374448 english
  17797377 english.1024MB
   3461885 english.200MB
  57633710 total

$time for i in english*; do sed -n '10000000p;q' $i >/dev/null; done; 

real    0m2.059s
user    0m1.904s
sys     0m0.144s



$time for i in english*; do head -10000000 $i | tail -1 >/dev/null; done;

real    0m1.535s
user    0m1.420s
sys     0m0.788s

【讨论】:

单个sed 调用对于低行位置稍快一些,例如i + 1000。请参阅@roel's answer 和我的 cmets:对于像 100k 这样的大行位置,我可以重现与您非常相似的结果,并且还确认 Roel 的结果,对于较短的计数,单独使用 sed 更好。 (对我来说,在 i7-6700k 桌面 Skylake 上,head|tail 甚至比你更好,大 n 的相对加速更大。可能比你测试的系统更好的内核间带宽,因此管道所有数据的成本更低。) 【参考方案3】:

放弃对cat的无用使用并执行以下操作:

$ sed -n '1p;q' file

这将在打印行后退出sed 脚本。


基准测试脚本:

#!/bin/bash

TIMEFORMAT='%3R'
n=25
heading=('head -1 file' 'sed -n 1p file' "sed -n '1p;q file" 'read line < file && echo $line')

# files upto a hundred million lines (if your on slow machine decrease!!)
for (( j=1; j<=100,000,000;j=j*10 ))
do
    echo "Lines in file: $j"
    # create file containing j lines
    seq 1 $j > file
    # initial read of file
    cat file > /dev/null

    for comm in 0..3
    do
        avg=0
        echo
        echo $heading[$comm]    
        for (( i=1; i<=$n; i++ ))
        do
            case $comm in
                0)
                    t=$(  time head -1 file > /dev/null;  2>&1);;
                1)
                    t=$(  time sed -n 1p file > /dev/null;  2>&1);;
                2)
                    t=$(  time sed '1p;q' file > /dev/null;  2>&1);;
                3)
                    t=$(  time read line < file && echo $line > /dev/null;  2>&1);;
            esac
            avg=$avg+$t
        done
        echo "scale=3;($avg)/$n" | bc
    done
done

只需保存为benchmark.sh 并运行bash benchmark.sh

结果:

head -1 file
.001

sed -n 1p file
.048

sed -n '1p;q file
.002

read line < file && echo $line
0

**1,000,000 行文件的结果。*

所以sed -n 1p 的时间将随着文件的长度线性增长,但其他变化的时间将是恒定的(并且可以忽略不计),因为它们都在读取第一行后退出:

注意:由于在更快的 Linux 机器上,时间与原始帖子不同。

【讨论】:

或者可能是sed 1q file 不太忙。 @potong 我使用了这种格式,所以我可以用来打印文件中的任何一行。 理想情况下,您应该每次都重新创建文件。根据文件系统的不同,缓存会影响时序,以便第一次运行真正的 I/O 和后续运行受益。 +1 用于详细的性能比较。顺便说一句,在您的脚本中,caseheading 中的 sed 行 (sed 1q) 是不同的。 :) 最好使它们相同,特别是对于性能测试。无论如何,很好的答案! @Kent 好地方,在我测试和更新时滑过。还添加了一个漂亮的图表!【参考方案4】:

如果您真的只是获取第一行并读取数百个文件,那么考虑使用 shell 内置命令而不是外部外部命令,请使用 read,它是 bash 和 ksh 的内置 shell。这消除了使用awksedhead 等创建进程的开销。

另一个问题是对 I/O 进行定时性能分析。第一次打开然后读取文件时,文件数据可能没有缓存在内存中。但是,如果您再次对同一文件尝试第二个命令,则数据和 inode 已被缓存,因此计时结果可能会更快,几乎与您使用的命令无关。此外,inode 几乎可以永久缓存。例如,它们在 Solaris 上运行。或者无论如何,几天。

例如 linux 缓存一切和厨房水槽,这是一个很好的性能属性。但是,如果您没有意识到这个问题,它会使基准测试成为问题。

所有这些缓存效果“干扰”都取决于操作系统和硬件。

所以 - 选择一个文件,使用命令读取它。现在它被缓存了。运行相同的测试命令数十次,这是对命令和子进程创建的效果进行采样,而不是您的 I/O 硬件。

这是 sed vs read 10 次迭代,在读取文件一次后获取同一文件的第一行:

sed:sed '1p;q' uopgenl20121216.lis

real    0m0.917s
user    0m0.258s
sys     0m0.492s

阅读:read foo &lt; uopgenl20121216.lis ; export foo; echo "$foo"

real    0m0.017s
user    0m0.000s
sys     0m0.015s

这显然是人为的,但确实显示了内置性能与使用命令之间的差异。

【讨论】:

+1 不错的答案。我已经编辑了我的帖子以包含read 的使用,果然它是最快的(除了偶尔的 0.001 甚至没有注册)。 如何使用“读取”解决方案打印第 n 行?【参考方案5】:

如何避免管道? sedhead 都支持文件名作为参数。这样你就可以避免从猫身边经过。我没有测量它,但是 head 在较大的文件上应该更快,因为它会在 N 行之后停止计算(而 sed 会遍历所有文件,即使它不打印它们 - 除非您指定 quit 选项如上所述)。

例子:

sed -n '1p;q' /path/to/file
head -n 1 /path/to/file

再次,我没有测试效率。

【讨论】:

以上是关于在文件中打印单行的最快方法的主要内容,如果未能解决你的问题,请参考以下文章

最简单、最快的模板方法,可能是 PDF

找出所有低于 40 亿的素数的最快方法

lua 如何最快速度入门

在 C# 中浏览 XML 文件的最快方法是啥?

在文本文件中求和整数的最快方法

在文本文件 Java 中写入大量数据的最快方法