添加/附加到大文件的快速方法

添加/附加到大文件的快速方法

我有一个 bash 脚本,它逐行读取一个相当大的文件,并对每一行进行一些处理并将结果写入另一个文件。目前我正在使用echo附加到结果文件的末尾,但随着文件大小的增长,这会变得越来越慢。所以我的问题是向大文件追加行的快速方法是什么?

将行添加到文件的顺序与我无关,因此我愿意添加到开始或者结尾或任何随机的文件中的位置。我还在具有大量 RAM 的服务器上运行该脚本,因此如果将结果保存在变量中并在最后写入整个内容会更快,这对我来说也很有效。

实际上有 2 个脚本,我在这里放置了每个脚本的示例(它们是实际脚本的一部分,但为了简单起见我删除了一些部分。

while read line
do
    projectName=`echo $line | cut -d' ' -f1`
    filepath=`echo $line | cut -d' ' -f2`
    numbers=`echo $line | cut -d' ' -f3`
    linestart=`echo $numbers | cut -d: -f2`
    length=`echo $numbers | cut -d: -f3`
    lang=`echo $line | cut -d' ' -f9`
    cloneID=`echo $line | cut -d' ' -f10`
    cloneSubID=`echo $line | cut -d' ' -f11`
    minToken=`echo $line | cut -d' ' -f12`
    stride=`echo $line | cut -d' ' -f13`
    similarity=`echo $line | cut -d' ' -f14`
    currentLine=$linestart
    endLine=$((linestart + length))
    while [ $currentLine -lt $endLine ];
    do
        echo "$projectName, $filepath, $lang, $linestart, $currentLine, $cloneID, $cloneSubID, $minToken, $stride, $similarity"
        currentLine=$((currentLine + 1))
    done
done < $filename

上面的代码我是这样使用的:./script filename > outputfile

第二个脚本是这样的:

while read -r line;
do
    echo "$line" | grep -q FILE
    if [ $? = 0 ];
    then
        if [[ $line = *"$pattern"* ]];
        then
            line2=`echo "${line//$pattern1/$sub1}" | sed "s#^[^$sub1]*##"`
            newFilePath=`echo "${line2//$pattern2/$sub2}"`
            projectName=`echo $newFilePath | sed 's#/.*##'`
            localProjectPath=`echo $newFilePath | sed 's#^[^/]*##' | sed 's#/##'`
            cloneID=$cloneCounter
            revisedFile="revised-$postClusterFile-$projectName"
            overallRevisedFile="$cluster_dir/revised-overall-post-cluster"
            echo $projectName $localProjectPath $lang $cloneID $cloneSubID $minToken $stride $similarity >> $overallRevisedFile
            cloneSubID=$((cloneSubID + 1))
        fi
    fi
done < $cluster_dir/$postClusterFile

第二个代码的用法如下:./script input output


更新

好吧,显然罪魁祸首是广泛使用反引号。第一个脚本经过大量修改,现在运行时间为 2 分钟,而之前的运行时间为 50 分钟。我对此非常满意。感谢@BinaryZebra 提供以下代码:

while read -r projectName filepath numbers a a a a a lang cloneID cloneSubID minToken stride similarity;
do
    IFS=':' read -r a linestart length <<<"$numbers"
    currentLine=$linestart
    endLine=$((linestart + length))

    while [ $currentLine -lt $endLine ]; do
        echo "$projectName, $filepath, $lang, $linestart, $currentLine, $cloneID, $cloneSubID, $minToken, $stride, $similarity"
        currentLine=$((currentLine + 1))
    done
done < $filename >>$outputfile

但对于第二个脚本,我已将其修改为如下所示(我还在此处添加了更多实际脚本):

while read -r line;
do
  echo "$line" | grep -q FILE
  if [ $? = 0 ];
  then
    if [[ $line = *"$pattern"* ]];
    then
      IFS=$'\t' read -r a a filetest  <<< "$line"
      filetest="${filetest#*$pattern1}"
      projectName="${filetest%%/*}"
      localProjectPath="${filetest#*/}"
      cloneID=$cloneCounter
      revisedFile="revised-$postClusterFile-$projectName"
      echo $projectName $localProjectPath $lang $cloneID $cloneSubID $minToken $stride $similarity
      cloneSubID=$((cloneSubID + 1))
    fi
  else
    echo "This is a line: $line" | grep -q \n
    if [ $? = 0 ];
    then
       cloneCounter=$((cloneCounter + 1))
       cloneSubID=0
    fi
  fi
done < $cluster_dir/$postClusterFile >> $overallRevisedFile

它比以前快了很多:7 分钟 vs. 20 分钟,但我需要它更快,而且在较大的测试中我仍然感觉到速度变慢。它已经运行了大约24小时,此时输出大小接近200MB。我预计输出文件大约为 3GB,因此这可能需要 2 周的时间,但我无法承受。输出的大小/增长也是非线性的,随着时间的推移而减慢。

我还有什么可以做的吗,或者只是这样?

答案1

一些想法:
1.- 不要在每一行重复调用 cut,而是利用 read。
切入的变量列表' '是:

projectName 1
filepath 2
numbers 3
lang 9
cloneID 10
cloneSubID 11
minToken 12
stride 13
similarity 14

这可以直接通过阅读来完成:

while read -r projectName filepath numbers a a a a a lang cloneID cloneSubID minToken stride similarity;

生产线更长,但处理时间更短。变量a只是为了填充未使用值的空间。

2.- 重新处理要除以 ':' 的变量数字可以这样完成(您的问题标记为 bash):

IFS=':' read -r a linestart length <<<"$numbers"

这使得代码简化为:

while read -r projectName filepath numbers a a a a a lang cloneID cloneSubID minToken stride similarity;
do
    IFS=':' read -r a linestart length <<<"$numbers"

    currentLine=$linestart
    endLine=$((linestart + length))

    while [ $currentLine -lt $endLine ]; do
        echo "$projectName, $filepath, $lang, $linestart, $currentLine, $cloneID, $cloneSubID, $minToken, $stride, $similarity"
        currentLine=$((currentLine + 1))
    done
done < $filename >>$outputfile

3.- 至于第二个脚本,没有描述变量 sub1 和/或 sub2 是什么。

4.- 一般来说,如果您可以将一个脚本拆分为一系列较小的脚本,那么您可以对每个脚本进行计时,以找出耗时的区域。

5.-并且,正如其他一些答案所建议的那样,将文件(以及所有中间结果)放置在内存分区中将使读取第一个文件时的速度更快。脚本的后续执行将从内存中的缓存中读取,隐藏任何改进。本指南应该有帮助。

答案2

您是否尝试过将文件放入 /dev/shm,这是一个内存驻留的文件系统。它将提高您对文件的读取和写入的访问速度。最后,您可以将文件从 shm 复制到永久磁盘分区。

答案3

这里的问题是你这样做:

while : loop
do    : processing
      echo "$results" >>output
done  <input

这将导致每次迭代的执行时间急剧增加,因为outputopen()以比上次稍大的偏移量重复 ** ed。我说细致地因为有几乎在较早的偏移量处打开一个文件与在较晚的偏移量处打开文件所需的时间没有区别,但有一些。而每次你open() O_APPEND你这样做的位置比 ypu 上次的位置稍远。这需要多长时间取决于磁盘配置/底层文件系统,但我认为假设会有一些每次发生的成本,并且随着文件大小的增加,它也会在一定程度上增加。

你可能应该做的只是其中之一open()并维持write()循环生命周期的描述符。你可能会做这样的事情:

while : loop
do    : processing
      echo "$results"
done  <input >>output

这可能不是主要原因。对我来说,这是最明显的原因,可能与增加迭代直接相关,但循环中发生了很多可能不应该发生的事情。您几乎绝对不应该在每个循环迭代中进行 10 次或更多子壳数据评估。最佳实践是执行其中零个 - 通常,如果您无法有效地构建一个独立的 shell 循环,使其可以在没有 fork 的情况下从头到尾完全执行,那么您可能不应该执行此操作根本不。

相反,您应该使用可以通过在这里切片和那里切片来管理评估的工具来集中评估连续剧- 这就是编写良好的管道应该如何工作 - 而不是在每次循环迭代中抓取许多死循环。试着这样想:

input |
(Single app single loop) |
(Single app single loop) |
(Single app single loop) |
output

这是一个管道,其中每个单个循环与其前面的循环同时执行。

但你宁愿:

input |
(Single app \
        (input slice|single app single loop);
        (input slice|single app single loop);
        (input slice|single app single loop);
 single loop) |
 output

这就是依赖子 shell 的 shell 循环的工作原理。这无论如何都不是有效的,并且输入和输出可能也没有缓冲也没有帮助。

子 shell 并不是邪恶的 - 它们是包含求值上下文的便捷方法。但几乎总是最好在任何类型的循环之前或之后应用它们,因为这是为了更好地准备或条件输入或输出所必需的适合更有效的循环。不要在循环中执行这些操作,而是先花时间正确设置它们,然后一旦开始就不要再执行其他操作。

答案4

  • 大文件的处理速度可能比小文件稍慢——我的意思不仅仅是因为有更多的数据。如果文件 是文件大小的 1000 倍 A,那么整个处理过程可能需要 1001 或 1002 倍的时间。
  • 在每次迭代中重新打开输出文件(并查找结尾)会略微消耗性能。尝试更改你的第二个脚本

    读取 -r 行时
                echo "$projectName $localProjectPath … $stride $similarity"
    完成 <“$cluster_dir/$postClusterFile”>>“$overallRevisedFile”

    如果您不向先前存在的$overallRevisedFile文件添加内容,只需在行中输​​入> "$overallRevisedFile"( 而不是) 即可。>>done

    但我不认为这会产生很大的影响。

  • 如果您不想重定向整个循环的标准输出,您可以执行类似的操作

    读取 -r 行时
                echo "$projectName $localProjectPath … $stride $similarity">&3
    完成 <“$cluster_dir/$postClusterFile”  3>>“$overallRevisedFile”

    如果您需要在多个循环中访问输出文件,请执行

    执行3>>“$overallRevisedFile”
    读取 -r 行时
                echo "$projectName $localProjectPath … $stride $similarity">&3
    完成 <“$cluster_dir/$postClusterFile”
    (其他代码) >&3
    执行3>&-
  • 有一些事情可能会让你的脚本变得更好,但不一定更快:

    • 您应该始终引用您的 shell 变量引用(例如,"$line""$cluster_dir""$postClusterFile""$overallRevisedFile"),除非您有充分的理由不这样做,并且您当然你知道你在做什么。
    • $(command)几乎等同于并且被广泛认为更具可读性。`command`
    • 你(至少)有一个echo你不需要的东西。

      newFilePath=`echo "${line2//$pattern2/$sub2}"`
      

      可以简化为

      newFilePath="${line2//$pattern2/$sub2}"
      

相关内容