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.TotalMilliseconds

Para 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-Host em loops grandes, pois a saída para o console é lenta. Prefira coletar resultados e exibi-los no final.
  • Usar try/finally para 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 namespace para 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

Exercícios

  1. Exercício 1: Crie um script que use += para construir um array de 10.000 inteiros e meça o tempo com Measure-Command. Depois, refatore para usar List[int] e compare os tempos. Qual é a diferença aproximada?
  2. ✓ Resposta: O script com += 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"
  3. 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 loop foreach com List[int]. Qual é mais rápido? Por quê?
  4. ✓ Resposta: O loop 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"
  5. 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-Object e o método Sum() do LINQ. Meça o tempo de cada um e identifique o mais rápido.
  6. ✓ Resposta: O método LINQ 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"
  7. Exercício 4: Crie uma string de 10.000 caracteres usando concatenação com += e depois com StringBuilder. Meça o tempo de cada um. Explique a diferença.
  8. ✓ Resposta: 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"
  9. 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 usando Get-Content e usando StreamReader do .NET. Qual é mais rápido e por quê?
  10. ✓ Resposta: 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"