Быстрый способ добавления / добавления в большой файл

У меня есть скрипт bash, который читает довольно большой файл, по очереди, и для каждой строки обрабатывает и записывает результаты в другой файл. В настоящее время я использую echo для добавления к концу файла результатов, но по мере роста размера файла это происходит медленнее и медленнее. Итак, мой вопрос – это быстрый способ добавления строк в большой файл?

Порядок, по которому строки добавляются в файл, не имеет отношения ко мне, поэтому я открыт для добавления в начало или конец или в любое случайное местоположение в файле. Я также запускаю сценарий на сервере с большим объемом ОЗУ, поэтому, если держать результаты в переменной и написать все в конце быстрее, это работает и для меня.

На самом деле есть 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 aaaaa lang cloneID cloneSubID minToken stride similarity; do IFS=':' read -ra 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 -raa 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 минут против 20 минут, но мне нужно, чтобы он был еще быстрее, и я все еще чувствую замедление в более крупных тестах. Он работает в течение примерно 24 часов, а размер вывода составляет почти 200 МБ в этот момент. Я ожидаю, что выходной файл будет примерно 3 ГБ, так что это может занять 2 недели, которые я не могу себе позволить. Размер / рост выпуска также нелинейный, с течением времени замедляется.

Есть ли что-то, что я могу сделать, или это все, что это?

    4 Solutions collect form web for “Быстрый способ добавления / добавления в большой файл”

    Некоторые идеи:
    1.- Вместо того, чтобы вызывать несколько раз на каждой строке, используйте чтение.
    Список переменных, нарезанных ' ' :

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

    Это можно сделать непосредственно, прочитав так:

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

    более длинная строка, но более короткое время обработки. Переменная a предназначена для заполнения пространства неиспользуемых значений.

    2.- Повторная обработка переменных чисел, которые должны быть разделены на::, может быть выполнена как это (ваш вопрос отмечен bash):

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

    Который сводит код до:

     while read -r projectName filepath numbers aaaaa lang cloneID cloneSubID minToken stride similarity; do IFS=':' read -ra 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. Что касается второго скрипта, то нет описания того, что такое vars sub1 и / или sub2.

    4.- В общем случае, если вы могли бы разделить один скрипт на ряд небольших скриптов, вы могли бы каждый раз найти, где находится трудоемкая область.

    5.- И, как рекомендует какой-то другой ответ, размещение файла (и всех промежуточных результатов) в разделе памяти ускорит чтение первого файла. Последующие исполнения скрипта будут считываться из кеша в памяти, скрывая любое улучшение. Это руководство должно помочь.

    Пробовали ли вы поместить файл в / dev / shm, который является файловой системой с ram-файлами. Это повысит скорость доступа как для чтения, так и для записи. Наконец, вы можете скопировать файл из shm в постоянный раздел диска.

    • Большие файлы могут работать немного медленнее, чем небольшие файлы, и я не имею в виду только потому, что есть больше данных. Если файл B в 1000 раз превышает размер файла A , то для обработки в целом может потребоваться 1001 или 1002 раза.
    • Повторное открытие выходного файла (и поиск до конца) на каждой итерации – это небольшой пропуск производительности. Попробуйте изменить второй скрипт

        при чтении строки -r
       делать
             ︙
                   echo "$ projectName $ localProjectPath ... $ stride $ сходство"
             ︙
       done <"$ cluster_dir / $ postClusterFile" >> "$ totalRevisedFile" 

      Если вы не добавляете контент в ранее существующий файл $overallRevisedFile , просто скажите > "$overallRevisedFile" (вместо >> ) на done строке.

      Но я не ожидал, что это будет иметь большое значение.

    • Если вы не хотите перенаправлять стандартный вывод для всего цикла, вы можете сделать что-то вроде

        при чтении строки -r
       делать
             ︙
                   echo "$ projectName $ localProjectPath ... $ stride $ сходство" > & 3
             ︙
       done <"$ cluster_dir / $ postClusterFile" 3 >> "$ totalRevisedFile" 

      Если вам нужен доступ к выходному файлу не более чем в цикле, сделайте

        exec 3 >> "$ generalRevisedFile"
       при чтении строки -r
       делать
             ︙
                   echo "$ projectName $ localProjectPath ... $ stride $ сходство" > & 3
             ︙
       done <"$ cluster_dir / $ postClusterFile"
          ︙
       (другой код) > & 3exec 3> & - 
    • Несколько вещей, которые могут улучшить ваш сценарий, но не обязательно быстрее:

      • Вы всегда должны указывать ссылки на вашу оболочку (например, "$line" , "$cluster_dir" , "$postClusterFile" и "$overallRevisedFile" ), если у вас нет веских оснований, и вы уверены, что знаете, что вы делаю.
      • $( command ) в значительной степени эквивалентна ` command ` и считается более читаемой.
      • У вас есть (по крайней мере) одно echo которое вам не нужно.

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

        может быть упрощено до

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

    Одна из проблем заключается в том, что вы делаете:

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

    Это приведет к минимальному увеличению времени выполнения на итерацию просто потому, что output неоднократно * open() * ed при немного большем смещении, чем в последний раз. Я говорю, потому что практически нет разницы в том, сколько времени требуется, чтобы открыть файл с более ранним смещением, чем на более позднем, но есть некоторые . И каждый раз, когда вы open() O_APPEND вы делаете это в несколько более поздней позиции, чем ypu в последний раз. Как долго это нужно делать, зависит от дисковой конфигурации / базовой файловой системы, но я думаю, что вполне разумно предположить, что будет какая-то стоимость за вхождение, и что она будет увеличиваться до некоторой степени, как это делает файл.

    Вместо этого вы должны использовать только один open() и поддерживать дескриптор write() для жизни цикла. Вы можете сделать такую ​​вещь, как:

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

    Это не может быть основной причиной. Это самая очевидная причина для меня, которая может быть напрямую связана с увеличением итераций, но в вашем цикле много чего происходит, чего, вероятно, не должно быть. Вы почти наверняка не должны делать 10 или более подгруженных оценок данных на каждую итерацию цикла. Лучшей практикой было бы сделать нулевое из них – обычно, если вы не можете эффективно построить автономный цикл оболочки таким образом, чтобы он мог полностью выполняться от начала до конца без вилки, то вы просто не должны делать одно вообще.

    Вместо этого вы должны сосредоточить свои оценки на инструментах, которые могут управлять им, нарезая кусок здесь и кусок там по серийному номеру – именно так должен работать хорошо написанный конвейер, а не отбрасывать множество тупиковых циклов на итерацию цикла. Попытайтесь думать об этом так:

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

    Это конвейер, где каждая из этих одиночных циклов выполняется одновременно с tp до него.

    Но вы скорее всего:

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

    Вот как работают петли оболочки, которые полагаются на подоболочки. Это неэффективно, и не помогает, что вход и выход, вероятно, также не загружены.

    Подседки не являются злыми – они являются удобным средством для содержания контекста оценки. Но почти всегда лучше применять их до или после любого цикла, необходимого для подготовки или условия ввода или вывода, чтобы лучше соответствовать более эффективному циклу . Не делайте их в цикле, но найдите время, чтобы правильно настроить его, а затем больше не делайте, как только вы начнете.

    Linux и Unix - лучшая ОС в мире.