O código a seguir deve demonstrar e ajudar a testar Pattern Matching
expressões ineficientes em Parameter Substitution
uma variável de string separada por nova linha vs. matriz.
O objetivo é atingir pelo menos um desempenho equivalente ao grep
, ao filtrar apenas git status -s
resultados que envolvam index
alterações (total ou parcialmente staged
).
Então, basicamente, cada entrada de alteração que começa com um sinalizador de status curto do Git, como [MTARDC]
(sinalizando uma alteração em estágio/índice), incluindo sinalizadores duplos (sinalizando alterações parcialmente em estágio no índice e worktree
), exceto untracked
ou unstaged-only
alterações (começando com [ ?]).
Observe que R
os sinalizadores de alteração (renomear) podem ser seguidos por vários dígitos, veja também exemplos nos dados de teste abaixo (possivelmente até mesmo para índice e árvore de trabalho, ou seja, R104R104). Veja: formato curto do status do Git
Os dados de teste também contêm nomes de arquivos com caracteres especiais potencialmente problemáticos, como sequências de escape, espaço ou "e" comercial: [\ $*"]
.
Observe também que a substituição baseada em Pattern Matching requer uma negação de padrões, em comparação a um RegEx
for grep com os mesmos resultados. Para imprimir os resultados, simplesmente comente as &>/dev/null
partes.
#! /bin/bash
# Set extended pattern matching active
shopt -s extglob
clear
unset -v tmpVar tmpArr
# Populate tmpVar and tmpArr for testing
for i in {1..3}; do
tmpVar+=' A addedW1'[$i]$'\n'
tmpVar+='A addedI1'[$i]$'\n'
tmpVar+='AM addedA1'[$i]$'\n'
tmpVar+=' C copiedW1'[$i]$'\n'
tmpVar+='C copiedI1'[$i]$'\n'
tmpVar+='CR copied A1'[$i]$'\n'
tmpVar+=' D removedW1'[$i]$'\n'
tmpVar+='D removedI1'[$i]$'\n'
tmpVar+='DM removedA1'[$i]$'\n'
tmpVar+=' M modifiedW1'[$i]$'\n'
tmpVar+='M modifiedW1'[$i]$'\n'
tmpVar+='MR modifiedA1'[$i]$'\n'
tmpVar+=' R101 renamedW1'[$i]$'\n'
tmpVar+='R102 renamedI2'[$i]$'\n'
tmpVar+='R103D renamedA1'[$i]$'\n'
tmpVar+=' T typeChangedW1'[$i]$'\n'
tmpVar+='T typeChangedI1'[$i]$'\n'
tmpVar+='TM typeChangedA1'[$i]$'\n'
tmpVar+='?? exec2.bin'[$i]$'\n'
tmpVar+='?? file1.txt'[$i]$'\n'
tmpVar+='?? test.launch2'[$i]$'\n'
tmpVar+='A file00 0.bin'[$i]$'\n'
tmpVar+='A file11*1.bin'[$i]$'\n'
tmpVar+='A file22\03457zwei.bin'[$i]$'\n'
tmpVar+='A file33\t3.bin'[$i]$'\n'
tmpVar+='A file44$4.bin'[$i]$'\n'
tmpVar+='A file55"$(echo EXE)"5.bin'[$i]$'\n'
tmpVar+='M exec1.bin'[$i]$'\n'
tmpVar+=' M test.launch1'[$i]$'\n'
tmpVar+=' M myproject/src/main/java/util/MyUtil.java'[$i]$'\n'
tmpVar+='M myproject/src/test/util/MyUtilTest.java'[$i]$'\n'
tmpVar+='R104R104 myproject/src/test/util/MyUtil2Test.java'[$i]$'\n'
tmpVar+=' A invalidAdd'[$i]$'\n'
tmpVar+='R invalidRename'[$i]$'\n'
tmpArr+=(" A addedW1[$i]")
tmpArr+=("A addedI1[$i]")
tmpArr+=("AM addedA1[$i]")
tmpArr+=(" C copiedW1[$i]")
tmpArr+=("C copiedI1[$i]")
tmpArr+=("CR copied A1[$i]")
tmpArr+=(" D removedW1[$i]")
tmpArr+=("D removedI1[$i]")
tmpArr+=("DM removedA1[$i]")
tmpArr+=(" M modifiedW1[$i]")
tmpArr+=("M modifiedW1[$i]")
tmpArr+=("MR modifiedA1[$i]")
tmpArr+=(" R101 renamedW1[$i]")
tmpArr+=("R102 renamedI2[$i]")
tmpArr+=("R103D renamedA1[$i]")
tmpArr+=(" T typeChangedW1[$i]")
tmpArr+=("T typeChangedI1[$i]")
tmpArr+=("TM typeChangedA1[$i]")
tmpArr+=("?? exec2.bin[$i]")
tmpArr+=("?? file1.txt[$i]")
tmpArr+=("?? test.launch2[$i]")
tmpArr+=("A file00 0.bin[$i]")
tmpArr+=("A file11*1.bin[$i]")
tmpArr+=("A file22\03457zwei.bin[$i]")
tmpArr+=("A file33\t3.bin[$i]")
tmpArr+=("A file44$4.bin[$i]")
tmpArr+=('A file55"$(echo EXE)"5.bin['$i']')
tmpArr+=("M exec1.bin[$i]")
tmpArr+=(" M test.launch1[$i]")
tmpArr+=(" M myproject/src/main/java/util/MyUtil.java[$i]")
tmpArr+=("M myproject/src/test/util/MyUtilTest.java[$i]")
tmpArr+=("R104R104 myproject/src/test/util/MyUtil2Test.java[$i]")
tmpArr+=(" A invalidAdd[$i]")
tmpArr+=("R invalidRename[$i]")
done
# Perf-test array or string var filtering via grep
_IFS="$IFS"; IFS=$'\n'
startTime="$EPOCHREALTIME"
grep '^[MTARDC]' <<<"${tmpArr[*]}" &>/dev/null
stopTime="$EPOCHREALTIME"
IFS="$_IFS"
echo
awk 'BEGIN { printf "ELAPSED TIME via grep filtering from ARRAY: "; print '"$stopTime"' - '"$startTime"' }'
# Perf-test array filtering via Pattern Matching in Parameter Substitution
startTime="$EPOCHREALTIME"
printf '%s\n' "${tmpArr[@]/#[? ][?MTARDC]*([0-9]) *}" &>/dev/null
stopTime="$EPOCHREALTIME"
echo
awk 'BEGIN { printf "ELAPSED TIME via parameter substitution from ARRAY: "; print '"$stopTime"' - '"$startTime"' }'
# Perf-test string var filtering via Pattern Matching in Parameter Substitution
startTime="$EPOCHREALTIME"
printf '%s\n' "${tmpVar//[? ][?MTARDC]*([0-9]) *([^$'\n'])?($'\n')}" &>/dev/null
stopTime="$EPOCHREALTIME"
echo
awk 'BEGIN { printf "ELAPSED TIME via parameter substitution from VAR: "; print '"$stopTime"' - '"$startTime"' }'
# RESULT:
#ELAPSED TIME via grep filtering from ARRAY: 0.054975
#ELAPSED TIME via parameter substitution from ARRAY: 0.00031805
#ELAPSED TIME via parameter substitution from VAR: 4.546
Como pode ser visto, grep
é bom, mas a variante nº 2 (filtragem de matriz via Parameter substitution
) é muito mais rápida, então, para matrizes grandes, há uma boa alternativa ao grep.
A filtragem de strings var via Parameter Substitution
, por outro lado, é terrivelmente lenta.
Principalmente devido ao fato de que o padrão de correspondência não pode terminar com *
(o que removeria tudo até o final da string da primeira correspondência), mas porque *([^$'\n'])?($'\n')
, em vez disso, ele precisa, para corresponder (e remover) tudo em uma correspondência, até a próxima nova linha e, até certo ponto, devido à tmpVar//
correspondência gananciosa.
Existe outra maneira/padrão para o exemplo, para processar strings var com Pattern Matching
, da mesma forma que para array - sem usar a problemática e lenta correspondência de caracteres de negação de nova linha e para chegar perto da velocidade do exemplo de array?
Uma resposta curta para minha pergunta inicial poderia ser: "evite isso" .
Quanto a um
PARAMETER SUBSTITUTION
filtro sobre uma string separada por novas linhas como em:Não consegui encontrar uma alternativa viável para o problema, mas infelizmente precisei de extensão
PATTERN MATCHERS
.E agora?
Um exame detalhado e comparativo de como diferentes abordagens de filtragem funcionariam sob condições variadas — poucos ou muitos loops, com poucos ou muitos itens, em strings ou matrizes inteiras — produziu alguns resultados interessantes.
Ao usar uma máquina de alta especificação (aqui: 8 núcleos), ou não houver necessidade de se preocupar com eficiência, provavelmente não importará qual método for escolhido, ao permanecer abaixo de uma carga de 1000 elementos e 100 loops - talvez você queira parar de ler aqui.
No entanto, se você realmente (precisa) se importar com eficiência , seja devido às baixas especificações da máquina (4 núcleos/notebook/embarcado), ao consumo de energia ou aos tempos gerais de execução somados - ou em cenários de carga mais pesada, talvez você queira continuar lendo.
Os esforços para buscar uma alternativa melhor - ou até mesmo a melhor - se transformaram em uma lição valiosa sobre quando e por que evitar completamente a abordagem mencionada acima e retornaram informações sobre o que usar melhor em qual situação.
Geralmente, os desempenhos do método estão caindo linearmente ao número crescente de elementos a serem processados, ou ao número de loops iterados. Mas alguns já começam com uma penalidade, que aumenta junto com um número maior de elementos e iterações.
Alguns métodos terão um desempenho excepcionalmente bom em um conjunto limitado de casos de uso, enquanto se tornarão praticamente inutilizáveis para outros cenários - especialmente, onde um grande número de conjuntos (à 10 elementos) foi envolvido. Outros tiveram um desempenho excelente, se repetidos muitas vezes com um número baixo de elementos.
Alerta de spoiler:
PARAMETER SUBSTITUTION
sobre array , comnative array fetching
resultados foi a única abordagem que escalou e funcionou melhor para todos os tipos de casos de uso.Resultados em detalhes:
Para 10 ou menos elementos, os resultados entre duas máquinas completamente diferentes com sistemas operacionais diferentes não seriam muito diferentes ou seriam quase exatamente iguais.
PARAMETER SUBSTITUTION
(PSUB) não é recomendável para filtragem de elementos de substring VAR (${var//[? ]*}
) e tem desempenho excepcionalmente ruim , especialmente com strings muito grandes.Portanto, evite filtrar um grande número de elementos delimitados por nova linha em uma única
VAR STRING
viaPSUB
. Filtrar pequenos conjuntos <~50 uma vez pode ser OK, mas não faça algo assim com strings grandes e/ou loops internos:Nota:
EXTENDED PATTERN MATCHING
like<@*+?!>(...)
pode tornar um script significativamente mais lento (isso também vale para[[ $var =~ ... ]]
, em comparação com[[ $var == *...* ]]
).É necessário escanear toda a sequência de caracteres
EXTENDED PATTERN MATCHING
aqui para corresponder e remover elementos individuais da sequência, o que torna esse tipo de filtro muito mais lento.Então, para grandes sequências de elementos, provavelmente é melhor dividir em novas linhas primeiro e processar a matriz resultante, conforme indicado abaixo, ou fazer tudo isso dentro de um
AWK
script, ou usarGREP
(quando não for repetido com muita frequência), que mostrou os melhores resultados para esse tipo de caso de uso, superando todos os outros métodos, quando as cargas estão ficando muito altas (~1 milhão de sequências de elementos).Quando aplicado a matrizes , por outro lado,
PARAMETER SUBSTITUTION
pode ser uma das melhores opções disponíveis para quase qualquer cenário.Ele demonstrou eficiência superior, superando outros métodos/ferramentas dedicadas como
SED
,GREP
eAWK
, especialmente para conjuntos de elementos de pequeno a médio porte (<100.000), com até 1.000 iterações e, ainda mais obviamente, com um número ainda maior de iterações.Começando com 100.000 elementos e para um determinado número de 1.000 iterações, AWK e SED começaram a ganhar terreno e, especialmente, SED se tornou significativamente mais rápido com matrizes , enquanto GREP ficou para trás.
Penalidades de invocação para ferramentas dedicadas como AWK, SED e especialmente GREP:
Eles sofrem uma queda significativa de desempenho para conjuntos <=1000 em muitas iterações, começando em ~50-100 loops e crescendo exponencialmente a partir daí, deixando máquinas lentas em séria desvantagem.
Portanto, use matrizes em vez de strings delimitadas por (novas linhas) sempre que possível e aplique a atribuição de matriz nativa do Bash , especialmente ao criar e trabalhar com grandes conjuntos de elementos, e considere usá-la
PARAMETER SUBSTITUTION
ao filtrá-los.Criar e filtrar uma matriz por meio de - se for usada uma CORRESPONDÊNCIA DE PADRÕES simples e
PSUB/PMATCH
cuidadosamente elaborada - e então armazenar o resultado novamente por meio da atribuição de matriz /native, pode ter um impacto tremendamente positivo no desempenho, em comparação a outras abordagens!IFS=$'\n'
Converter de strings delimitadas por nova linha com restrito
IFS
:Ou, para coletar/excluir-filtrar de uma matriz, basta fazer (e filtrar os vazios depois):
Isso terá um desempenho muito melhor do que um loop de coleção dedicado e significativamente melhor do que
readarray
- particularmente, quando a criação do array em si é feita em loop. E pode converter de forma muito eficiente strings de elementos delimitados por nova linha existentes em arrays muito mais processáveis.Ele deve ter um desempenho muito bom para até 1.000.000 de elementos , em comparação com qualquer outra ferramenta dedicada como AWK, SED ou GREP, ou pode até mesmo superá- los drasticamente, principalmente para um número de elementos abaixo de 10.000 , e cada vez mais, com mais iterações .
O esforço de criar novas matrizes via nativo
array+=(...)
é mínimo; coletar matrizes de resultados filtrados novamente e depois processá-las definitivamente supera o processamento de grandes strings concatenadas em todos os casos de uso possíveis, exceto nos menores e mais simples.Por outro lado , criar grandes conjuntos de strings de concatenação separadas por novas linhas em um loop dedicado tem um desempenho muito ruim e pode até mesmo travar para strings muito grandes >1.000.000 de elementos , então a vantagem de desempenho que GREP ou SED podem oferecer aqui para processamento, em comparação ao uso de matrizes, é frustrada desde o início, devido aos custos de concatenação da criação de strings em geral.
FOR/WHILE read -r
, com (simples)PATTERN MATCHING
(PMATCH) por elemento também pode ser uma boa opção, que ainda deve superar as ferramentas dedicadas usuais, como AWK, SED e GREP.Desde que a
Here Document
técnica de expansão direta de matriz/PMATCH seja usada:Não o streaming muito mais caro via
Pipe
,Sub Process' or
Process Substitution` como:Mas cuidado , em máquinas lentas o desempenho pode cair muito , começando mesmo com ~50 elementos em >10 iterações. Para hardware rápido, pode ser ~1000 elementos em bem mais de 100 iterações, até que as coisas fiquem notavelmente lentas.
Porém, se o número de elementos permanecer abaixo dessas margens críticas,
LOOP/PMATCH
o comportamento de dimensionamento do loop externo será muito melhor do que as chamadas de programas externos, como GREP, superado apenas pela abordagem PSUB.Das ferramentas dedicadas,
AWK
deve apresentar o melhor desempenho, sendo significativamente mais lento, enquanto é melhor eGREP
fica um pouco atrás.RegEX
SED/replace
SED/delete
Abaixo de ~50 elementos , os custos de invocação de AWK, SED e GREP aumentarão significativamente o tempo de execução consumido, em comparação aos
for/while
loops, ~4x ou mais.Aqui, os loops de filtro dedicados, ou
PSUB
geralmente apresentam melhor desempenho.Acima de ~100 elementos , ferramentas dedicadas superarão rapidamente a filtragem com loops explícitos, se a vantagem não for anulada novamente por muitas invocações em um loop.
Para um número muito maior de loops de encapsulamento, os loops de filtragem explícitos devem vencer a corrida, desde que
PATTERN MATCHING
sejam restritos a padrões simples .Caso contrário, para ~1000 elementos ou mais , AWK, SED e GREP devem mostrar desempenho superior (~10x ou melhor), em relação a
for/while
loops explícitos comPATTERN MATCHING
.Porém, somente para matrizes muito grandes (>~1.000.000) eles conseguiram superar a enorme vantagem inicial de desempenho
PSUB
.Para muitas iterações/requisitos de alto desempenho, não se atenha dogmaticamente ao
DRY
princípio: no script Bash, cada comando simples iterado comoif
pode ter um impacto significativo nos tempos de execução.Se o desempenho for insuficiente, agrupar caminhos de comutação dedicados no loop de código semelhante em loops separados e comutados externamente e EVITAR TODO O CÓDIGO NÃO NECESSÁRIO DENTRO dos loops pode fazer a diferença:
Comparando tempos de execução de ramificação de loop/condição 1.000.000 para iterações:
-> 48,4s
-> 61,8s
Cerca de 13% do tempo de execução poderia ser economizado , embora ao custo de alta redundância de código.
Os tempos de criação para conjuntos de teste array vs. concat string var diferem imensamente: quanto mais elementos, mais extremo.
Enquanto uma string concat é gerada muito rapidamente a partir de uma matriz existente, o tempo para construir grandes strings concatenadas e separadas por novas linhas em um loop dedicado cresce desproporcionalmente muito rápido em máquinas lentas (aqui: 4-Core).
Uma matriz de 1.000.000 de elementos ainda era construída em ~4s, e convertê-la em uma string de concat levaria apenas 0,8s, mas construir diretamente uma string do mesmo tamanho de elementos em um loop consumiria ~127s :
EM GERAL:
Ao filtrar matrizes em um loop grande, usar uma
FILTERING ARRAYS
expressão apropriada deve ser de longe a solução mais eficiente e rápida.Com as ferramentas usuais, deveria ser o contrário; devido às suas penalidades de invocação, elas ficarão muito para trás com muitas iterações e poucos itens, mas podem chegar ao topo, com grandes quantidades de elementos em apenas algumas iterações.
Aqui estão alguns números dos resultados dos testes:
Para matrizes muito grandes (acima de 1.00.000), pode valer a pena usar uma ferramenta dedicada, principalmente GREP e AWK:
1x 1.000.000 em ~0,9s pelo GREP, comparado ao próximo melhor ~1,7s pelo AWK e PSUB.
Para filtrar um único conjunto apenas uma vez, SED(em 8 núcleos)/GREP(em 4 núcleos) apresentam o melhor desempenho, seguidos por AWK e PSUB.
Para grandes sequências de VAR - dependendo da complexidade do
RegEx
uso - GREP mostra o melhor desempenho geral, seguido por AWK e depois SED.O PSUB não foi considerado para este caso de uso, o que seria um exagero.
O desempenho das ferramentas dedicadas cai em ~1/2, ao processar ARRAYS, para aproximadamente o mesmo rendimento, conforme observado com o PSUB, que está tendo desempenho igual ou melhor.
Alguns diagramas para ilustrar:
CONFIGURAÇÃO DO TESTE:
Um filtro mais próximo possível/de melhor desempenho
RegEx
(^[ ?].*
), substituição (${var/#[? ]*}
) ou padrão de comparação (!= [\ \?]*
) foi aplicado para cada uma das seguintes abordagens de filtragem:[
PSUB from VAR
] -> ignorado, desempenho muito baixo para qualquer requisito, exceto os mais baixos!The test scenario was to filter out all
git status -s
changes beginning with[ ?]
(untracked + unstaged), from a given set of 10 changes elements, which would then be multiplied tenfold for (100x1 -> 1000 elements), per each of the test cases with higher requirements, and/or again looped 10x more often each time, beginning at 1 iteration, then 10, 100, 1k, 10k, 100k, up to 1M loops (were still feasible).In order to get the best possible test runtime measurement precision, start and stop test times were stored and summed up separately for each approach.
The tests were performed on 2 different test machines, an Intel 4-Core notebook (Cygwin/MinGW) and an AMD 8-Core workstation (Linux/Bash).
'AND THE WINNER IS':
By far the best performing variant over the charts is the native
PARAMETER SUBSTITUTION
inARRAY
deletion method, provided, only simplePattern Matching
expressions are used. On the other hand, extended Pattern Matching expressions like[+*@?!](...)
can quickly spoil the performance advantage.One drawback is, that this method (with native array collecting) will create aresult set containing empty entries. If empties are no problem, these can easily be filtered out later, while processing the result array.
The performance rewards can be tremendous, though.
For many iterations of small sets, it outperformed dedicated tools by ~10-20x; with numerous wrapping loops, applying this technique would clearly win over external program calls anyway, too.
CAVEAT:
The tests should be extended to more complex filters, comparing these results, too.