Performance em PowerShell
LidoEsta aula ensina técnicas essenciais para melhorar a performance de scripts PowerShell, abordando desde a escolha de estruturas de dados até o uso de métodos .NET e medição de tempo. Você aprenderá a evitar armadilhas comuns como o operador += em arrays, a otimizar o uso de pipelines e a aproveitar a potência do .NET para operações críticas.
Performance em PowerShell é um tópico crucial para quem deseja escrever scripts que executam de forma rápida e eficiente, especialmente quando lidamos com grandes volumes de dados. Nesta aula, vamos explorar técnicas práticas para otimizar seus scripts, desde a escolha das estruturas de dados até a medição precisa do tempo de execução. Você verá como pequenas mudanças podem ter um impacto significativo no desempenho geral.
PowerShell é uma linguagem poderosa, mas seu dinamismo e facilidade de uso podem levar a armadilhas de performance se não tomarmos cuidado. Ao entender como o PowerShell funciona por baixo dos panos e aplicar boas práticas, você poderá escrever scripts que rodam em uma fração do tempo original.
Evitando += em arrays
Um dos erros mais comuns em scripts PowerShell é usar o operador += para adicionar elementos a um array. Isso parece inofensivo, mas por trás dos panos, o PowerShell cria um novo array a cada iteração e copia todos os elementos existentes mais o novo. Isso resulta em uma complexidade O(n²), o que se torna extremamente lento para grandes conjuntos de dados.
Em vez de usar arrays para acumular resultados, utilize uma List<T> do .NET, que é projetada para crescer dinamicamente sem a necessidade de recriar toda a estrutura. Veja um exemplo comparativo:
# Abordagem lenta com += em array
$array = @()
for ($i = 0; $i -lt 10000; $i++) {
$array += $i
}
# Abordagem eficiente com List[int]
$list = [System.Collections.Generic.List[int]]::new()
for ($i = 0; $i -lt 10000; $i++) {
$list.Add($i)
}
# Converter para array se necessário
$array = $list.ToArray()Além de List<T>, você pode usar ArrayList (menos recomendado por não ser genérico) ou até mesmo StringBuilder para concatenar strings. O princípio é sempre o mesmo: evite operações que recriam a estrutura de dados a cada iteração.
Pipeline vs loops
O pipeline do PowerShell é um dos seus recursos mais elegantes, permitindo encadear comandos de forma concisa. No entanto, o pipeline introduz uma sobrecarga significativa devido à passagem de objetos entre comandos. Para operações simples, um loop foreach tradicional pode ser muito mais rápido.
Vamos comparar a performance de um filtro simples usando pipeline e usando um loop:
$numbers = 1..100000
# Usando pipeline
$filtered = $numbers | Where-Object { $_ % 2 -eq 0 }
# Usando loop foreach
$result = [System.Collections.Generic.List[int]]::new()
foreach ($n in $numbers) {
if ($n % 2 -eq 0) { $result.Add($n) }
}
$filtered = $result.ToArray()Em muitos casos, o loop é mais rápido porque evita a criação de objetos intermediários e a sobrecarga de invocação de funções. No entanto, o pipeline brilha em legibilidade e em cenários onde você precisa encadear múltiplas operações. A regra geral é: para scripts simples, use pipeline; para processamento pesado de dados, prefira loops ou métodos .NET.
.NET para performance
PowerShell é construído sobre o .NET Framework (ou .NET Core), e você pode acessar diretamente classes e métodos .NET para obter ganhos de performance significativos. Isso é especialmente útil para operações matemáticas, manipulação de strings e coleções.
Por exemplo, para somar uma lista de números, você pode usar o método Sum() da classe Linq.Enumerable em vez de um loop:
$numbers = 1..100000
# Usando LINQ
$sum = [System.Linq.Enumerable]::Sum([int[]]$numbers)
# Usando loop
$sum = 0
foreach ($n in $numbers) { $sum += $n }Outro exemplo é a conversão de tipos: Convert.ToInt32() é mais rápido que [int]::Parse() em alguns contextos, e para strings, StringBuilder é muito mais eficiente que a concatenação com + ou +=.
# Concatenação ineficiente
$str = ''
for ($i = 0; $i -lt 1000; $i++) { $str += 'a' }
# Com StringBuilder
$sb = [System.Text.StringBuilder]::new()
for ($i = 0; $i -lt 1000; $i++) { [void]$sb.Append('a') }
$str = $sb.ToString()Explore as bibliotecas .NET disponíveis: System.IO para arquivos, System.Net para rede, System.Collections.Generic para coleções. Usar essas classes diretamente pode reduzir drasticamente o tempo de execução.
Medindo (Measure-Command)
Para otimizar, você precisa medir. O cmdlet Measure-Command é a ferramenta básica para cronometrar a execução de um bloco de script. Ele retorna um objeto TimeSpan com a duração.
$time = Measure-Command {
# código a medir
Start-Sleep -Milliseconds 500
}
$time.TotalMillisecondsPara comparações mais precisas, execute várias vezes e calcule a média. Você pode usar um loop:
$times = @()
for ($i = 0; $i -lt 10; $i++) {
$time = Measure-Command { 1..100000 | Where-Object { $_ % 2 -eq 0 } }
$times += $time.TotalMilliseconds
}
$average = ($times | Measure-Object -Average).Average
"Tempo médio: $average ms"Existem também ferramentas mais avançadas como o PSProfiler (módulo da comunidade) e o Trace-Command para análise detalhada, mas Measure-Command é suficiente para a maioria das otimizações.
Boas práticas e observações finais
Além das técnicas acima, lembre-se de:
- Evitar o uso excessivo de
Write-Hostem loops grandes, pois a saída para o console é lenta. Prefira coletar resultados e exibi-los no final. - Usar
try/finallypara garantir a liberação de recursos (como arquivos abertos). - Desativar a transcrição ou logging em produção se não for necessário.
- Utilizar
using namespacepara simplificar o acesso a classes .NET. - Quando possível, usar funções nativas do PowerShell (como
Sort-Object) em vez de implementar algoritmos próprios, pois elas são otimizadas.
Lembre-se: a otimização prematura pode complicar seu código. Sem meça primeiro, identifique os gargalos e então aplique as técnicas adequadas.
Referências
- Measure-Command (Microsoft Learn)
- List
Class (Microsoft Docs) - StringBuilder Class (Microsoft Docs)
- Everything about arrays (PowerShell)
- Everything about pipelines (PowerShell)
- LINQ (Language-Integrated Query) - Microsoft Docs
Exercícios
- Exercício 1: Crie um script que use
+=para construir um array de 10.000 inteiros e meça o tempo comMeasure-Command. Depois, refatore para usarList[int]e compare os tempos. Qual é a diferença aproximada? - Exercício 2: Dado um array de 100.000 números, compare o tempo de filtrar números pares usando pipeline (
Where-Object) e usando um loopforeachcomList[int]. Qual é mais rápido? Por quê? - Exercício 3: Escreva um script que some os números de 1 a 1.000.000 usando três abordagens: um loop simples, o cmdlet
Measure-Objecte o métodoSum()do LINQ. Meça o tempo de cada um e identifique o mais rápido. - Exercício 4: Crie uma string de 10.000 caracteres usando concatenação com
+=e depois comStringBuilder. Meça o tempo de cada um. Explique a diferença. - Exercício 5: Usando
Measure-Command, meça o tempo de execução de um script que lê um arquivo de texto linha por linha usandoGet-Contente usandoStreamReaderdo .NET. Qual é mais rápido e por quê?
+= será significativamente mais lento (pode ser 10x ou mais). Exemplo de código:$time1 = Measure-Command {
$array = @()
for ($i = 0; $i -lt 10000; $i++) { $array += $i }
}
$time2 = Measure-Command {
$list = [System.Collections.Generic.List[int]]::new()
for ($i = 0; $i -lt 10000; $i++) { $list.Add($i) }
}
"Tempo +=: $($time1.TotalMilliseconds) ms"
"Tempo List: $($time2.TotalMilliseconds) ms"foreach tende a ser mais rápido porque evita a sobrecarga do pipeline. Exemplo de comparação:$numbers = 1..100000
$t1 = Measure-Command { $even = $numbers | Where-Object { $_ % 2 -eq 0 } }
$t2 = Measure-Command {
$list = [System.Collections.Generic.List[int]]::new()
foreach ($n in $numbers) { if ($n % 2 -eq 0) { $list.Add($n) } }
$even = $list.ToArray()
}
"Pipeline: $($t1.TotalMilliseconds) ms"
"Loop: $($t2.TotalMilliseconds) ms"Sum() geralmente é o mais rápido. Exemplo:$numbers = 1..1000000
$t1 = Measure-Command { $sum = 0; foreach ($n in $numbers) { $sum += $n } }
$t2 = Measure-Command { $sum = ($numbers | Measure-Object -Sum).Sum }
$t3 = Measure-Command { $sum = [System.Linq.Enumerable]::Sum([int[]]$numbers) }
"Loop: $($t1.TotalMilliseconds) ms"
"Measure-Object: $($t2.TotalMilliseconds) ms"
"LINQ: $($t3.TotalMilliseconds) ms"StringBuilder é muito mais rápido porque não recria a string a cada append. Exemplo:$t1 = Measure-Command {
$str = ''
for ($i = 0; $i -lt 10000; $i++) { $str += 'a' }
}
$t2 = Measure-Command {
$sb = [System.Text.StringBuilder]::new()
for ($i = 0; $i -lt 10000; $i++) { [void]$sb.Append('a') }
$str = $sb.ToString()
}
"+=: $($t1.TotalMilliseconds) ms"
"StringBuilder: $($t2.TotalMilliseconds) ms"StreamReader é mais rápido porque lê o arquivo de forma mais eficiente, sem carregar todo o conteúdo na memória de uma vez. Exemplo:$file = 'C:\temp\large.txt'
$t1 = Measure-Command { Get-Content $file }
$t2 = Measure-Command {
$reader = [System.IO.StreamReader]::new($file)
try { while ($null -ne $reader.ReadLine()) {} }
finally { $reader.Dispose() }
}
"Get-Content: $($t1.TotalMilliseconds) ms"
"StreamReader: $($t2.TotalMilliseconds) ms"