Guia de Desempenho

Esta página apresenta as características de desempenho do inewave, com base nos resultados da suite de benchmarks incluída no repositório. Os números apresentados são aproximados e dependem do hardware, da versão do Python e do estado do cache de módulos no momento da execução.

Nota

Os valores de tempo apresentados nesta página foram obtidos em um ambiente Linux x86-64 com Python 3.14. Em outras configurações de hardware ou versão do Python, os tempos podem variar significativamente.

Visão Geral de Desempenho

O custo de uso do inewave se distribui em três dimensões principais:

  • Tempo de importação: custo único de carregamento do módulo em cada processo Python. É dominado pelo primeiro acesso ao pacote inewave de nível superior.

  • Tempo de leitura: custo de parsing e construção do DataFrame para cada arquivo lido do disco. Varia conforme a complexidade e o tipo do arquivo (número de patamares, quantidade de seções, etc.).

  • Tempo de agregação: custo de acessar a propriedade .valores() nas classes de agregação do NWLISTOP, que dispara a concatenação dos blocos de dados internos.

A tabela a seguir apresenta um resumo de tempos representativos medidos com dados de referência (arquivos mockados, sem I/O real de disco):

Tempos aproximados por operação

Operação

Tipo de arquivo / Handler

Tempo aproximado

Memória pico (Python)

import inewave

Pacote de nível superior (cache parcialmente quente)

~360 ms

~33 MB

import inewave.nwlistop

Sub-pacote (após importação do nível superior)

~5 ms

~0,3 MB

Leitura — não-patamar (REE/SIN)

Earmf, Earmfsin, Earmfp

~70 ms

~9 MB

Leitura — patamar Submercado

Cmarg

~210 ms

~28 MB

Leitura — SectionFile Newave

Pmo

~644 ms

~21 MB

Leitura — binário Newave

Hidr (agregação)

~84 ms

~1 MB

Agregação .valores()

Earmf, Cmarg, Earmfp (dados mockados)

~0,1 ms

< 0,1 MB

Tempos de Importação

O inewave utiliza o padrão de importação preguiçosa (lazy import) em todos os seus sub-pacotes. O mecanismo é implementado via a função especial __getattr__ em cada __init__.py, que registra um dicionário _LAZY_IMPORTS mapeando nomes de classe para módulos internos. A classe em questão só é carregada de fato quando é acessada pela primeira vez — e então armazenada no escopo global do pacote para que acessos subsequentes sejam imediatos.

Consequência prática:

  • import inewave leva aproximadamente ~360 ms em um processo com cache parcialmente quente (pandas e numpy já carregados). Esse custo reflete a travessia do grafo de módulos do Python para um pacote com muitos sub-módulos.

  • import inewave.nwlistop ou import inewave.newave leva apenas ~5 ms, pois as dependências compartilhadas já estão resolvidas.

  • O acesso a uma classe individual como inewave.nwlistop.earmf leva ~7 ms.

Nota

O custo de ~360 ms ocorre uma única vez por processo. Em scripts de processamento em lote que processam centenas de arquivos, esse custo é diluído e se torna irrelevante.

Para minimizar o custo de importação em scripts que usam poucas classes, prefira importar o sub-pacote ou a classe diretamente, em vez de importar o pacote de nível superior:

# Padrão recomendado para poucos arquivos do mesmo sub-pacote:
from inewave.newave import Dger, Confhd

# Ainda mais direto — importa apenas o módulo específico:
from inewave.newave.dger import Dger

# Evite isto se o pacote principal ainda não foi importado
# e você só precisa de uma ou duas classes:
import inewave  # importa o grafo inteiro (~360 ms)

Tempos de Leitura

O custo de leitura de cada arquivo é medido sem I/O real de disco — os benchmarks usam mock_open com os mesmos arquivos de teste da suite de testes unitários. O tempo reflete, portanto, o custo de parsing e construção do DataFrame, que é o custo dominante em sistemas com SSD ou em processamento em memória.

Os principais fatores que influenciam o tempo de leitura são:

Número de patamares

Arquivos do NWLISTOP com múltiplos patamares (como Cmarg) são aproximadamente 3x mais lentos que arquivos sem patamar (como Earmf), pois cada patamar adiciona um conjunto de chamadas ao campo FloatField._textual_read no parser.

Complexidade de seções

O Pmo (SectionFile do Newave) é o arquivo mais lento (~644 ms) porque seu método converte_tabela_em_df aplica DataFrame.apply e DataFrame.melt em cada bloco de seção. O profiler atribui aproximadamente 171 ms dessas operações às chamadas ao pandas.

Distribuição de tempo no pipeline de leitura

Para os arquivos do NWLISTOP, o parsing via TabularParser.parse_lines consome entre 28–32% do tempo total. A construção do DataFrame via _build_dataframe e formata_df_meses_para_datas_nwlistop consome cerca de 21%. O restante (~50%) corresponde ao despacho de blocos do cfinterface e ao overhead Python.

Otimização para Processamento em Lote

Ao processar centenas ou milhares de arquivos NWLISTOP, as estratégias a seguir reduzem significativamente o tempo total de execução.

1. Importe a classe uma única vez, fora do loop

O lazy import é resolvido na primeira vez que a classe é acessada. Importar dentro do loop provoca a resolução desnecessária do nome a cada iteração:

from inewave.nwlistop import Cmarg

caminhos = [f"./nwlistop/cmarg{i:03d}.out" for i in range(1, 201)]

resultados = []
for caminho in caminhos:
    arq = Cmarg.read(caminho)
    resultados.append(arq.valores)

2. Prefira imports de sub-pacote ou de módulo direto

Se o script utiliza apenas classes de um sub-pacote específico, importe diretamente do sub-pacote para evitar o custo de travessia do pacote de nível superior:

# Em vez de:
from inewave.newave import Dger, Pmo, Confhd

# Importe o sub-pacote diretamente quando precisar de poucos arquivos:
from inewave.newave.dger import Dger

3. Paralelize a leitura com multiprocessing ou concurrent.futures

Cada chamada a read é independente e não há estado global compartilhado entre instâncias. É seguro paralelizar a leitura usando ProcessPoolExecutor ou multiprocessing.Pool:

from concurrent.futures import ProcessPoolExecutor
from inewave.nwlistop import Earmf

def ler_arquivo(caminho):
    return Earmf.read(caminho).valores

caminhos = [f"./nwlistop/earmf{i:03d}.out" for i in range(1, 201)]

with ProcessPoolExecutor() as executor:
    resultados = list(executor.map(ler_arquivo, caminhos))

Nota

Use ProcessPoolExecutor (múltiplos processos) em vez de ThreadPoolExecutor (múltiplas threads). O parsing em Python é limitado pelo GIL — o paralelismo real só é obtido com processos separados.

Suite de Benchmarks

O repositório inclui uma suite de benchmarks em benchmarks/ que pode ser executada para medir o desempenho na sua própria máquina e comparar resultados entre versões. Todos os comandos devem ser executados a partir da raiz do repositório.

Suite completa de benchmarks

Executa benchmarks de importação, leitura e agregação com 10 iterações por padrão e grava os resultados em benchmarks/benchmark_results.md:

python benchmarks/run_benchmarks.py

Para aumentar o número de iterações e obter medições mais estáveis:

python benchmarks/run_benchmarks.py --iterations 20

Em execuções subsequentes, uma coluna Delta é populada automaticamente, comparando as médias atuais com os valores da execução anterior. Um Delta positivo indica que a execução atual foi mais lenta.

Profiler por fase

Mede o tempo de parede dividido por fase interna (parsing, construção do DataFrame, formatação, concatenação) para cada tipo de arquivo representativo e grava os resultados em benchmarks/profile_report.md:

python benchmarks/profile_read.py

Interpretando os resultados

O arquivo benchmarks/benchmark_results.md contém três tabelas — importação, leitura e agregação — com as seguintes colunas:

  • Mean (s): média aritmética dos tempos de parede de todas as iterações. É a métrica principal de comparação.

  • Median (s): mediana dos tempos. Menos sensível a outliers do que a média.

  • Min (s): iteração mais rápida. Aproxima o desempenho no melhor caso.

  • Std Dev (s): desvio padrão amostral. Valores altos indicam medições ruidosas — considere aumentar o número de iterações.

  • Peak Memory (MB): pico de heap Python medido pelo tracemalloc. Captura apenas alocações Python; buffers C-level do numpy e pandas não são contabilizados.

O arquivo benchmarks/profile_report.md contém uma tabela de fases com as colunas Parsing %, DataFrame %, Aggregation % e Other %, que indicam qual fração do tempo total de leitura foi consumida por cada fase.

Nota

As porcentagens de fase no profile_report.md podem não somar 100%. As fases não são mutuamente exclusivas: _build_dataframe chama formata_df_meses_para_datas_nwlistop internamente, de modo que as colunas DataFrame % e Parsing % podem se sobrepor.

Limitações Conhecidas

Aviso

Escalonamento O(n²) na agregação ``__monta_tabela``: o método interno __monta_tabela, acionado pela propriedade .valores() das classes de agregação do NWLISTOP, utiliza um padrão de pd.concat incremental que escala como O(n²) em relação ao número de blocos de anos no arquivo. Com os dados mockados da suite de testes (um único bloco de ano), o tempo medido (~0,1 ms) é desprezível. Em arquivos de produção reais com 20 ou mais blocos de anos, o custo pode ser significativamente maior do que os benchmarks sugerem. Não conclua, a partir dos tempos de agregação medidos, que essa operação é rápida em produção.

Medições de memória são apenas no nível Python

A coluna Peak Memory (MB) usa tracemalloc, que rastreia apenas alocações feitas por objetos Python. Buffers internos do numpy e do pandas (arrays C-level) não são capturados. O consumo real de memória residente do processo é maior do que o valor registrado.

Benchmarks usam dados mockados, não arquivos reais

Os benchmarks de leitura usam mock_open com os mesmos arquivos de teste da suite de testes unitários. Esses arquivos mockados contêm apenas uma fração dos dados de um arquivo real de produção. O tempo de leitura de arquivos reais, que podem ser ordens de grandeza maiores, será proporcionalmente maior.

Estado de cache de importação parcialmente quente

Os benchmarks de importação limpam apenas as entradas inewave.* do sys.modules entre iterações. O cfinterface, o pandas e o numpy permanecem carregados. Os tempos medidos representam o custo marginal de carregar o próprio grafo de módulos do inewave — não o custo de um processo totalmente frio.