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
inewavede 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):
Operação |
Tipo de arquivo / Handler |
Tempo aproximado |
Memória pico (Python) |
|---|---|---|---|
|
Pacote de nível superior (cache parcialmente quente) |
~360 ms |
~33 MB |
|
Sub-pacote (após importação do nível superior) |
~5 ms |
~0,3 MB |
Leitura — não-patamar (REE/SIN) |
|
~70 ms |
~9 MB |
Leitura — patamar Submercado |
|
~210 ms |
~28 MB |
Leitura — SectionFile Newave |
|
~644 ms |
~21 MB |
Leitura — binário Newave |
|
~84 ms |
~1 MB |
Agregação |
|
~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 inewaveleva 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.nwlistopouimport inewave.newaveleva apenas ~5 ms, pois as dependências compartilhadas já estão resolvidas.O acesso a uma classe individual como
inewave.nwlistop.earmfleva ~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 (comoEarmf), pois cada patamar adiciona um conjunto de chamadas ao campoFloatField._textual_readno parser.- Complexidade de seções
O
Pmo(SectionFiledo Newave) é o arquivo mais lento (~644 ms) porque seu métodoconverte_tabela_em_dfaplicaDataFrame.applyeDataFrame.meltem 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_linesconsome entre 28–32% do tempo total. A construção do DataFrame via_build_dataframeeformata_df_meses_para_datas_nwlistopconsome 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_opencom 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.*dosys.modulesentre 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.