Maneira rápida de adicionar / anexar a um arquivo grande [fechado]

2

Eu tenho um script bash que lê um arquivo bastante grande, linha por linha, e para cada linha faz algum processamento e grava os resultados em outro arquivo. Atualmente, estou usando echo para anexar ao final do arquivo de resultado, mas à medida que o tamanho do arquivo cresce, isso fica mais lento e lento. Então, minha pergunta é: qual é a maneira mais rápida de acrescentar linhas a um arquivo grande?

A ordem pela qual as linhas são adicionadas ao arquivo é irrelevante para mim, por isso estou aberto para adicionar ao início ou ao end ou a qualquer localização aleatória no arquivo. Eu também estou executando o script em um servidor com grandes quantidades de RAM, por isso, se segurar os resultados em uma variável e escrever a coisa toda no final é mais rápido, isso funciona para mim também.

Na verdade, existem 2 scripts, eu coloquei uma amostra de cada um aqui (eles são parte do script real, mas eu removi algumas partes por uma questão de simplicidade.

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

O código acima eu uso assim: ./script filename > outputfile

E o segundo script é assim:

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

O segundo código é usado como: ./script input output

Atualizar

OK, aparentemente, o principal culpado foi o uso extensivo de backticks. O primeiro script foi strongmente modificado e agora é executado em 2 minutos em relação ao tempo de execução anterior de 50 minutos. Estou completamente feliz com isso. Obrigado a @BinaryZebra pelo seguinte Código:

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

Mas, para o segundo script, eu o modifiquei para parecer algo assim (também incluí um pouco mais do script atual aqui):

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

É muito mais rápido que antes: 7 minutos x 20 minutos, mas ainda preciso que seja mais rápido e ainda sinto a lentidão nos testes maiores. Ele está funcionando há aproximadamente 24 horas e o tamanho da saída é de quase 200 MB neste momento. Espero que o ficheiro de saída seja de aproximadamente 3 GB, o que poderá demorar 2 semanas, algo que não posso pagar. O tamanho / crescimento da saída também é não linear, diminuindo à medida que o tempo passa.

Existe mais alguma coisa que eu possa fazer ou é apenas o que é?

    
por Mohammad Gharehyazie 03.08.2015 / 22:39

4 respostas

3

Algumas ideias:
1.- Em vez de chamar cortar repetidamente em cada linha, aproveite a leitura.
A lista de variáveis cortadas em ' ' é:

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

Isso pode ser feito diretamente por meio da leitura:

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

uma linha mais longa, mas menor tempo de processamento. A variável a é apenas para preencher o espaço de valores não utilizados.

2.- O re-processamento de números variáveis a serem divididos por ':' poderia ser feito como este (sua pergunta é marcada como bash):

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

O que leva o código para:

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.- Quanto ao segundo script, não há descrição de quais são as variáveis sub1 e / ou sub2.

4.- Em geral, se você pudesse dividir o script em uma série de scripts menores, você poderia determinar cada um deles para descobrir onde é a área que consome tempo.

5.- E, como outras respostas recomendam, colocar o arquivo (e todos os resultados intermediários) em uma partição de memória irá tornar as coisas mais rápidas no primeiro arquivo lido. Execuções subseqüentes do script serão lidas do cache na memória, ocultando qualquer melhoria. Este guia deve ajudar.

    
por 04.08.2015 / 07:50
1

Você tentou colocar o arquivo em / dev / shm, que é um sistema de arquivos residido em RAM. Isso aumentará sua velocidade de acesso tanto para leitura quanto para gravação de arquivos. Finalmente, você pode copiar o arquivo de shm para a partição de disco permanente.

    
por 03.08.2015 / 22:53
1
  • Arquivos grandes podem ser um pouco mais lentos para trabalhar do que arquivos pequenos - e não quero dizer só porque há mais dados. Se o arquivo B for 1000 vezes o tamanho do arquivo A , então, pode levar 1001 ou 1002 vezes mais tempo para processar na sua totalidade.
  • Reabrindo o arquivo de saída (e buscando até o final) em cada iteração é um dreno de desempenho leve. Tente mudar seu segundo script para fazer

    while read -r line
    do
          ︙
                echo "$projectName $localProjectPath … $stride $similarity"
          ︙
    done < "$cluster_dir/$postClusterFile" >> "$overallRevisedFile"

    Se você não está adicionando conteúdo para um arquivo $overallRevisedFile previamente existente, apenas diga > "$overallRevisedFile" (em vez de >> ) na linha done .

    Mas eu não esperaria que isso fizesse uma grande diferença.

  • Se você não quiser redirecionar a saída padrão para todo o seu loop, você pode fazer algo parecido com

    while read -r line
    do
          ︙
                echo "$projectName $localProjectPath … $stride $similarity" >&3
          ︙
    done < "$cluster_dir/$postClusterFile"  3>> "$overallRevisedFile"

    Se você precisar acessar o arquivo de saída em mais do que apenas um loop, faça

    exec 3>> "$overallRevisedFile"
    while read -r line
    do
          ︙
                echo "$projectName $localProjectPath … $stride $similarity" >&3
          ︙
    done < "$cluster_dir/$postClusterFile"
       ︙
    (other code) >&3exec 3>&-
  • Algumas coisas que podem melhorar o seu script, mas não necessariamente mais rápido:

    • Você deve sempre citar suas referências de variáveis de shell (por exemplo, "$line" , "$cluster_dir" , "$postClusterFile" , e "$overallRevisedFile" ) a menos que você tenha uma boa razão para não e você está certo de que sabe o que está fazendo.
    • $(command) é praticamente equivalente para 'command' e é amplamente considerado mais legível.
    • Você tem (pelo menos) um echo que você não precisa.

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

      pode ser simplificado para

      newFilePath="${line2//$pattern2/$sub2}"
      
por 04.08.2015 / 02:58
1

Um problema aqui é que você faz:

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

Isso resultará em um aumento minimo do tempo de execução por iteração, simplesmente porque o output é repetidamente * open() * ed em um deslocamento um pouco maior do que na última vez. Eu digo minutamente porque não há virtualmente nenhuma diferença em quanto tempo leva para abrir um arquivo em um deslocamento anterior do que em um posterior, mas há algum . E toda vez que você open() O_APPEND , você o faz em uma posição ligeiramente mais avançada que o ypu da última vez. O tempo que isso leva para fazer depende do sistema de arquivos / config do disco subjacente, mas eu acho razoável supor que haverá algum custo por ocorrência, e que ele aumentará até certo ponto como o tamanho do arquivo. também faz.

O que você provavelmente deve fazer é apenas um open() e manter o descritor write() para a duração do loop. Você pode fazer algo como:

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

Esta pode não ser a causa principal. É a causa mais óbvia para mim que pode estar diretamente relacionada ao aumento de iterações, mas há muita coisa acontecendo em seu loop que provavelmente não deveria estar. Você quase definitivamente não deveria estar fazendo 10 ou mais avaliações de dados subdelados por iteração de loop. A melhor prática seria fazer zero desses - normalmente, se você não puder construir eficientemente um loop de shell independente de tal forma que ele possa executar totalmente do início ao fim sem um fork, então você provavelmente não deveria estar fazendo um em tudo.

Você deve focar suas avaliações com ferramentas que podem gerenciá-lo cortando um pedaço aqui e um pedaço lá em serial - que é como o pipeline bem escrito deve funcionar - em vez de tirar muitos mortos -Alterar loops por iteração de loop. Tente pensar assim:

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

É um pipeline em que cada um desses loops únicos é executado simultaneamente ao anterior.

Mas você prefere:

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

É assim que os loops de shell que dependem de subshells funcionam. Isso não é eficiente de forma alguma, e não ajuda que a entrada e a saída provavelmente não estejam vazias.

Subshells não são maus - são um meio conveniente de conter um contexto de avaliação. Mas é quase sempre melhor aplicá-las antes ou depois de um loop de qualquer tipo, conforme necessário para preparar ou condicionar entrada ou saída para melhor atender a um loop mais eficiente . Não os faça no loop, mas reserve um tempo para configurá-los corretamente primeiro e depois não faça mais nada assim que começar.

    
por 04.08.2015 / 04:11